proj-flow 0.11.2__tar.gz → 0.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. {proj_flow-0.11.2 → proj_flow-0.12.0}/CHANGELOG.rst +16 -0
  2. {proj_flow-0.11.2 → proj_flow-0.12.0}/PKG-INFO +1 -1
  3. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/__init__.py +1 -1
  4. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/release.py +18 -0
  5. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/cmd.py +12 -0
  6. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/cmake/__init__.py +2 -2
  7. proj_flow-0.11.2/src/proj_flow/ext/cplusplus/cmake/version.py → proj_flow-0.12.0/src/proj_flow/ext/cplusplus/cmake/project.py +0 -4
  8. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/github/cli.py +65 -5
  9. proj_flow-0.12.0/src/proj_flow/ext/github/publishing.py +65 -0
  10. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/python/rtdocs.py +2 -11
  11. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/sign/__init__.py +4 -5
  12. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/sign/api.py +1 -1
  13. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/sign/win32.py +3 -1
  14. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/store.py +1 -11
  15. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/commit.py +45 -2
  16. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/hosting/github.py +72 -6
  17. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/release.py +4 -3
  18. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.flow/flow.py.mustache +19 -12
  19. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.gitignore +2 -0
  20. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix.cmake.mustache +2 -1
  21. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions/.github/workflows/build.yml +3 -5
  22. {proj_flow-0.11.2 → proj_flow-0.12.0}/.flow/config.yaml +0 -0
  23. {proj_flow-0.11.2 → proj_flow-0.12.0}/.gitignore +0 -0
  24. {proj_flow-0.11.2 → proj_flow-0.12.0}/.readthedocs.yaml +0 -0
  25. {proj_flow-0.11.2 → proj_flow-0.12.0}/LICENSE +0 -0
  26. {proj_flow-0.11.2 → proj_flow-0.12.0}/README.md +0 -0
  27. {proj_flow-0.11.2 → proj_flow-0.12.0}/pyproject.toml +0 -0
  28. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/__main__.py +0 -0
  29. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/__init__.py +0 -0
  30. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/arg.py +0 -0
  31. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/completers.py +0 -0
  32. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/ctx.py +0 -0
  33. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/env.py +0 -0
  34. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/init.py +0 -0
  35. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/makefile.py +0 -0
  36. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/api/step.py +0 -0
  37. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/__cmake_version__.py +0 -0
  38. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/__init__.py +0 -0
  39. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/inspect.py +0 -0
  40. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/matrix.py +0 -0
  41. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/name_list.py +0 -0
  42. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/plugins.py +0 -0
  43. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/registry.py +0 -0
  44. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/base/uname.py +0 -0
  45. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/cli/__init__.py +0 -0
  46. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/cli/argument.py +0 -0
  47. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/cli/finder.py +0 -0
  48. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/dependency.py +0 -0
  49. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/__init__.py +0 -0
  50. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/__init__.py +0 -0
  51. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/cmake/parser.py +0 -0
  52. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/cmake/steps.py +0 -0
  53. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/conan/__init__.py +0 -0
  54. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/cplusplus/conan/_conan.py +0 -0
  55. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/github/__init__.py +0 -0
  56. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/github/hosting.py +0 -0
  57. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/github/switches.py +0 -0
  58. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/markdown_changelog.py +0 -0
  59. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/python/__init__.py +0 -0
  60. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/python/steps.py +0 -0
  61. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/python/version.py +0 -0
  62. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/ext/re_structured_changelog.py +0 -0
  63. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/flow/__init__.py +0 -0
  64. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/flow/configs.py +0 -0
  65. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/flow/layer.py +0 -0
  66. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/flow/steps.py +0 -0
  67. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/__init__.py +0 -0
  68. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/error.py +0 -0
  69. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/fmt.py +0 -0
  70. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/format.py +0 -0
  71. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/hosting/__init__.py +0 -0
  72. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/msg.py +0 -0
  73. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/rich_text/__init__.py +0 -0
  74. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/rich_text/api.py +0 -0
  75. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/rich_text/markdown.py +0 -0
  76. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/log/rich_text/re_structured_text.py +0 -0
  77. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/__init__.py +0 -0
  78. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/base.py +0 -0
  79. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/bootstrap.py +0 -0
  80. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/init.py +0 -0
  81. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/list.py +0 -0
  82. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/run.py +0 -0
  83. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/minimal/system.py +0 -0
  84. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/__init__.py +0 -0
  85. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/api.py +0 -0
  86. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/cplusplus/__init__.py +0 -0
  87. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/cplusplus/cmake_context.py +0 -0
  88. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/cplusplus/conan_context.py +0 -0
  89. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/cplusplus/project.py +0 -0
  90. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/data.py +0 -0
  91. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/project/interact.py +0 -0
  92. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.clang-format +0 -0
  93. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.flow/config.yml +0 -0
  94. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.flow/matrix.yml +0 -0
  95. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/.flow/official.yml +0 -0
  96. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/README.md.mustache +0 -0
  97. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/flow +0 -0
  98. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base/flow.cmd +0 -0
  99. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/base.json +0 -0
  100. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/cmake/common.cmake.mustache +0 -0
  101. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/__init__.py.mustache +0 -0
  102. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/icons/__init__.py.mustache +0 -0
  103. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/icons/magick.py.mustache +0 -0
  104. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/base.cmake.mustache +0 -0
  105. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/config.cmake +0 -0
  106. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/cpack.cmake +0 -0
  107. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix/cpack.cmake.mustache +0 -0
  108. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix/patches.in.wix +0 -0
  109. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/CMakeGraphVizOptions.cmake +0 -0
  110. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/CMakeLists.txt.mustache +0 -0
  111. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/CMakePresets.json +0 -0
  112. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/assets/appicon.ico +0 -0
  113. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/assets/appicon.png +0 -0
  114. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/assets/wix_banner.bmp +0 -0
  115. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/assets/wix_dialog.bmp +0 -0
  116. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/icons/.gitignore +0 -0
  117. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/icons/appicon-mask.svg +0 -0
  118. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/data/icons/appicon.svg +0 -0
  119. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/src/main.cc.mustache +0 -0
  120. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/src/version.hpp.in.mustache +0 -0
  121. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake/tests/test.cc.mustache +0 -0
  122. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/cmake.json +0 -0
  123. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/conan/.flow/cmake/libcxx_toolchain.cmake +0 -0
  124. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/conan/.flow/cmake/output_dirs_setup.cmake +0 -0
  125. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/conan/conanfile.txt +0 -0
  126. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/conan.json +0 -0
  127. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions/.github/linters/.isort.cfg +0 -0
  128. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions/.github/linters/.mypy.ini +0 -0
  129. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions/.github/workflows/linter.yml +0 -0
  130. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions/CPPLINT.cfg +0 -0
  131. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_actions.json +0 -0
  132. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/bug_report.md.mustache +0 -0
  133. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/feature_request.md.mustache +0 -0
  134. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_social/CODE_OF_CONDUCT.md.mustache +0 -0
  135. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_social/CONTRIBUTING.md +0 -0
  136. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/layers/github_social.json +0 -0
  137. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/licenses/0BSD.mustache +0 -0
  138. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/licenses/MIT.mustache +0 -0
  139. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/licenses/Unlicense.mustache +0 -0
  140. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/licenses/WTFPL.mustache +0 -0
  141. {proj_flow-0.11.2 → proj_flow-0.12.0}/src/proj_flow/template/licenses/Zlib.mustache +0 -0
@@ -4,6 +4,22 @@ Changelog
4
4
 
5
5
  All notable changes to this project will be documented in this file.
6
6
 
7
+ `0.12.0 <https://github.com/mzdun/proj-flow/compare/v0.11.3...v0.12.0>`_ (2025-02-22)
8
+ =====================================================================================
9
+
10
+ New Features
11
+ ------------
12
+
13
+ - add ``github publish`` (`2f6ef1e <https://github.com/mzdun/proj-flow/commit/2f6ef1eaf4053f25633bf3f5037991fa4567023b>`_)
14
+
15
+ `0.11.3 <https://github.com/mzdun/proj-flow/compare/v0.11.2...v0.11.3>`_ (2025-02-19)
16
+ =====================================================================================
17
+
18
+ Bug Fixes
19
+ ---------
20
+
21
+ - finish sign changes (`14c19b5 <https://github.com/mzdun/proj-flow/commit/14c19b503ce7859808888a30b64be01cc9a3e047>`_)
22
+
7
23
  `0.11.2 <https://github.com/mzdun/proj-flow/compare/v0.11.1...v0.11.2>`_ (2025-02-19)
8
24
  =====================================================================================
9
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proj-flow
3
- Version: 0.11.2
3
+ Version: 0.12.0
4
4
  Summary: C++ project maintenance, automated
5
5
  Project-URL: Changelog, https://github.com/mzdun/proj-flow/blob/main/CHANGELOG.rst
6
6
  Project-URL: Documentation, https://proj-flow.readthedocs.io/en/latest/
@@ -6,4 +6,4 @@ The **proj_flow** contains only ``__version__`` to be updated, nothing more.
6
6
  This is in an attempt to make this module easy to load initially.
7
7
  """
8
8
 
9
- __version__ = "0.11.2"
9
+ __version__ = "0.12.0"
@@ -56,6 +56,14 @@ class Project:
56
56
  def tag_name(self):
57
57
  return f"v{self.version}"
58
58
 
59
+ @property
60
+ def package_prefix(self):
61
+ return f"{self.archive_name}-"
62
+
63
+ @property
64
+ def package_suffix(self):
65
+ return ""
66
+
59
67
 
60
68
  class ProjectSuite(ABC):
61
69
  @abstractmethod
@@ -97,3 +105,13 @@ class ProjectSuite(ABC):
97
105
 
98
106
 
99
107
  project_suites = registry.Registry[ProjectSuite]("ProjectSuite")
108
+
109
+
110
+ def get_project(rt: env.Runtime):
111
+ def wrap(suite: ProjectSuite):
112
+ return suite.get_project(rt)
113
+
114
+ _, project = project_suites.find(wrap)
115
+ if project is None:
116
+ rt.fatal(f"Cannot get project information from {rt.root}")
117
+ return project
@@ -5,8 +5,10 @@
5
5
  The **proj_flow.base.cmd** defines environment for `proj_flow.api.env.Runtime`.
6
6
  """
7
7
 
8
+ import os
8
9
  import shutil
9
10
  import subprocess
11
+ from contextlib import contextmanager
10
12
  from typing import Optional
11
13
 
12
14
 
@@ -48,3 +50,13 @@ def run(app: str, *args: str, capture_output=False):
48
50
  return subprocess.run(
49
51
  [cmd, *args], shell=False, encoding="UTF-8", capture_output=capture_output
50
52
  )
53
+
54
+
55
+ @contextmanager
56
+ def cd(path: str):
57
+ prev = os.getcwd()
58
+ os.chdir(path)
59
+ try:
60
+ yield
61
+ finally:
62
+ os.chdir(prev)
@@ -7,6 +7,6 @@ The **proj_flow.ext.cplusplus.cmake** provides ``"CMake"``, ``"Build"``,
7
7
  context.
8
8
  """
9
9
 
10
- from . import parser, steps, version
10
+ from . import parser, project, steps
11
11
 
12
- __all__ = ["parser", "steps", "version"]
12
+ __all__ = ["parser", "project", "steps"]
@@ -5,12 +5,8 @@
5
5
  The **proj_flow.ext.cplusplus.cmake.version** provides project suite plugin.
6
6
  """
7
7
 
8
- import os
9
- import re
10
8
  from typing import NamedTuple, Optional
11
9
 
12
- import toml
13
-
14
10
  from proj_flow.api import env, release
15
11
  from proj_flow.ext.cplusplus.cmake.parser import get_project
16
12
 
@@ -15,8 +15,9 @@ import sys
15
15
  import typing
16
16
 
17
17
  from proj_flow import log
18
- from proj_flow.api import arg, env
18
+ from proj_flow.api import arg, env, release
19
19
  from proj_flow.base.name_list import name_list
20
+ from proj_flow.ext.github import publishing
20
21
  from proj_flow.flow.configs import Configs
21
22
  from proj_flow.log import commit, hosting, rich_text
22
23
 
@@ -63,7 +64,7 @@ def matrix(
63
64
 
64
65
 
65
66
  @arg.command("github", "release")
66
- def release(
67
+ def release_cmd(
67
68
  rt: env.Runtime,
68
69
  all: typing.Annotated[
69
70
  bool, arg.FlagArgument(help="Take all Conventional Commits.")
@@ -86,9 +87,9 @@ def release(
86
87
  ],
87
88
  ):
88
89
  """
89
- Bumps the project version based on current git logs, creates a "chore"
90
- commit for the change, attaches an annotated tag with the version number
91
- and pushes it all to GitHub.
90
+ Bump the project version based on current git logs, create a "chore"
91
+ commit for the change, attach an annotated tag with the version number
92
+ and push it all to GitHub.
92
93
  """
93
94
 
94
95
  generator = (
@@ -123,3 +124,62 @@ def release(
123
124
  with open(GITHUB_OUTPUT, "a", encoding="UTF-8") as github_output:
124
125
  print(f"tag={next_tag}", file=github_output)
125
126
  print(f"released={json.dumps(released)}", file=github_output)
127
+
128
+
129
+ @arg.command("github", "publish")
130
+ def publish(
131
+ rt: env.Runtime,
132
+ ref: typing.Annotated[
133
+ typing.Optional[str],
134
+ arg.Argument(
135
+ help="Publish this release draft. In case this is called from within "
136
+ "GitHub Actions and your release is named exactly like the tag used to "
137
+ "trigger this flow, you can use ${{github.action_ref}} variable. "
138
+ "Defaults to current tag.",
139
+ meta="release",
140
+ ),
141
+ ],
142
+ upload: typing.Annotated[
143
+ typing.Optional[str],
144
+ arg.Argument(
145
+ help="If present, will upload files from the directory to "
146
+ "the referenced release before publishing.",
147
+ meta="directory",
148
+ ),
149
+ ],
150
+ ):
151
+ """
152
+ Upload package artifacts to a GitHub release and in case the release
153
+ is still in draft, publish it.
154
+ """
155
+
156
+ git = commit.Git(rt)
157
+ gh_links = hosting.github.GitHub.from_repo(git) or commit.NoHosting()
158
+ project = release.get_project(rt)
159
+
160
+ tag_name = ref or project.tag_name
161
+
162
+ release_info = gh_links.locate_release(tag_name)
163
+ if release_info is None and not rt.dry_run:
164
+ rt.fatal(f"No release matches {tag_name}")
165
+
166
+ if upload is not None:
167
+ matcher = publishing.build_regex(project)
168
+ directory, names = publishing.gather_artifacts(upload, matcher)
169
+ if not len(names):
170
+ rt.fatal(f"No artifact matches {matcher.pattern}")
171
+
172
+ publishing.checksums(rt, directory, names, "sha256sum.txt")
173
+
174
+ if release_info is not None:
175
+ gh_links.upload_to_release(release_info, directory, names)
176
+ else:
177
+ rt.message(f"Would upload:", level=env.Msg.STATUS)
178
+ for name in names:
179
+ rt.message(f" * {name}", level=env.Msg.STATUS)
180
+
181
+ if release_info is not None:
182
+ info = gh_links.publish(release_info)
183
+ if info.url:
184
+ msg = "Visit draft at" if info.is_draft else "Visit release at"
185
+ rt.message(msg, info.url, level=env.Msg.ALWAYS)
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.github.publishing** provides utilities for ``github
6
+ publish`` command.
7
+ """
8
+
9
+
10
+ import hashlib
11
+ import io
12
+ import os
13
+ import re
14
+ import typing
15
+ import zipfile
16
+
17
+ from proj_flow.api import env, release
18
+
19
+
20
+ def _safe_regex(value: str) -> str:
21
+ for esc in "\\.+*?()[]":
22
+ value = value.replace(esc, f"\\{esc}")
23
+ return value
24
+
25
+
26
+ def build_regex(project: release.Project):
27
+ regexPre = _safe_regex(project.package_prefix)
28
+ regexPost = _safe_regex(project.package_suffix)
29
+ regex = f"^{regexPre}(.*){regexPost}$"
30
+ return re.compile(regex)
31
+
32
+
33
+ def gather_artifacts(directory: str, matcher: re.Pattern):
34
+ if os.path.isdir(directory):
35
+ for _, dirnames, filenames in os.walk(directory):
36
+ dirnames[:] = []
37
+ names = [name for name in filenames if matcher.match(name)]
38
+ else:
39
+ next_directory = f"{directory}-dir"
40
+ os.makedirs(next_directory, exist_ok=True)
41
+ with zipfile.ZipFile(directory) as zip:
42
+ names = [name for name in zip.namelist() if matcher.match(name)]
43
+ for name in names:
44
+ zip.extract(name, path=next_directory)
45
+ directory = next_directory
46
+
47
+ return directory, names
48
+
49
+
50
+ def _hash(filename: str) -> str:
51
+ sha = hashlib.sha256()
52
+ with open(filename, "rb") as data:
53
+ for block in iter(lambda: data.read(io.DEFAULT_BUFFER_SIZE), b""):
54
+ sha.update(block)
55
+ return sha.hexdigest()
56
+
57
+
58
+ def checksums(rt: env.Runtime, directory: str, names: typing.List[str], outname: str):
59
+ rt.print("sha256sum", "-b")
60
+ if not rt.dry_run:
61
+ with open(os.path.join(directory, outname), "w") as output:
62
+ for name in names:
63
+ digest = _hash(os.path.join(directory, name))
64
+ print(f"{digest} *{name}", file=output)
65
+ names.append(outname)
@@ -15,6 +15,7 @@ from contextlib import contextmanager
15
15
  from typing import Any, Callable, Dict, List, Optional, cast
16
16
 
17
17
  from proj_flow.api import env, step
18
+ from proj_flow.base import cmd
18
19
 
19
20
 
20
21
  @step.register
@@ -172,16 +173,6 @@ _job_listing = [
172
173
  ]
173
174
 
174
175
 
175
- @contextmanager
176
- def _cd(path: str):
177
- prev = os.getcwd()
178
- os.chdir(path)
179
- try:
180
- yield
181
- finally:
182
- os.chdir(prev)
183
-
184
-
185
176
  def _get_venv_path(root: str):
186
177
  bindir = os.path.join(".venv", "bin")
187
178
  scripts = os.path.join(".venv", "Scripts")
@@ -198,7 +189,7 @@ def _get_venv_path(root: str):
198
189
  def _activate_virtual_env(venv, root: str):
199
190
  global PYTHON_EXECUTABLE
200
191
 
201
- with _cd(root):
192
+ with cmd.cd(root):
202
193
  exec_ext = ".exe" if sys.platform == "win32" else ""
203
194
  python_exec = f"python{exec_ext}"
204
195
  bindir = _get_venv_path(root)
@@ -104,7 +104,7 @@ class SignFiles(SignBase):
104
104
  continue
105
105
 
106
106
  full_path = os.path.join(curr_dir, filename)
107
- if tool.is_executable(full_path, as_package=False):
107
+ if tool.is_signable(full_path, as_package=False):
108
108
  result.append(full_path)
109
109
  return result
110
110
 
@@ -134,7 +134,7 @@ class SignMsi(SignBase):
134
134
  dirnames[:] = []
135
135
  for filename in filenames:
136
136
  full_path = os.path.join(curr_dir, filename)
137
- if tool.is_executable(full_path, as_package=False):
137
+ if tool.is_signable(full_path, as_package=True):
138
138
  result.append(full_path)
139
139
 
140
140
  return result
@@ -142,9 +142,8 @@ class SignMsi(SignBase):
142
142
 
143
143
  class SignInit(init.InitStep):
144
144
  def postprocess(self, rt: env.Runtime, context: dict):
145
- if sys.platform == "win32":
146
- with open(".gitignore", "ab") as ignoref:
147
- ignoref.write("\n/signature.key\n".encode("UTF-8"))
145
+ with open(".gitignore", "ab") as ignoref:
146
+ ignoref.write("\n/signature.key\n".encode("UTF-8"))
148
147
 
149
148
 
150
149
  init.register_init_step(SignInit())
@@ -77,7 +77,7 @@ class SigningTool(ABC):
77
77
  def sign(self, config: env.Config, rt: env.Runtime, files: List[str]): ...
78
78
 
79
79
  @abstractmethod
80
- def is_executable(self, filename: str, as_package: bool): ...
80
+ def is_signable(self, filename: str, as_package: bool): ...
81
81
 
82
82
 
83
83
  signing_tool = base.registry.Registry[SigningTool]("SigningTool")
@@ -34,7 +34,9 @@ if sys.platform == "win32":
34
34
  def is_signable(self, filename: str, as_package: bool):
35
35
  if as_package:
36
36
  _, ext = os.path.splitext(filename)
37
- return ext.lower() == ".msi"
37
+ if ext.lower() == ".msi":
38
+ return True
39
+ # suport NSIS by checking if the archive is a PE
38
40
  return _is_pe_exec(filename)
39
41
 
40
42
  Version = Tuple[int, int, int]
@@ -25,13 +25,6 @@ def _package_name(config: env.Config, pkg: str, group: str):
25
25
  return f"{pkg}-{_system}{_version}-{_arch}{debug}{suffix}"
26
26
 
27
27
 
28
- def _get_project(rt: env.Runtime):
29
- def wrap(suite: release.ProjectSuite):
30
- return suite.get_project(rt)
31
-
32
- return wrap
33
-
34
-
35
28
  @step.register
36
29
  class StorePackages(step.Step):
37
30
  """Stores archives and installers build for ``preset`` config value."""
@@ -52,10 +45,7 @@ class StorePackages(step.Step):
52
45
 
53
46
  global _project_pkg
54
47
  if _project_pkg is None:
55
- _, project = release.project_suites.find(_get_project(rt))
56
- if project is None:
57
- rt.fatal(f"Cannot get project information from {rt.root}")
58
- _project_pkg = project.archive_name
48
+ _project_pkg = release.get_project(rt).archive_name
59
49
 
60
50
  main_group = cast(str, rt._cfg.get("package", {}).get("main-group"))
61
51
  if main_group is not None and not rt.dry_run:
@@ -188,7 +188,10 @@ def _sem_ver(tag: str):
188
188
 
189
189
  @dataclass
190
190
  class ReleaseInfo:
191
- draft_url: Optional[str]
191
+ url: Optional[str] = None
192
+ is_draft: Optional[bool] = None
193
+ ref: Optional[str] = None
194
+ tag: Optional[str] = None
192
195
 
193
196
 
194
197
  class Hosting(ABC):
@@ -241,6 +244,32 @@ class Hosting(ABC):
241
244
  """
242
245
  ...
243
246
 
247
+ @abstractmethod
248
+ def locate_release(self, release_name: str) -> Optional[ReleaseInfo]:
249
+ """
250
+ Locate a release by its name.
251
+ """
252
+ ...
253
+
254
+ @abstractmethod
255
+ def upload_to_release(
256
+ self,
257
+ release: ReleaseInfo,
258
+ directory: str,
259
+ names: list[str],
260
+ ):
261
+ """
262
+ Upload package artifacts to the release.
263
+ """
264
+ ...
265
+
266
+ @abstractmethod
267
+ def publish(self, release: ReleaseInfo) -> ReleaseInfo:
268
+ """
269
+ Publish given release, return updated release info.
270
+ """
271
+ ...
272
+
244
273
 
245
274
  class NoHosting(Hosting):
246
275
  """
@@ -266,7 +295,21 @@ class NoHosting(Hosting):
266
295
  def add_release(
267
296
  self, log: ChangeLog, setup: "LogSetup", git: "Git", draft: bool
268
297
  ) -> ReleaseInfo:
269
- return ReleaseInfo(draft_url=None)
298
+ return ReleaseInfo(is_draft=False)
299
+
300
+ def locate_release(self, release_name: str) -> Optional[ReleaseInfo]:
301
+ return None
302
+
303
+ def upload_to_release(
304
+ self,
305
+ release: ReleaseInfo,
306
+ directory: str,
307
+ names: list[str],
308
+ ):
309
+ return None
310
+
311
+ def publish(self, release: ReleaseInfo):
312
+ return ReleaseInfo(is_draft=False)
270
313
 
271
314
 
272
315
  @dataclass
@@ -23,6 +23,21 @@ class _GitHub(NamedTuple):
23
23
  _NO_GITHUB = _GitHub("", "", "")
24
24
 
25
25
 
26
+ class ReleaseInfo(commit.ReleaseInfo):
27
+ id: int
28
+
29
+ def __init__(
30
+ self,
31
+ url: Optional[str],
32
+ is_draft: Optional[bool],
33
+ id: int,
34
+ ref: Optional[str] = None,
35
+ tag: Optional[str] = None,
36
+ ):
37
+ super().__init__(url, is_draft, ref, tag)
38
+ self.id = id
39
+
40
+
26
41
  class GitHub(commit.Hosting):
27
42
  """
28
43
  Generates links to GitHub.
@@ -78,6 +93,7 @@ class GitHub(commit.Hosting):
78
93
  *args: str,
79
94
  method: Optional[str] = None,
80
95
  capture_output: bool = True,
96
+ ro_call: bool = False,
81
97
  **kwargs,
82
98
  ):
83
99
  url = f"{self.root}{res}"
@@ -94,15 +110,18 @@ class GitHub(commit.Hosting):
94
110
  call.extend(["--method", method.upper()])
95
111
 
96
112
  if capture_output:
97
- if self.rt.dry_run:
113
+ if not ro_call and self.rt.dry_run:
98
114
  self.rt.print(*call, url, *args)
99
115
  return None
100
116
  return self.rt.capture(*call, url, *args)
101
117
 
102
- self.rt.print(*call, url, *args)
118
+ return self._run(*call, url, *args)
119
+
120
+ def _run(self, *args: str):
121
+ self.rt.print(*args)
103
122
  if self.rt.dry_run:
104
123
  return None
105
- return cmd.run(*call, url, *args)
124
+ return cmd.run(*args)
106
125
 
107
126
  def json_from(
108
127
  self,
@@ -111,8 +130,9 @@ class GitHub(commit.Hosting):
111
130
  method: Optional[str] = None,
112
131
  server: Optional[str] = None,
113
132
  default: Any = {},
133
+ ro_call: bool = False,
114
134
  ):
115
- proc = self.gh(res, *args, method=method, server=server)
135
+ proc = self.gh(res, *args, method=method, server=server, ro_call=ro_call)
116
136
  if proc is None:
117
137
  return default
118
138
 
@@ -131,6 +151,15 @@ class GitHub(commit.Hosting):
131
151
 
132
152
  return json.loads(proc.stdout)
133
153
 
154
+ def _release_from_json(self, data: dict, draft: bool = False):
155
+ html_url = cast(Optional[str], data.get("html_url"))
156
+ draft = cast(bool, data.get("draft", draft))
157
+ id = cast(int, data.get("id", 0))
158
+ name = cast(str, data.get("name"))
159
+ tag_name = cast(str, data.get("tag_name"))
160
+
161
+ return ReleaseInfo(url=html_url, is_draft=draft, id=id, ref=name, tag=tag_name)
162
+
134
163
  def add_release(
135
164
  self,
136
165
  log: commit.ChangeLog,
@@ -162,9 +191,46 @@ class GitHub(commit.Hosting):
162
191
  flags.append(f"{name}={value}")
163
192
 
164
193
  data = self.json_from("/releases", *flags, method="POST", default={})
165
- html_url = cast(Optional[str], data.get("html_url"))
194
+ return self._release_from_json(data, draft)
166
195
 
167
- return commit.ReleaseInfo(draft_url=html_url)
196
+ def locate_release(self, release_name: str) -> Optional[commit.ReleaseInfo]:
197
+ releases = self.json_from("/releases", default=[], ro_call=True)
198
+ for release in releases:
199
+ if release.get("name") == release_name:
200
+ return self._release_from_json(release)
201
+
202
+ return None
203
+
204
+ def upload_to_release(
205
+ self,
206
+ release: commit.ReleaseInfo,
207
+ directory: str,
208
+ names: list[str],
209
+ ):
210
+ with cmd.cd(directory):
211
+ return self._run(
212
+ "gh",
213
+ "release",
214
+ "upload",
215
+ release.tag or release.ref or "",
216
+ *names,
217
+ "--clobber",
218
+ )
219
+
220
+ def publish(self, release: commit.ReleaseInfo) -> commit.ReleaseInfo:
221
+ if not isinstance(release, ReleaseInfo):
222
+ return commit.ReleaseInfo(is_draft=False)
223
+
224
+ release_id = release.id
225
+ data = self.json_from(
226
+ f"/releases/{release_id}",
227
+ "-f",
228
+ "draft=false",
229
+ "-F",
230
+ "make_latest=legacy",
231
+ method="PATCH",
232
+ )
233
+ return self._release_from_json(data)
168
234
 
169
235
  @staticmethod
170
236
  def from_repo(git: commit.Git, remote: Optional[str] = None):
@@ -130,8 +130,9 @@ def add_release(
130
130
  git.annotated_tag(setup.curr_tag, commit_message)
131
131
 
132
132
  if hosting.is_active:
133
- draft_url = hosting.add_release(changelog, setup, git, draft).draft_url
134
- if draft_url:
135
- rt.message("Visit draft at", draft_url, level=env.Msg.ALWAYS)
133
+ info = hosting.add_release(changelog, setup, git, draft)
134
+ if info.url:
135
+ msg = "Visit draft at" if info.is_draft else "Visit release at"
136
+ rt.message(msg, info.url, level=env.Msg.ALWAYS)
136
137
 
137
138
  return setup.curr_tag
@@ -10,7 +10,7 @@ import subprocess
10
10
  import sys
11
11
  from contextlib import contextmanager
12
12
  from dataclasses import dataclass
13
- from typing import List, Optional, cast
13
+ from typing import Optional, cast
14
14
 
15
15
  CXX_FLOW_VERSION = "{{__flow_version__}}"
16
16
  PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
@@ -52,7 +52,7 @@ PYTHON_EXECUTABLE = sys.executable
52
52
 
53
53
 
54
54
  def python(
55
- *args: List[str],
55
+ *args: str,
56
56
  module: Optional[str] = None,
57
57
  capture_output: bool = True,
58
58
  ) -> subprocess.CompletedProcess:
@@ -67,11 +67,11 @@ def python(
67
67
  )
68
68
 
69
69
 
70
- def pip(*args: List[str], capture_output: bool = False):
70
+ def pip(*args: str, capture_output: bool = False):
71
71
  return python(*args, module="pip", capture_output=capture_output)
72
72
 
73
73
 
74
- def venv(*args: List[str], capture_output: bool = False):
74
+ def venv(*args: str, capture_output: bool = False):
75
75
  return python(*args, module="venv", capture_output=capture_output)
76
76
 
77
77
 
@@ -126,13 +126,18 @@ def activate_virtual_env():
126
126
  venv(".venv")
127
127
  bindir = get_venv_path()
128
128
 
129
- os.environ["PATH"] = (
130
- f"{os.path.abspath(bindir)}{os.pathsep}{os.environ['PATH']}"
131
- )
132
- PYTHON_EXECUTABLE = shutil.which("python") or sys.executable
129
+ if bindir is not None:
130
+ os.environ["PATH"] = (
131
+ f"{os.path.abspath(bindir)}{os.pathsep}{os.environ['PATH']}"
132
+ )
133
+ PYTHON_EXECUTABLE = shutil.which("python") or sys.executable
133
134
 
134
135
 
135
136
  def bootstrap_cxx_flow():
137
+ if proj_flow is None:
138
+ print('Cannot parse version "{CXX_FLOW_VERSION}"', file=sys.stderr)
139
+ return False
140
+
136
141
  if proj_flow.compatible_with(cxx_flow_version()):
137
142
  return True
138
143
 
@@ -159,13 +164,15 @@ def bootstrap_cxx_flow():
159
164
 
160
165
  def main():
161
166
  if not bootstrap_cxx_flow():
162
- print("Cannot find a working copy of proj-flow", file=sys.stderr)
167
+ print("Cannot find a working copy of proj-flow package", file=sys.stderr)
163
168
  return 1
164
169
 
165
170
  with cd(PROJECT_ROOT):
166
- return subprocess.run(
167
- [shutil.which("proj-flow"), *sys.argv[1:]], shell=False
168
- ).returncode
171
+ executable = shutil.which("proj-flow")
172
+ if executable is None:
173
+ print("Cannot find a working copy of proj-flow executable", file=sys.stderr)
174
+ return 1
175
+ return subprocess.run([executable, *sys.argv[1:]]).returncode
169
176
 
170
177
 
171
178
  if __name__ == "__main__":
@@ -37,3 +37,5 @@ build/
37
37
  web.log
38
38
  super-linter.log
39
39
  *.profraw
40
+
41
+ .venv/
@@ -5,7 +5,8 @@ set(CPACK_WIX_UI_DIALOG "${PROJECT_SOURCE_DIR}/data/assets/wix_dialog.bmp")
5
5
  set(CPACK_WIX_ROOT_FEATURE_TITLE "{{PROJECT.NAME}} ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
6
6
  set(CPACK_WIX_PATCH_FILE "${CMAKE_CURRENT_BINARY_DIR}/packages/_CPack_Packages/patches.wix")
7
7
  set(PROJECT_WIX_BIN_DIR "CM_DP_bin")
8
- set(PROJECT_WIX_BIN_DIR_COMP "CM_CP_bin.${CMAKE_PROJECT_NAME}.exe")
8
+ string(REPLACE "-" "_" SAVE_PROJECT_NAME "${CMAKE_PROJECT_NAME}")
9
+ set(PROJECT_WIX_BIN_DIR_COMP "CM_CP_bin.${SAVE_PROJECT_NAME}.exe")
9
10
  set(WIX_PATH_ID "{{NAME_PREFIX}}_PATH")
10
11
  set(CPACK_WIX_VERSION 4)
11
12
 
@@ -23,7 +23,7 @@ jobs:
23
23
 
24
24
  - name: Find builds
25
25
  id: flow
26
- run: python ./.flow/flow.py ci matrix
26
+ run: python ./.flow/flow.py github matrix
27
27
 
28
28
  build:
29
29
  needs: M
@@ -137,8 +137,6 @@ jobs:
137
137
  - name: Install Conan
138
138
  id: conan
139
139
  uses: turtlebrowser/get-conan@main
140
- with:
141
- version: 1.66.0
142
140
 
143
141
  - name: Conan cache
144
142
  uses: actions/cache@v4
@@ -237,5 +235,5 @@ jobs:
237
235
  name: Packages
238
236
  path: ${{github.workspace}}/build/download/packages
239
237
 
240
- - name: Upload to release
241
- run: python ./.flow/flow.py ci upload --ref "${{github.action_ref}}" "${{github.workspace}}/build/download/packages"
238
+ - name: Publish the release
239
+ run: python ./.flow/flow.py github publish --upload "${{github.workspace}}/build/download/packages"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes