proj-flow 0.11.3__tar.gz → 0.13.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.3 → proj_flow-0.13.0}/CHANGELOG.rst +16 -0
  2. {proj_flow-0.11.3 → proj_flow-0.13.0}/PKG-INFO +1 -1
  3. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/__init__.py +1 -1
  4. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/release.py +18 -0
  5. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/cmd.py +12 -0
  6. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/cmake/__init__.py +2 -2
  7. proj_flow-0.11.3/src/proj_flow/ext/cplusplus/cmake/version.py → proj_flow-0.13.0/src/proj_flow/ext/cplusplus/cmake/project.py +0 -4
  8. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/github/cli.py +66 -6
  9. proj_flow-0.13.0/src/proj_flow/ext/github/publishing.py +65 -0
  10. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/python/rtdocs.py +2 -11
  11. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/store.py +1 -11
  12. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/flow/configs.py +42 -1
  13. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/commit.py +45 -2
  14. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/hosting/github.py +72 -6
  15. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/release.py +4 -3
  16. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.flow/config.yml +0 -8
  17. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.flow/flow.py.mustache +19 -12
  18. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions/.github/workflows/build.yml +3 -5
  19. {proj_flow-0.11.3 → proj_flow-0.13.0}/.flow/config.yaml +0 -0
  20. {proj_flow-0.11.3 → proj_flow-0.13.0}/.gitignore +0 -0
  21. {proj_flow-0.11.3 → proj_flow-0.13.0}/.readthedocs.yaml +0 -0
  22. {proj_flow-0.11.3 → proj_flow-0.13.0}/LICENSE +0 -0
  23. {proj_flow-0.11.3 → proj_flow-0.13.0}/README.md +0 -0
  24. {proj_flow-0.11.3 → proj_flow-0.13.0}/pyproject.toml +0 -0
  25. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/__main__.py +0 -0
  26. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/__init__.py +0 -0
  27. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/arg.py +0 -0
  28. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/completers.py +0 -0
  29. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/ctx.py +0 -0
  30. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/env.py +0 -0
  31. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/init.py +0 -0
  32. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/makefile.py +0 -0
  33. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/api/step.py +0 -0
  34. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/__cmake_version__.py +0 -0
  35. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/__init__.py +0 -0
  36. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/inspect.py +0 -0
  37. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/matrix.py +0 -0
  38. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/name_list.py +0 -0
  39. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/plugins.py +0 -0
  40. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/registry.py +0 -0
  41. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/base/uname.py +0 -0
  42. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/cli/__init__.py +0 -0
  43. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/cli/argument.py +0 -0
  44. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/cli/finder.py +0 -0
  45. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/dependency.py +0 -0
  46. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/__init__.py +0 -0
  47. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/__init__.py +0 -0
  48. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/cmake/parser.py +0 -0
  49. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/cmake/steps.py +0 -0
  50. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/conan/__init__.py +0 -0
  51. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/cplusplus/conan/_conan.py +0 -0
  52. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/github/__init__.py +0 -0
  53. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/github/hosting.py +0 -0
  54. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/github/switches.py +0 -0
  55. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/markdown_changelog.py +0 -0
  56. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/python/__init__.py +0 -0
  57. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/python/steps.py +0 -0
  58. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/python/version.py +0 -0
  59. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/re_structured_changelog.py +0 -0
  60. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/sign/__init__.py +0 -0
  61. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/sign/api.py +0 -0
  62. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/ext/sign/win32.py +0 -0
  63. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/flow/__init__.py +0 -0
  64. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/flow/layer.py +0 -0
  65. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/flow/steps.py +0 -0
  66. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/__init__.py +0 -0
  67. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/error.py +0 -0
  68. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/fmt.py +0 -0
  69. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/format.py +0 -0
  70. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/hosting/__init__.py +0 -0
  71. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/msg.py +0 -0
  72. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/rich_text/__init__.py +0 -0
  73. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/rich_text/api.py +0 -0
  74. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/rich_text/markdown.py +0 -0
  75. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/log/rich_text/re_structured_text.py +0 -0
  76. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/__init__.py +0 -0
  77. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/base.py +0 -0
  78. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/bootstrap.py +0 -0
  79. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/init.py +0 -0
  80. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/list.py +0 -0
  81. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/run.py +0 -0
  82. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/minimal/system.py +0 -0
  83. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/__init__.py +0 -0
  84. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/api.py +0 -0
  85. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/cplusplus/__init__.py +0 -0
  86. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/cplusplus/cmake_context.py +0 -0
  87. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/cplusplus/conan_context.py +0 -0
  88. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/cplusplus/project.py +0 -0
  89. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/data.py +0 -0
  90. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/project/interact.py +0 -0
  91. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.clang-format +0 -0
  92. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.flow/matrix.yml +0 -0
  93. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.flow/official.yml +0 -0
  94. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/.gitignore +0 -0
  95. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/README.md.mustache +0 -0
  96. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/flow +0 -0
  97. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base/flow.cmd +0 -0
  98. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/base.json +0 -0
  99. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/cmake/common.cmake.mustache +0 -0
  100. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/__init__.py.mustache +0 -0
  101. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/icons/__init__.py.mustache +0 -0
  102. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/extensions/wall/icons/magick.py.mustache +0 -0
  103. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/base.cmake.mustache +0 -0
  104. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/config.cmake +0 -0
  105. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/cpack.cmake +0 -0
  106. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix/cpack.cmake.mustache +0 -0
  107. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix/patches.in.wix +0 -0
  108. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/.flow/packages/wix.cmake.mustache +0 -0
  109. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/CMakeGraphVizOptions.cmake +0 -0
  110. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/CMakeLists.txt.mustache +0 -0
  111. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/CMakePresets.json +0 -0
  112. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/assets/appicon.ico +0 -0
  113. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/assets/appicon.png +0 -0
  114. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/assets/wix_banner.bmp +0 -0
  115. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/assets/wix_dialog.bmp +0 -0
  116. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/icons/.gitignore +0 -0
  117. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/icons/appicon-mask.svg +0 -0
  118. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/data/icons/appicon.svg +0 -0
  119. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/src/main.cc.mustache +0 -0
  120. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/src/version.hpp.in.mustache +0 -0
  121. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake/tests/test.cc.mustache +0 -0
  122. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/cmake.json +0 -0
  123. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/conan/.flow/cmake/libcxx_toolchain.cmake +0 -0
  124. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/conan/.flow/cmake/output_dirs_setup.cmake +0 -0
  125. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/conan/conanfile.txt +0 -0
  126. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/conan.json +0 -0
  127. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions/.github/linters/.isort.cfg +0 -0
  128. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions/.github/linters/.mypy.ini +0 -0
  129. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions/.github/workflows/linter.yml +0 -0
  130. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions/CPPLINT.cfg +0 -0
  131. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_actions.json +0 -0
  132. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/bug_report.md.mustache +0 -0
  133. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/feature_request.md.mustache +0 -0
  134. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_social/CODE_OF_CONDUCT.md.mustache +0 -0
  135. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_social/CONTRIBUTING.md +0 -0
  136. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/layers/github_social.json +0 -0
  137. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/licenses/0BSD.mustache +0 -0
  138. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/licenses/MIT.mustache +0 -0
  139. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/licenses/Unlicense.mustache +0 -0
  140. {proj_flow-0.11.3 → proj_flow-0.13.0}/src/proj_flow/template/licenses/WTFPL.mustache +0 -0
  141. {proj_flow-0.11.3 → proj_flow-0.13.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.13.0 <https://github.com/mzdun/proj-flow/compare/v0.12.0...v0.13.0>`_ (2025-02-22)
8
+ =====================================================================================
9
+
10
+ New Features
11
+ ------------
12
+
13
+ - calculate lts.ubuntu from current date (`2bfa12e <https://github.com/mzdun/proj-flow/commit/2bfa12e87cf7b5b163ef88473d2f4779afeed938>`_)
14
+
15
+ `0.12.0 <https://github.com/mzdun/proj-flow/compare/v0.11.3...v0.12.0>`_ (2025-02-22)
16
+ =====================================================================================
17
+
18
+ New Features
19
+ ------------
20
+
21
+ - add ``github publish`` (`2f6ef1e <https://github.com/mzdun/proj-flow/commit/2f6ef1eaf4053f25633bf3f5037991fa4567023b>`_)
22
+
7
23
  `0.11.3 <https://github.com/mzdun/proj-flow/compare/v0.11.2...v0.11.3>`_ (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.3
3
+ Version: 0.13.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.3"
9
+ __version__ = "0.13.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.")
@@ -72,7 +73,7 @@ def release(
72
73
  typing.Optional[str],
73
74
  arg.Argument(
74
75
  help="Ignore the version change from changelog and instead use this value. "
75
- f"Allowed values are: {name_list(FORCED_LEVEL_CHOICES)}",
76
+ f"Allowed values are: {name_list(FORCED_LEVEL_CHOICES)}.",
76
77
  meta="level",
77
78
  choices=FORCED_LEVEL_CHOICES,
78
79
  ),
@@ -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, upload files from the directory to the referenced "
146
+ "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)
@@ -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:
@@ -9,7 +9,9 @@ using ``-D`` switches.
9
9
 
10
10
 
11
11
  import argparse
12
+ import datetime
12
13
  import os
14
+ import sys
13
15
  from typing import Any, Callable, Dict, List
14
16
 
15
17
  from proj_flow.api import env
@@ -90,9 +92,43 @@ def _expand_one(config: dict, github_os: str, os_in_name: str):
90
92
  return config
91
93
 
92
94
 
95
+ __printed_lts_ubuntu_warning = False
96
+
97
+
98
+ def _ubuntu_lts(today=datetime.date.today(), lts_years=5):
99
+ year = today.year
100
+ for y in range(year - lts_years, year + 1):
101
+ if y % 2 != 0:
102
+ continue
103
+ release = datetime.date(y, 4, 1)
104
+ end_of_life = datetime.date(y + lts_years, 1, 31)
105
+ if release > today or end_of_life < today:
106
+ continue
107
+ yield f"ubuntu-{y % 100}.04"
108
+
109
+
110
+ def _lts_list(config: dict, lts_list: Dict[str, List[str]]):
111
+ os = config["os"]
112
+ raw = lts_list.get(os)
113
+ if os == "ubuntu":
114
+ if raw is not None:
115
+ global __printed_lts_ubuntu_warning
116
+ if not __printed_lts_ubuntu_warning:
117
+ __printed_lts_ubuntu_warning = True
118
+ print(
119
+ "\033[1;33m-- lts.ubuntu in config.yaml is deprecated; "
120
+ "please remove it, so it can be calculated base on "
121
+ "current date\033[m",
122
+ file=sys.stderr,
123
+ )
124
+ else:
125
+ raw = list(_ubuntu_lts())
126
+ return raw or []
127
+
128
+
93
129
  def _expand_config(config: dict, spread_lts: bool, lts_list: Dict[str, List[str]]):
94
130
  if spread_lts:
95
- spread = lts_list.get(config["os"], [])
131
+ spread = _lts_list(config, lts_list)
96
132
  if len(spread):
97
133
  return [
98
134
  _expand_one({key: config[key] for key in config}, lts, lts)
@@ -136,6 +172,11 @@ class Configs:
136
172
  # from commands/github
137
173
  spread_lts = hasattr(args, "matrix") and not not args.matrix
138
174
 
175
+ if not spread_lts:
176
+ # allow "run" to see the warning about "lts.ubuntu"
177
+ for config in configs:
178
+ _lts_list(config, rt.lts_list)
179
+
139
180
  turned = matrix.flatten(
140
181
  [
141
182
  _expand_config(config, spread_lts, rt.lts_list)
@@ -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
@@ -18,17 +18,9 @@ compiler:
18
18
  gcc: [ gcc, g++ ]
19
19
  os-default: { ubuntu: gcc, windows: msvc }
20
20
 
21
- lts:
22
- ubuntu:
23
- - ubuntu-20.04
24
- - ubuntu-22.04
25
- - ubuntu-24.04
26
-
27
21
  postproc:
28
22
  exclude:
29
- - { github_os: ubuntu-20.04, sanitizer: true }
30
23
  - { github_os: ubuntu-24.04, sanitizer: true }
31
- - { github_os: ubuntu-20.04, compiler: clang }
32
24
 
33
25
 
34
26
  shortcuts: