semantic-tag-increment 0.1.1__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 (48) hide show
  1. semantic_tag_increment-0.1.1/.editorconfig +23 -0
  2. semantic_tag_increment-0.1.1/.github/actionlint.yaml +11 -0
  3. semantic_tag_increment-0.1.1/.github/dependabot.yml +20 -0
  4. semantic_tag_increment-0.1.1/.github/workflows/autolabeler.yaml +59 -0
  5. semantic_tag_increment-0.1.1/.github/workflows/build-test-release.yaml +568 -0
  6. semantic_tag_increment-0.1.1/.github/workflows/build-test.yaml +433 -0
  7. semantic_tag_increment-0.1.1/.github/workflows/codeql.yml +35 -0
  8. semantic_tag_increment-0.1.1/.github/workflows/release-drafter.yaml +35 -0
  9. semantic_tag_increment-0.1.1/.github/workflows/semantic-pull-request.yaml +55 -0
  10. semantic_tag_increment-0.1.1/.github/workflows/testing.yaml +574 -0
  11. semantic_tag_increment-0.1.1/.github/workflows/verify-gha-versions.yaml +29 -0
  12. semantic_tag_increment-0.1.1/.gitignore +62 -0
  13. semantic_tag_increment-0.1.1/.gitlint +37 -0
  14. semantic_tag_increment-0.1.1/.pre-commit-config.yaml +140 -0
  15. semantic_tag_increment-0.1.1/.readthedocs.yml +32 -0
  16. semantic_tag_increment-0.1.1/.yamllint +13 -0
  17. semantic_tag_increment-0.1.1/LICENSE +1 -0
  18. semantic_tag_increment-0.1.1/LICENSES/Apache-2.0.txt +201 -0
  19. semantic_tag_increment-0.1.1/Makefile +177 -0
  20. semantic_tag_increment-0.1.1/PKG-INFO +533 -0
  21. semantic_tag_increment-0.1.1/README.md +484 -0
  22. semantic_tag_increment-0.1.1/REUSE.toml +13 -0
  23. semantic_tag_increment-0.1.1/action.yaml +161 -0
  24. semantic_tag_increment-0.1.1/pyproject.toml +223 -0
  25. semantic_tag_increment-0.1.1/src/semantic_tag_increment/__init__.py +169 -0
  26. semantic_tag_increment-0.1.1/src/semantic_tag_increment/__main__.py +14 -0
  27. semantic_tag_increment-0.1.1/src/semantic_tag_increment/app_context.py +143 -0
  28. semantic_tag_increment-0.1.1/src/semantic_tag_increment/cli.py +31 -0
  29. semantic_tag_increment-0.1.1/src/semantic_tag_increment/cli_interface.py +434 -0
  30. semantic_tag_increment-0.1.1/src/semantic_tag_increment/config.py +169 -0
  31. semantic_tag_increment-0.1.1/src/semantic_tag_increment/exceptions.py +377 -0
  32. semantic_tag_increment-0.1.1/src/semantic_tag_increment/git_operations.py +226 -0
  33. semantic_tag_increment-0.1.1/src/semantic_tag_increment/github_actions.py +291 -0
  34. semantic_tag_increment-0.1.1/src/semantic_tag_increment/incrementer.py +753 -0
  35. semantic_tag_increment-0.1.1/src/semantic_tag_increment/io_operations.py +73 -0
  36. semantic_tag_increment-0.1.1/src/semantic_tag_increment/logging_config.py +205 -0
  37. semantic_tag_increment-0.1.1/src/semantic_tag_increment/main.py +36 -0
  38. semantic_tag_increment-0.1.1/src/semantic_tag_increment/modes.py +204 -0
  39. semantic_tag_increment-0.1.1/src/semantic_tag_increment/parser.py +334 -0
  40. semantic_tag_increment-0.1.1/tests/scripts/performance_test.py +179 -0
  41. semantic_tag_increment-0.1.1/tests/test_cli.py +604 -0
  42. semantic_tag_increment-0.1.1/tests/test_incrementer.py +595 -0
  43. semantic_tag_increment-0.1.1/tests/test_integration.py +507 -0
  44. semantic_tag_increment-0.1.1/tests/test_io_operations.py +168 -0
  45. semantic_tag_increment-0.1.1/tests/test_logging_config.py +228 -0
  46. semantic_tag_increment-0.1.1/tests/test_parser.py +374 -0
  47. semantic_tag_increment-0.1.1/tests/test_performance_optimizations.py +198 -0
  48. semantic_tag_increment-0.1.1/uv.lock +708 -0
@@ -0,0 +1,23 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+
4
+ root = true
5
+
6
+ [*]
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+ indent_style = space
11
+ indent_size = 4
12
+
13
+ [*.{json,yaml,yml}]
14
+ indent_size = 2
15
+
16
+ [*.markdown]
17
+ max_line_length = 80
18
+
19
+ [*.py]
20
+ max_line_legth = 120
21
+
22
+ [*.sh]
23
+ max_line_length = 80
@@ -0,0 +1,11 @@
1
+ ---
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
4
+
5
+ # Path-specific configurations.
6
+ paths:
7
+ .github/workflows/testing.yaml:
8
+ ignore:
9
+ # Ignore deliberate test failure
10
+ - 'input "invalid_input" is not defined in action .+'
11
+ - 'missing input "input" which is required by action .+'
@@ -0,0 +1,20 @@
1
+ ---
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ version: 2
6
+ updates:
7
+ - package-ecosystem: "github-actions"
8
+ open-pull-requests-limit: 15
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
12
+ commit-message:
13
+ prefix: "Chore"
14
+ - package-ecosystem: "uv"
15
+ open-pull-requests-limit: 15
16
+ directory: "/"
17
+ schedule:
18
+ interval: "weekly"
19
+ commit-message:
20
+ prefix: "Chore"
@@ -0,0 +1,59 @@
1
+ ---
2
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ name: 'Autolabeler'
6
+
7
+ # yamllint disable-line rule:truthy
8
+ on:
9
+ # pull_request is required for autolabeler
10
+ pull_request:
11
+ types:
12
+ - opened
13
+ - synchronize
14
+ - reopened
15
+ # pull_request_target is required for autolabeler on PRs from forks
16
+ pull_request_target:
17
+ types:
18
+ - opened
19
+ - synchronize
20
+ - reopened
21
+
22
+ permissions: {}
23
+
24
+ concurrency:
25
+ # yamllint disable-line rule:line-length
26
+ group: ${{ format('al-{0}-pr-{1}', github.event_name, github.event.pull_request.number) }}
27
+ cancel-in-progress: true
28
+
29
+ jobs:
30
+ autolabel:
31
+ name: 'Autolabel PR'
32
+ # Run on pull_request_target for forks, or pull_request for same-repo PRs
33
+ # This prevents duplicate runs for same-repo PRs
34
+ # yamllint disable rule:line-length
35
+ if: >
36
+ (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork) ||
37
+ (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork)
38
+ # yamllint enable rule:line-length
39
+ # SECURITY: pull_request_target with write permissions is safe here because:
40
+ # 1. This workflow does NOT checkout any code from the PR
41
+ # 2. The workflow code itself runs from the base branch (not the fork)
42
+ # 3. release-drafter only makes GitHub API calls (no code execution)
43
+ # 4. pull_request_target is needed ONLY for autolabeling fork PRs
44
+ permissions:
45
+ # write permission is required for autolabeler
46
+ pull-requests: write
47
+ # read is sufficient; autolabeler does not create releases
48
+ contents: read
49
+ runs-on: 'ubuntu-latest'
50
+ timeout-minutes: 3
51
+ steps:
52
+ # Harden the runner used by this workflow
53
+ # yamllint disable-line rule:line-length
54
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
55
+ with:
56
+ egress-policy: 'audit'
57
+
58
+ # yamllint disable-line rule:line-length
59
+ - uses: release-drafter/release-drafter/autolabeler@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
@@ -0,0 +1,568 @@
1
+ ---
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # SPDX-FileCopyrightText: 2025 The Linux Foundation
4
+
5
+ # Runs on a new pull request, performs build and runs tests
6
+ name: 'Python Build/Test/Release'
7
+
8
+ # yamllint disable-line rule:truthy
9
+ on:
10
+ # Trigger on tag push events
11
+ push:
12
+ tags:
13
+ - '**'
14
+
15
+ permissions: {}
16
+
17
+ concurrency:
18
+ group: ${{ github.workflow }}-${{ github.ref }}
19
+ cancel-in-progress: true
20
+
21
+ jobs:
22
+ repository-metadata:
23
+ name: "Repository Metadata"
24
+ runs-on: ubuntu-latest
25
+ permissions:
26
+ contents: read
27
+ pull-requests: read
28
+ timeout-minutes: 5
29
+ steps:
30
+ # yamllint disable-line rule:line-length
31
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
32
+ with:
33
+ egress-policy: audit
34
+
35
+ # yamllint disable-line rule:line-length
36
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
37
+ with:
38
+ fetch-depth: 0
39
+
40
+ - name: "Gather repository metadata"
41
+ id: repo-metadata
42
+ # yamllint disable-line rule:line-length
43
+ uses: lfreleng-actions/repository-metadata-action@ceabcd987d13d7bfefd2372e01eebb0ddac45956 # v0.2.0
44
+ with:
45
+ github_token: ${{ secrets.GITHUB_TOKEN }}
46
+ github_summary: 'true'
47
+ gerrit_summary: 'false'
48
+ artifact_upload: 'true'
49
+ artifact_formats: 'json'
50
+
51
+ tag-validate:
52
+ name: 'Validate Tag Push'
53
+ runs-on: 'ubuntu-latest'
54
+ permissions:
55
+ contents: write # Needed to draft a release
56
+ timeout-minutes: 5
57
+ outputs:
58
+ tag: "${{ steps.tag-validate.outputs.tag_name }}"
59
+ steps:
60
+ # Harden the runner used by this workflow
61
+ # yamllint disable-line rule:line-length
62
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
63
+ with:
64
+ egress-policy: 'audit'
65
+
66
+ - name: 'Checkout repository'
67
+ # yamllint disable-line rule:line-length
68
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
69
+ with:
70
+ fetch-depth: 0
71
+ fetch-tags: true
72
+
73
+ - name: 'Verify pushed tag'
74
+ id: 'tag-validate'
75
+ # yamllint disable-line rule:line-length
76
+ uses: lfreleng-actions/tag-validate-action@67695fa3d045917ca7ecc0f1d5f0cad03e231104 # v1.0.1
77
+ with:
78
+ token: "${{ secrets.GITHUB_TOKEN }}"
79
+ require_type: 'semver'
80
+ reject_development: 'true'
81
+ require_github: 'true'
82
+ # yamllint disable-line rule:line-length
83
+ require_signed: 'ssh,gpg-unverifiable' # Cannot verify GPG without key
84
+
85
+ - name: 'Ensure draft release exists'
86
+ id: 'ensure-release'
87
+ shell: bash
88
+ env:
89
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
90
+ run: |
91
+ # Ensure draft release exists
92
+ TAG="${{ steps.tag-validate.outputs.tag_name }}"
93
+ # Check if release exists and get its draft status
94
+ if RELEASE_INFO=$(gh release view "$TAG" \
95
+ --json isDraft 2>/dev/null); then
96
+ IS_DRAFT=$(echo "$RELEASE_INFO" | jq -r '.isDraft')
97
+ if [ "$IS_DRAFT" = "true" ]; then
98
+ echo "Draft release for tag $TAG already exists"
99
+ else
100
+ echo "Published release for tag $TAG already exists"
101
+ fi
102
+ else
103
+ echo "Creating draft release for tag $TAG"
104
+ gh release create "$TAG" \
105
+ --draft \
106
+ --title "Release $TAG" \
107
+ --notes "Automated release for $TAG"
108
+ fi
109
+
110
+ python-build:
111
+ name: 'Python Build'
112
+ needs: 'tag-validate'
113
+ runs-on: 'ubuntu-latest'
114
+ outputs:
115
+ matrix_json: "${{ steps.python-build.outputs.matrix_json }}"
116
+ artefact_name: "${{ steps.python-build.outputs.artefact_name }}"
117
+ artefact_path: "${{ steps.python-build.outputs.artefact_path }}"
118
+ permissions:
119
+ contents: read
120
+ id-token: write # Needed for attestations
121
+ attestations: write # Needed for attestations
122
+ timeout-minutes: 12
123
+ env:
124
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
125
+ steps:
126
+ # Harden the runner used by this workflow
127
+ # yamllint disable-line rule:line-length
128
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
129
+ with:
130
+ egress-policy: 'audit'
131
+
132
+ # yamllint disable-line rule:line-length
133
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
134
+
135
+ - name: 'Build Python project'
136
+ id: 'python-build'
137
+ # yamllint disable-line rule:line-length
138
+ uses: lfreleng-actions/python-build-action@38b9a4e7ee34e56d178f5bd248df52e41f7d496e # v1.0.6
139
+ with:
140
+ sigstore_sign: true
141
+ attestations: true
142
+
143
+ python-tests:
144
+ name: 'Python Tests'
145
+ runs-on: 'ubuntu-latest'
146
+ needs: 'python-build'
147
+ # Matrix job
148
+ strategy:
149
+ fail-fast: false
150
+ matrix: "${{ fromJson(needs.python-build.outputs.matrix_json) }}"
151
+ permissions:
152
+ contents: read
153
+ timeout-minutes: 12
154
+ steps:
155
+ # Harden the runner used by this workflow
156
+ # yamllint disable-line rule:line-length
157
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
158
+ with:
159
+ egress-policy: 'audit'
160
+
161
+ # yamllint disable-line rule:line-length
162
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
163
+
164
+ - name: 'Test Python project [PYTEST]'
165
+ # yamllint disable-line rule:line-length
166
+ uses: lfreleng-actions/python-test-action@82aa55cf21a6be32d8d5b365716b2b349e8058bd # v1.0.2
167
+ with:
168
+ python_version: "${{ matrix.python-version }}"
169
+
170
+ python-audit:
171
+ name: 'Python Audit'
172
+ runs-on: 'ubuntu-latest'
173
+ needs: 'python-build'
174
+ # Matrix job
175
+ strategy:
176
+ fail-fast: false
177
+ matrix: "${{ fromJson(needs.python-build.outputs.matrix_json) }}"
178
+ permissions:
179
+ contents: read
180
+ timeout-minutes: 10
181
+ steps:
182
+ # Harden the runner used by this workflow
183
+ # yamllint disable-line rule:line-length
184
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
185
+ with:
186
+ egress-policy: 'audit'
187
+
188
+ # yamllint disable-line rule:line-length
189
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
190
+
191
+ - name: 'Audit Python project'
192
+ # yamllint disable-line rule:line-length
193
+ uses: lfreleng-actions/python-audit-action@1776538e768c52c55c3c96495f6e25f0762ad28f # v0.2.6
194
+ with:
195
+ python_version: "${{ matrix.python-version }}"
196
+ permit_fail: "${{ vars.NO_BLOCK_AUDIT_FAIL == 'true' }}"
197
+
198
+ sbom:
199
+ name: 'Generate SBOM'
200
+ runs-on: ubuntu-latest
201
+ needs: 'python-build'
202
+ timeout-minutes: 10
203
+ permissions:
204
+ contents: read
205
+ steps:
206
+ # Harden the runner used by this workflow
207
+ # yamllint disable-line rule:line-length
208
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
209
+ with:
210
+ egress-policy: 'audit'
211
+
212
+ # yamllint disable-line rule:line-length
213
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
214
+
215
+ - name: "Generate SBOM"
216
+ id: sbom
217
+ # yamllint disable-line rule:line-length
218
+ uses: lfreleng-actions/python-sbom-action@ae4aca2ef28d7da4ec95049cc78be43e632d322a # v0.1.0
219
+ with:
220
+ include_dev: "false"
221
+ sbom_format: "both"
222
+
223
+ - name: "Upload SBOM artifacts"
224
+ # yamllint disable-line rule:line-length
225
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
226
+ with:
227
+ name: sbom-files
228
+ path: |
229
+ sbom-cyclonedx.json
230
+ sbom-cyclonedx.xml
231
+ retention-days: 45
232
+
233
+ - name: "SBOM summary"
234
+ run: |
235
+ # SBOM summary
236
+ {
237
+ echo "## SBOM Summary"
238
+ echo "SBOM count: ${{ steps.sbom.outputs.component_count }}"
239
+ echo "Tool used: ${{ steps.sbom.outputs.dependency_manager }}"
240
+ } >> "$GITHUB_STEP_SUMMARY"
241
+
242
+ grype:
243
+ name: 'Grype Audit SBOM'
244
+ runs-on: ubuntu-latest
245
+ needs: 'sbom'
246
+ timeout-minutes: 10
247
+ permissions:
248
+ contents: read
249
+ steps:
250
+ # Harden the runner used by this workflow
251
+ # yamllint disable-line rule:line-length
252
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
253
+ with:
254
+ egress-policy: 'audit'
255
+
256
+ - name: "Download SBOM artefact"
257
+ # yamllint disable-line rule:line-length
258
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
259
+ with:
260
+ name: sbom-files
261
+
262
+ - name: "Install Grype"
263
+ id: grype
264
+ # yamllint disable-line rule:line-length
265
+ uses: anchore/scan-action/download-grype@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
266
+ with:
267
+ cache-db: "true"
268
+
269
+ # The NO_BLOCK_AUDIT_FAIL repository variable (when set to
270
+ # 'true') disables failure propagation so releases can proceed
271
+ # when blocked by newly discovered CVEs in transitive
272
+ # dependencies. Without the override, this step turns red in
273
+ # the workflow run's Actions/Checks UI and is the sole source
274
+ # of the job's failure annotation; the CVE table is printed
275
+ # directly into the step log by echoing grype-results.txt.
276
+ - name: "Grype audit SBOM"
277
+ id: grype-audit
278
+ env:
279
+ GRYPE_CMD: "${{ steps.grype.outputs.cmd }}"
280
+ NO_BLOCK_AUDIT_FAIL: >-
281
+ ${{ vars.NO_BLOCK_AUDIT_FAIL == 'true' }}
282
+ run: |
283
+ # Run grype once, emitting all three output formats
284
+ # Render the human-readable table in this step's log
285
+ set +e
286
+ "${GRYPE_CMD}" \
287
+ -o "sarif=grype-results.sarif" \
288
+ -o "json=grype-results.json" \
289
+ -o "table=grype-results.txt" \
290
+ --fail-on medium \
291
+ "sbom:sbom-cyclonedx.json"
292
+ grype_exit=$?
293
+ set -e
294
+
295
+ echo "--- Grype scan results ---"
296
+ if [ -f grype-results.txt ]; then
297
+ cat grype-results.txt
298
+ else
299
+ echo "No table output produced"
300
+ fi
301
+
302
+ # grype returns 2 when it finds vulnerabilities at or
303
+ # above the configured --fail-on threshold; any other
304
+ # non-zero exit is treated as a grype failure.
305
+ if [ "${grype_exit}" = "0" ]; then
306
+ echo "No vulnerabilities at or above 'medium'"
307
+ exit 0
308
+ fi
309
+ if [ "${grype_exit}" != "2" ]; then
310
+ echo "::error::Grype exited with code ${grype_exit}"
311
+ exit "${grype_exit}"
312
+ fi
313
+
314
+ if [ "${NO_BLOCK_AUDIT_FAIL}" = "true" ]; then
315
+ echo "::warning::Grype found vulnerabilities at or" \
316
+ "above the 'medium' threshold, but" \
317
+ "NO_BLOCK_AUDIT_FAIL is set so the job will not" \
318
+ "fail."
319
+ exit 0
320
+ fi
321
+ echo "::error::Grype found vulnerabilities at or above" \
322
+ "the 'medium' severity threshold. See the table" \
323
+ "above for the offending packages and CVEs."
324
+ exit 1
325
+
326
+ - name: "Upload Grype scan results"
327
+ # yamllint disable-line rule:line-length
328
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
329
+ if: always()
330
+ with:
331
+ name: grype-scan-results
332
+ path: |
333
+ grype-results.sarif
334
+ grype-results.json
335
+ grype-results.txt
336
+ retention-days: 90
337
+ if-no-files-found: warn
338
+
339
+ - name: "Grype summary"
340
+ if: always()
341
+ run: |
342
+ # Render a Markdown table of Grype matches in the job
343
+ # step summary. The CVE data is sourced from the JSON
344
+ # scan output (grype-results.json) and processed with
345
+ # jq so that multi-word cells cannot accidentally break
346
+ # the Markdown table column layout. Any literal pipe
347
+ # characters present in cell content are escaped.
348
+ {
349
+ echo "## Grype Vulnerability Scan"
350
+ echo ""
351
+ if [ ! -f grype-results.json ]; then
352
+ echo "No scan results available"
353
+ exit 0
354
+ fi
355
+ match_count=$(jq '.matches | length' grype-results.json)
356
+ if [ "${match_count}" = "0" ]; then
357
+ echo "No vulnerabilities found at or above the" \
358
+ "configured severity threshold."
359
+ exit 0
360
+ fi
361
+ echo "Found ${match_count} matching Grype record(s)."
362
+ echo ""
363
+ echo "| Package | Installed | Fixed In | Type |" \
364
+ "Vulnerability | Severity | EPSS | Risk |"
365
+ echo "| --- | --- | --- | --- | --- | --- | --- | --- |"
366
+ jq -r '
367
+ def esc: tostring | gsub("\\|"; "\\|");
368
+ # Round a numeric value to two decimal places
369
+ # using half-up rounding via jq built-in `round`.
370
+ def round2: (. * 100 | round) / 100;
371
+ def epss_pct:
372
+ ( .vulnerability.epss // [] ) as $e
373
+ | if ($e | length) == 0 then "-"
374
+ else ( $e[0].epss // 0 ) as $p
375
+ | if $p == 0 then "0%"
376
+ elif ($p * 100) < 0.01 then "<0.01%"
377
+ else ( ($p * 100) | round2 | tostring
378
+ + "%" ) end
379
+ end;
380
+ def risk_fmt:
381
+ .vulnerability.risk
382
+ | if . == null then "-"
383
+ elif type == "number" then
384
+ (round2 | tostring)
385
+ else (. | tostring) end;
386
+ .matches[] |
387
+ [ (.artifact.name // "-" | esc),
388
+ (.artifact.version // "-" | esc),
389
+ ( ( .vulnerability.fix.versions // [] )
390
+ | if length == 0 then "-"
391
+ else join(", ") end | esc ),
392
+ (.artifact.type // "-" | esc),
393
+ (.vulnerability.id // "-" | esc),
394
+ (.vulnerability.severity // "-" | esc),
395
+ epss_pct,
396
+ risk_fmt
397
+ ] | "| " + join(" | ") + " |"
398
+ ' grype-results.json
399
+ } >> "$GITHUB_STEP_SUMMARY"
400
+
401
+ # NOTE: PyPI (test and production) will reject duplicate uploads for the
402
+ # same package version. This means the publishing steps below are NOT
403
+ # idempotent and will fail on workflow re-runs once a version has been
404
+ # published. This is expected behaviour and by design; it prevents the
405
+ # release workflow as a whole from being re-runnable after success.
406
+ test-pypi:
407
+ name: 'Test PyPI Publishing'
408
+ runs-on: 'ubuntu-latest'
409
+ needs:
410
+ - 'tag-validate'
411
+ - 'python-tests'
412
+ - 'python-audit'
413
+ - 'grype'
414
+ environment:
415
+ name: 'development'
416
+ permissions:
417
+ contents: read
418
+ id-token: write # IMPORTANT: mandatory for trusted publishing
419
+ timeout-minutes: 5
420
+ steps:
421
+ # Harden the runner used by this workflow
422
+ # yamllint disable-line rule:line-length
423
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
424
+ with:
425
+ egress-policy: 'audit'
426
+
427
+ - name: 'Test PyPI publishing'
428
+ # yamllint disable-line rule:line-length
429
+ uses: lfreleng-actions/pypi-publish-action@f07400a2b57119f1ceac420f559803264b491f23 # v0.1.6
430
+ with:
431
+ environment: 'development'
432
+ tag: "${{ needs.tag-validate.outputs.tag }}"
433
+ pypi_credential: "${{ secrets.TEST_PYPI_CREDENTIAL }}"
434
+
435
+ pypi:
436
+ name: 'Release PyPI Package'
437
+ runs-on: 'ubuntu-latest'
438
+ needs:
439
+ - 'tag-validate'
440
+ - 'test-pypi'
441
+ environment:
442
+ name: 'production'
443
+ permissions:
444
+ contents: read
445
+ id-token: write # IMPORTANT: mandatory for trusted publishing
446
+ timeout-minutes: 5
447
+ steps:
448
+ # Harden the runner used by this workflow
449
+ # yamllint disable-line rule:line-length
450
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
451
+ with:
452
+ egress-policy: 'audit'
453
+
454
+ - name: 'PyPI release'
455
+ # yamllint disable-line rule:line-length
456
+ uses: lfreleng-actions/pypi-publish-action@f07400a2b57119f1ceac420f559803264b491f23 # v0.1.6
457
+ with:
458
+ environment: 'production'
459
+ attestations: true
460
+ tag: "${{ needs.tag-validate.outputs.tag }}"
461
+ pypi_credential: "${{ secrets.PYPI_CREDENTIAL }}"
462
+
463
+ # Attach build artefacts prior to release promotion
464
+ # This enables the GitHub immutable releases feature
465
+ #
466
+ # NOTE: This job does NOT need an isDraft guard for full workflow
467
+ # re-runs. If the workflow is re-run after a release has already been
468
+ # promoted (immutable), the earlier PyPI publishing steps will fail
469
+ # first (PyPI rejects duplicate uploads), so this job will never be
470
+ # reached. Individual job re-runs are not supported for this workflow.
471
+ attach-artefacts:
472
+ name: 'Attach Artefacts to Release'
473
+ runs-on: 'ubuntu-latest'
474
+ needs:
475
+ - 'tag-validate'
476
+ - 'python-build'
477
+ - 'pypi'
478
+ # yamllint disable-line rule:line-length
479
+ permissions:
480
+ contents: write # IMPORTANT: needed to edit release, attach artefacts
481
+ timeout-minutes: 5
482
+ env:
483
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
484
+ steps:
485
+ # Harden the runner used by this workflow
486
+ # yamllint disable-line rule:line-length
487
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
488
+ with:
489
+ egress-policy: 'audit'
490
+
491
+ # yamllint disable-line rule:line-length
492
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
493
+
494
+ - name: '⬇ Download build artefacts'
495
+ # yamllint disable-line rule:line-length
496
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
497
+ with:
498
+ name: "${{ needs.python-build.outputs.artefact_name }}"
499
+ path: "${{ needs.python-build.outputs.artefact_path }}"
500
+
501
+ - name: 'Attach build artefacts to release'
502
+ # yamllint disable-line rule:line-length
503
+ uses: lfreleng-actions/release-assets-action@985ecd2ba521dde165514af406dd114d3db5dab6 # v0.1.0
504
+ with:
505
+ asset_paths: '["${{ needs.python-build.outputs.artefact_path }}/**"]'
506
+ release_tag: "${{ needs.tag-validate.outputs.tag }}"
507
+
508
+
509
+ promote-release:
510
+ name: 'Promote Draft Release'
511
+ # yamllint disable-line rule:line-length
512
+ needs:
513
+ - 'tag-validate'
514
+ - 'attach-artefacts'
515
+ runs-on: 'ubuntu-latest'
516
+ permissions:
517
+ contents: write # IMPORTANT: needed for draft release promotion
518
+ timeout-minutes: 2
519
+ outputs:
520
+ # yamllint disable-line rule:line-length
521
+ release_url: "${{ steps.promote-release.outputs.release_url || steps.set-promoted-url.outputs.release_url }}"
522
+ steps:
523
+ # Harden the runner used by this workflow
524
+ # yamllint disable-line rule:line-length
525
+ - uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
526
+ with:
527
+ egress-policy: 'audit'
528
+
529
+ # yamllint disable-line rule:line-length
530
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
531
+
532
+ - name: 'Check if release is already promoted'
533
+ id: 'check-promoted'
534
+ shell: bash
535
+ env:
536
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
537
+ run: |
538
+ TAG="${{ needs.tag-validate.outputs.tag }}"
539
+ if gh release view "$TAG" --json isDraft --jq '.isDraft' \
540
+ 2>/dev/null | grep -q "false"; then
541
+ echo "Release $TAG is already promoted, skipping promotion"
542
+ echo "already_promoted=true" >> "$GITHUB_OUTPUT"
543
+ else
544
+ echo "Release $TAG is draft or doesn't exist, " \
545
+ "proceeding with promotion"
546
+ echo "already_promoted=false" >> "$GITHUB_OUTPUT"
547
+ fi
548
+
549
+ - name: 'Promote draft release'
550
+ id: 'promote-release'
551
+ if: steps.check-promoted.outputs.already_promoted == 'false'
552
+ # yamllint disable-line rule:line-length
553
+ uses: lfreleng-actions/draft-release-promote-action@cd7cf442875ecaea5dbb070d0de94f21ece107c8 # v0.1.3
554
+ with:
555
+ token: "${{ secrets.GITHUB_TOKEN }}"
556
+ tag: "${{ needs.tag-validate.outputs.tag }}"
557
+ latest: true
558
+
559
+ - name: 'Set release URL for already promoted release'
560
+ id: 'set-promoted-url'
561
+ if: steps.check-promoted.outputs.already_promoted == 'true'
562
+ shell: bash
563
+ env:
564
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
565
+ run: |
566
+ TAG="${{ needs.tag-validate.outputs.tag }}"
567
+ RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url')
568
+ echo "release_url=$RELEASE_URL" >> "$GITHUB_OUTPUT"