tag-publish 1.3.0.dev7__tar.gz → 1.3.0.dev8__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.
@@ -1,3 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: tag-publish
3
+ Version: 1.3.0.dev8
4
+ Summary: Tools used to publish Python packages, Docker images and Helm charts for GitHub tag and branch
5
+ License: FreeBSD
6
+ License-File: LICENSE
7
+ Keywords: ci
8
+ Author: Camptocamp
9
+ Author-email: info@camptocamp.com
10
+ Requires-Python: >=3.10
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Typing :: Typed
23
+ Provides-Extra: docker
24
+ Provides-Extra: helm
25
+ Provides-Extra: new
26
+ Provides-Extra: pypi
27
+ Provides-Extra: validation
28
+ Requires-Dist: PyYAML (>=6,<7)
29
+ Requires-Dist: applications-download (>=1,<2) ; extra == "helm"
30
+ Requires-Dist: debian-inspector (>=31,<32) ; extra == "docker"
31
+ Requires-Dist: githubkit (>=0,<1)
32
+ Requires-Dist: id (>=1,<2) ; extra == "pypi"
33
+ Requires-Dist: jsonschema-validator-new (>=0,<1) ; extra == "validation"
34
+ Requires-Dist: multi-repo-automation (>=1,<2) ; extra == "new"
35
+ Requires-Dist: requests (>=2,<3) ; extra == "pypi"
36
+ Requires-Dist: ruamel-yaml (>=0,<1) ; extra == "new"
37
+ Requires-Dist: security-md (>=1,<2)
38
+ Requires-Dist: twine (>=6,<7) ; extra == "pypi"
39
+ Project-URL: Bug Tracker, https://github.com/camptocamp/tag-publish/issues
40
+ Project-URL: Repository, https://github.com/camptocamp/tag-publish
41
+ Description-Content-Type: text/markdown
42
+
1
43
  # Tag Publish
2
44
 
3
45
  ## Publishing
@@ -23,11 +65,12 @@ permissions:
23
65
  contents: write
24
66
  ```
25
67
 
26
- Install the package in the worklow:
68
+ Install the package in the worklow, with only the extras you need (see
69
+ [Extras](#extras) below), to reduce the attack surface during the publishing:
27
70
 
28
71
  ```yaml
29
72
  - name: Install tag-publish
30
- run: pip install tag-publish
73
+ run: pip install 'tag-publish[docker,helm]'
31
74
  ```
32
75
 
33
76
  Do the publishing:
@@ -39,6 +82,29 @@ Do the publishing:
39
82
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
83
  ```
41
84
 
85
+ ## Extras
86
+
87
+ To reduce the attack surface, the dependencies are split into optional extras, one
88
+ per kind of publishing. Only install the extras you actually use: a plain
89
+ `pip install tag-publish` installs the core only (`PyYAML`, `security-md`,
90
+ `githubkit`) and will fail with an explicit message if you try a publishing whose
91
+ extra is missing.
92
+
93
+ | Extra | Kind of publishing | Dependencies |
94
+ | ------------ | --------------------------------- | --------------------------------------------------- |
95
+ | `pypi` | Python packages to PyPI | `requests`, `twine`, `id` (OIDC trusted publishing) |
96
+ | `docker` | Docker images (dpkg audit) | `debian-inspector` |
97
+ | `helm` | Helm charts (GitHub/OCI) | `applications-download` (chart-releaser, cosign) |
98
+ | `new` | `tag-publish-new` command | `multi-repo-automation`, `ruamel-yaml` |
99
+ | `validation` | `.github/publish.yaml` validation | `jsonschema-validator-new` |
100
+
101
+ Notes:
102
+
103
+ - The Node (npm) publishing needs no extra, it only uses the `npm` CLI.
104
+ - The `validation` extra is optional: without it the schema validation of
105
+ `.github/publish.yaml` is skipped with a warning.
106
+ - Install several extras at once, e.g. `pip install 'tag-publish[pypi,docker,helm]'`.
107
+
42
108
  ## New version
43
109
 
44
110
  To create a new version you should create a Git tag with the version number.
@@ -355,3 +421,4 @@ Install the pre-commit hooks:
355
421
  pip install pre-commit
356
422
  pre-commit install --allow-missing-config
357
423
  ```
424
+
@@ -1,39 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: tag-publish
3
- Version: 1.3.0.dev7
4
- Summary: Tools used to publish Python packages, Docker images and Helm charts for GitHub tag and branch
5
- License: FreeBSD
6
- License-File: LICENSE
7
- Keywords: ci
8
- Author: Camptocamp
9
- Author-email: info@camptocamp.com
10
- Requires-Python: >=3.10
11
- Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Environment :: Console
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Typing :: Typed
23
- Requires-Dist: PyYAML (>=6,<7)
24
- Requires-Dist: applications-download (>=1,<2)
25
- Requires-Dist: debian-inspector (>=31,<32)
26
- Requires-Dist: githubkit (>=0,<1)
27
- Requires-Dist: id (>=1,<2)
28
- Requires-Dist: jsonschema-validator-new (>=0,<1)
29
- Requires-Dist: multi-repo-automation (>=1,<2)
30
- Requires-Dist: requests (>=2,<3)
31
- Requires-Dist: security-md (>=1,<2)
32
- Requires-Dist: twine (>=6,<7)
33
- Project-URL: Bug Tracker, https://github.com/camptocamp/tag-publish/issues
34
- Project-URL: Repository, https://github.com/camptocamp/tag-publish
35
- Description-Content-Type: text/markdown
36
-
37
1
  # Tag Publish
38
2
 
39
3
  ## Publishing
@@ -59,11 +23,12 @@ permissions:
59
23
  contents: write
60
24
  ```
61
25
 
62
- Install the package in the worklow:
26
+ Install the package in the worklow, with only the extras you need (see
27
+ [Extras](#extras) below), to reduce the attack surface during the publishing:
63
28
 
64
29
  ```yaml
65
30
  - name: Install tag-publish
66
- run: pip install tag-publish
31
+ run: pip install 'tag-publish[docker,helm]'
67
32
  ```
68
33
 
69
34
  Do the publishing:
@@ -75,6 +40,29 @@ Do the publishing:
75
40
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76
41
  ```
77
42
 
43
+ ## Extras
44
+
45
+ To reduce the attack surface, the dependencies are split into optional extras, one
46
+ per kind of publishing. Only install the extras you actually use: a plain
47
+ `pip install tag-publish` installs the core only (`PyYAML`, `security-md`,
48
+ `githubkit`) and will fail with an explicit message if you try a publishing whose
49
+ extra is missing.
50
+
51
+ | Extra | Kind of publishing | Dependencies |
52
+ | ------------ | --------------------------------- | --------------------------------------------------- |
53
+ | `pypi` | Python packages to PyPI | `requests`, `twine`, `id` (OIDC trusted publishing) |
54
+ | `docker` | Docker images (dpkg audit) | `debian-inspector` |
55
+ | `helm` | Helm charts (GitHub/OCI) | `applications-download` (chart-releaser, cosign) |
56
+ | `new` | `tag-publish-new` command | `multi-repo-automation`, `ruamel-yaml` |
57
+ | `validation` | `.github/publish.yaml` validation | `jsonschema-validator-new` |
58
+
59
+ Notes:
60
+
61
+ - The Node (npm) publishing needs no extra, it only uses the `npm` CLI.
62
+ - The `validation` extra is optional: without it the schema validation of
63
+ `.github/publish.yaml` is skipped with a warning.
64
+ - Install several extras at once, e.g. `pip install 'tag-publish[pypi,docker,helm]'`.
65
+
78
66
  ## New version
79
67
 
80
68
  To create a new version you should create a Git tag with the version number.
@@ -391,4 +379,3 @@ Install the pre-commit hooks:
391
379
  pip install pre-commit
392
380
  pre-commit install --allow-missing-config
393
381
  ```
394
-
@@ -21,23 +21,33 @@ exclude = ["tag_publish/node_modules/**/test"]
21
21
 
22
22
  [tool.poetry.dependencies]
23
23
  python = ">=3.10,<4.0"
24
- requests = "2.33.0"
25
- twine = "6.2.0"
26
24
  PyYAML = "6.0.3"
27
- id = "1.5.0"
28
25
  security-md = "1.0.0"
29
- applications-download = "1.7.0"
30
- jsonschema-validator-new = "0.3.2"
31
- debian-inspector = "31.1.1"
32
- multi-repo-automation = "1.7.0"
33
26
  githubkit = "0.14.3"
27
+ requests = { version = "2.33.0", optional = true }
28
+ twine = { version = "6.2.0", optional = true }
29
+ id = { version = "1.5.0", optional = true }
30
+ applications-download = { version = "1.7.0", optional = true }
31
+ jsonschema-validator-new = { version = "0.3.2", optional = true }
32
+ debian-inspector = { version = "31.1.1", optional = true }
33
+ multi-repo-automation = { version = "1.7.0", optional = true }
34
+ ruamel-yaml = { version = "0.19.1", optional = true }
35
+
36
+ [tool.poetry.extras]
37
+ pypi = ["requests", "twine", "id"]
38
+ docker = ["debian-inspector"]
39
+ helm = ["applications-download"]
40
+ new = ["multi-repo-automation", "ruamel-yaml"]
41
+ validation = ["jsonschema-validator-new"]
34
42
 
35
43
  [tool.poetry.group.dev.dependencies]
36
44
  prospector = { version = "1.19.1", extras = ["with_bandit", "with_mypy", "with_pyroma", "with_ruff"] }
37
45
  prospector-profile-duplicated = "1.12.0"
38
46
  prospector-profile-utils = "1.27.0"
39
47
  types-requests = "2.33.0.20260712"
40
- types-pyyaml = "6.0.12.20260518"
48
+ types-pyyaml = "6.0.12.20260724"
49
+ ruff = "0.15.21"
50
+ pylint = "4.0.7"
41
51
 
42
52
  [tool.poetry-dynamic-versioning]
43
53
  enable = false
@@ -86,8 +96,15 @@ include = [
86
96
  "tag_publish/*.json",
87
97
  ]
88
98
  requires-python = ">=3.10"
89
- dependencies = ["requests<3,>=2", "twine<7,>=6", "PyYAML<7,>=6", "id<2,>=1", "security-md<2,>=1", "applications-download<2,>=1", "jsonschema-validator-new<1,>=0", "debian-inspector<32,>=31", "multi-repo-automation<2,>=1", "githubkit<1,>=0"]
90
- version = "1.3.0.dev7"
99
+ dependencies = ["PyYAML<7,>=6", "security-md<2,>=1", "githubkit<1,>=0"]
100
+ version = "1.3.0.dev8"
101
+
102
+ [project.optional-dependencies]
103
+ pypi = ["requests<3,>=2", "twine<7,>=6", "id<2,>=1"]
104
+ docker = ["debian-inspector<32,>=31"]
105
+ helm = ["applications-download<2,>=1"]
106
+ new = ["multi-repo-automation<2,>=1", "ruamel-yaml<1,>=0"]
107
+ validation = ["jsonschema-validator-new<1,>=0"]
91
108
 
92
109
  [project.urls]
93
110
  repository = "https://github.com/camptocamp/tag-publish"
@@ -99,7 +116,7 @@ tag-publish-new = "tag_publish.new:main"
99
116
 
100
117
  [build-system]
101
118
  requires = [
102
- "poetry-core>=1.0.0",
103
- "poetry-dynamic-versioning[plugin]>=0.19.0",
119
+ "poetry-core==2.4.0",
120
+ "poetry-dynamic-versioning[plugin]==1.10.0",
104
121
  ]
105
122
  build-backend = "poetry.core.masonry.api"
@@ -1,20 +1,19 @@
1
1
  """Tag Publish main module."""
2
2
 
3
3
  import base64
4
+ import importlib.util
4
5
  import json
5
6
  import os
6
7
  import pkgutil
7
8
  import re
8
9
  import subprocess # nosec
10
+ import sys
9
11
  from pathlib import Path
10
12
  from re import Match, Pattern
11
13
  from typing import Any, TypedDict, cast, overload
12
14
 
13
- import applications_download
14
15
  import githubkit
15
16
  import githubkit.versions.latest.models
16
- import jsonschema_validator
17
- import ruamel.yaml
18
17
  import security_md
19
18
  import yaml
20
19
 
@@ -32,6 +31,34 @@ VersionTransform = TypedDict(
32
31
  )
33
32
 
34
33
 
34
+ def require_extra(extra: str, *module_names: str) -> None:
35
+ """
36
+ Check that the modules provided by an optional extra are installed.
37
+
38
+ Exit with an installation hint if any module is missing.
39
+
40
+ Arguments:
41
+ extra: The name of the extra that provides the modules
42
+ module_names: The importable module names to check
43
+
44
+ """
45
+
46
+ def is_missing(module_name: str) -> bool:
47
+ try:
48
+ return importlib.util.find_spec(module_name) is None
49
+ except (ModuleNotFoundError, ValueError):
50
+ # Raised when a parent package of a dotted module name is not installed.
51
+ return True
52
+
53
+ missing = [module_name for module_name in module_names if is_missing(module_name)]
54
+ if missing:
55
+ print(
56
+ f"::error::Missing optional dependencies: {', '.join(missing)}, "
57
+ f"install tag-publish with: pip install tag-publish[{extra}]",
58
+ )
59
+ sys.exit(1)
60
+
61
+
35
62
  class GH:
36
63
  """GitHub helper class."""
37
64
 
@@ -112,13 +139,22 @@ def get_config() -> tag_publish.configuration.Configuration:
112
139
  """Get the configuration, with project and auto detections."""
113
140
  config: tag_publish.configuration.Configuration = {}
114
141
  if Path(".github/publish.yaml").exists():
115
- schema_data = pkgutil.get_data("tag_publish", "schema.json")
116
- assert schema_data is not None
117
- schema = json.loads(schema_data)
118
-
119
142
  with Path(".github/publish.yaml").open(encoding="utf-8") as open_file:
120
- yaml_ = ruamel.yaml.YAML()
121
- config = yaml_.load(open_file)
143
+ config = yaml.safe_load(open_file)
144
+
145
+ # The schema validation is optional, provided by the `validation` extra.
146
+ if importlib.util.find_spec("jsonschema_validator") is None:
147
+ print(
148
+ "::warning::The 'validation' extra is not installed, "
149
+ "the '.github/publish.yaml' schema validation is skipped",
150
+ )
151
+ else:
152
+ # Imported here because it is an optional dependency provided by the `validation` extra.
153
+ import jsonschema_validator # noqa: PLC0415
154
+
155
+ schema_data = pkgutil.get_data("tag_publish", "schema.json")
156
+ assert schema_data is not None
157
+ schema = json.loads(schema_data)
122
158
  jsonschema_validator.validate(".github/publish.yaml", cast("dict[str, Any]", config), schema)
123
159
 
124
160
  return config
@@ -209,6 +245,10 @@ def download_application(application_name: str, binary_filename: Path | None = N
209
245
  binary_full_filename = Path.home() / ".local" / "bin" / binary_filename if binary_filename else None
210
246
 
211
247
  if not binary_full_filename.exists() if binary_full_filename else True:
248
+ require_extra("helm", "applications_download")
249
+ # Imported here because it is an optional dependency provided by the `helm` extra.
250
+ import applications_download # noqa: PLC0415
251
+
212
252
  applications = applications_download.Applications()
213
253
  applications.install("helm/chart-releaser")
214
254
 
@@ -17,8 +17,6 @@ import yaml
17
17
 
18
18
  import tag_publish
19
19
  import tag_publish.configuration
20
- import tag_publish.lib.docker
21
- import tag_publish.lib.oidc
22
20
  import tag_publish.publish
23
21
 
24
22
 
@@ -195,8 +193,12 @@ def _handle_pypi_publish(
195
193
  success = True
196
194
  pypi_config = config.get("pypi", {})
197
195
  if pypi_config:
196
+ tag_publish.require_extra("pypi", "id", "requests", "twine")
197
+ # Imported here because it depends on optional dependencies provided by the `pypi` extra.
198
+ from tag_publish.lib import oidc # noqa: PLC0415
199
+
198
200
  if "packages" in pypi_config:
199
- tag_publish.lib.oidc.pypi_login()
201
+ oidc.pypi_login()
200
202
 
201
203
  for package in pypi_config.get("packages", []):
202
204
  if package.get("group", tag_publish.configuration.PIP_PACKAGE_GROUP_DEFAULT) == group:
@@ -289,6 +291,11 @@ def _handle_docker_publish(
289
291
  success = True
290
292
  docker_config = config.get("docker", {})
291
293
  if docker_config:
294
+ # Fail fast, before any push, if the dpkg audit dependencies are missing.
295
+ # Not required in dry-run because the dpkg audit is skipped (it exits before).
296
+ if not dry_run:
297
+ tag_publish.require_extra("docker", "debian_inspector")
298
+
292
299
  sys.stdout.flush()
293
300
  sys.stderr.flush()
294
301
  if docker_config.get("github_oidc_login", tag_publish.configuration.DOCKER_AUTO_LOGIN_DEFAULT):
@@ -376,11 +383,14 @@ def _handle_docker_publish(
376
383
  if dry_run:
377
384
  sys.exit(0)
378
385
 
386
+ # Imported here because it depends on an optional dependency provided by the `docker` extra.
387
+ from tag_publish.lib import docker # noqa: PLC0415
388
+
379
389
  dpkg_versions_path = Path(".github/dpkg-versions.yaml")
380
- versions_config, dpkg_config_found = tag_publish.lib.docker.get_versions_config()
390
+ versions_config, dpkg_config_found = docker.get_versions_config()
381
391
  dpkg_success = True
382
392
  for image in images_src:
383
- dpkg_success &= tag_publish.lib.docker.check_versions(versions_config.get(image, {}), image)
393
+ dpkg_success &= docker.check_versions(versions_config.get(image, {}), image)
384
394
 
385
395
  if not dpkg_success:
386
396
  current_versions_in_images: dict[str, dict[str, str]] = {}
@@ -390,7 +400,7 @@ def _handle_docker_publish(
390
400
  for image in images_src:
391
401
  if image in current_versions_in_images:
392
402
  current_versions_in_images[image] = dict(current_versions_in_images[image])
393
- _, versions_image = tag_publish.lib.docker.get_dpkg_packages_versions(image)
403
+ _, versions_image = docker.get_dpkg_packages_versions(image)
394
404
  for dpkg_package, package_version in versions_image.items():
395
405
  if dpkg_package not in current_versions_in_images.get(image, {}):
396
406
  current_versions_in_images.setdefault(image, {})[dpkg_package] = str(package_version)
@@ -6,10 +6,10 @@ import re
6
6
  import subprocess # nosec
7
7
  from pathlib import Path
8
8
 
9
- import multi_repo_automation as mra
10
- import ruamel.yaml
11
9
  import security_md
12
10
 
11
+ import tag_publish
12
+
13
13
 
14
14
  def _get_default_branch() -> str:
15
15
  """Get the default branch name."""
@@ -23,6 +23,13 @@ def _get_default_branch() -> str:
23
23
 
24
24
  def main() -> None:
25
25
  """Create a new version with its stabilization branch."""
26
+ tag_publish.require_extra("new", "multi_repo_automation", "ruamel.yaml")
27
+ # Imported here because they are optional dependencies provided by the `new` extra.
28
+ import multi_repo_automation as mra # noqa: PLC0415
29
+ import ruamel.yaml # noqa: PLC0415
30
+ import ruamel.yaml.comments # noqa: PLC0415
31
+ import ruamel.yaml.error # noqa: PLC0415
32
+
26
33
  args_parser = argparse.ArgumentParser(
27
34
  description="Create a new version with its stabilization branch",
28
35
  usage="""
@@ -8,8 +8,8 @@ import subprocess # nosec
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
- import ruamel
12
11
  import tomllib
12
+ import yaml
13
13
 
14
14
  import tag_publish
15
15
  import tag_publish.configuration
@@ -292,13 +292,25 @@ def helm(
292
292
 
293
293
  chart_name = ""
294
294
  try:
295
- yaml_ = ruamel.yaml.YAML()
296
- with (Path(folder) / "Chart.yaml").open(encoding="utf-8") as open_file:
297
- chart = yaml_.load(open_file)
298
- chart["version"] = version
295
+ chart_path = Path(folder) / "Chart.yaml"
296
+ with chart_path.open(encoding="utf-8") as open_file:
297
+ chart_content = open_file.read()
298
+ chart = yaml.load(chart_content, Loader=yaml.SafeLoader)
299
299
  chart_name = chart.get("name", "")
300
- with (Path(folder) / "Chart.yaml").open("w", encoding="utf-8") as open_file:
301
- yaml_.dump(chart, open_file)
300
+ # The version is updated with a targeted replacement to preserve the file formatting and comments
301
+ new_chart_content, number_of_substitutions = re.subn(
302
+ r"^version:.*$",
303
+ f"version: {version}",
304
+ chart_content,
305
+ count=1,
306
+ flags=re.MULTILINE,
307
+ )
308
+ if number_of_substitutions != 1:
309
+ print("::endgroup::")
310
+ print(f"::error::Unable to find the 'version' key in '{chart_path}'")
311
+ return False
312
+ with chart_path.open("w", encoding="utf-8") as open_file:
313
+ open_file.write(new_chart_content)
302
314
  for index, dependency in enumerate(chart.get("dependencies", [])):
303
315
  if dependency["repository"].startswith("https://"):
304
316
  subprocess.run(["helm", "repo", "add", str(index), dependency["repository"]], check=True)
@@ -319,6 +331,7 @@ def helm(
319
331
  f"--git-repo={repo}",
320
332
  f"--commit={commit_sha}",
321
333
  "--release-name-template={{ .Version }}",
334
+ "--skip-existing",
322
335
  f"--token={token}",
323
336
  ],
324
337
  check=True,
@@ -363,10 +376,15 @@ def helm(
363
376
  )
364
377
 
365
378
  if oci_config.get("sign", tag_publish.configuration.HELM_OCI_SIGN_DEFAULT):
366
- print(f"Signing chart with cosign at oci://{oci_repository}:{version}")
379
+ print(f"Signing chart with cosign at oci://{oci_repository}/{chart_name}:{version}")
367
380
  tag_publish.download_application("sigstore/cosign")
368
381
  subprocess.run(
369
- ["cosign", "sign", f"oci://{oci_repository}:{version}", "--yes"],
382
+ ["cosign", "login", registry, f"--username={owner}", "--password-stdin"],
383
+ input=f"{token}\n".encode(),
384
+ check=True,
385
+ )
386
+ subprocess.run(
387
+ ["cosign", "sign", f"{oci_repository}/{chart_name}:{version}", "--yes"],
370
388
  check=True,
371
389
  )
372
390
 
@@ -1,3 +1,3 @@
1
1
  # https://docs.renovatebot.com/modules/datasource/#github-releases-datasource
2
2
  helm/chart-releaser: v1.8.1 # github-releases
3
- sigstore/cosign: v3.1.1 # github-releases
3
+ sigstore/cosign: v3.1.2 # github-releases