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.
- semantic_tag_increment-0.1.1/.editorconfig +23 -0
- semantic_tag_increment-0.1.1/.github/actionlint.yaml +11 -0
- semantic_tag_increment-0.1.1/.github/dependabot.yml +20 -0
- semantic_tag_increment-0.1.1/.github/workflows/autolabeler.yaml +59 -0
- semantic_tag_increment-0.1.1/.github/workflows/build-test-release.yaml +568 -0
- semantic_tag_increment-0.1.1/.github/workflows/build-test.yaml +433 -0
- semantic_tag_increment-0.1.1/.github/workflows/codeql.yml +35 -0
- semantic_tag_increment-0.1.1/.github/workflows/release-drafter.yaml +35 -0
- semantic_tag_increment-0.1.1/.github/workflows/semantic-pull-request.yaml +55 -0
- semantic_tag_increment-0.1.1/.github/workflows/testing.yaml +574 -0
- semantic_tag_increment-0.1.1/.github/workflows/verify-gha-versions.yaml +29 -0
- semantic_tag_increment-0.1.1/.gitignore +62 -0
- semantic_tag_increment-0.1.1/.gitlint +37 -0
- semantic_tag_increment-0.1.1/.pre-commit-config.yaml +140 -0
- semantic_tag_increment-0.1.1/.readthedocs.yml +32 -0
- semantic_tag_increment-0.1.1/.yamllint +13 -0
- semantic_tag_increment-0.1.1/LICENSE +1 -0
- semantic_tag_increment-0.1.1/LICENSES/Apache-2.0.txt +201 -0
- semantic_tag_increment-0.1.1/Makefile +177 -0
- semantic_tag_increment-0.1.1/PKG-INFO +533 -0
- semantic_tag_increment-0.1.1/README.md +484 -0
- semantic_tag_increment-0.1.1/REUSE.toml +13 -0
- semantic_tag_increment-0.1.1/action.yaml +161 -0
- semantic_tag_increment-0.1.1/pyproject.toml +223 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/__init__.py +169 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/__main__.py +14 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/app_context.py +143 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/cli.py +31 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/cli_interface.py +434 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/config.py +169 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/exceptions.py +377 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/git_operations.py +226 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/github_actions.py +291 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/incrementer.py +753 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/io_operations.py +73 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/logging_config.py +205 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/main.py +36 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/modes.py +204 -0
- semantic_tag_increment-0.1.1/src/semantic_tag_increment/parser.py +334 -0
- semantic_tag_increment-0.1.1/tests/scripts/performance_test.py +179 -0
- semantic_tag_increment-0.1.1/tests/test_cli.py +604 -0
- semantic_tag_increment-0.1.1/tests/test_incrementer.py +595 -0
- semantic_tag_increment-0.1.1/tests/test_integration.py +507 -0
- semantic_tag_increment-0.1.1/tests/test_io_operations.py +168 -0
- semantic_tag_increment-0.1.1/tests/test_logging_config.py +228 -0
- semantic_tag_increment-0.1.1/tests/test_parser.py +374 -0
- semantic_tag_increment-0.1.1/tests/test_performance_optimizations.py +198 -0
- 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"
|