Skip to content

Commit 72bc3d3

Browse files
authored
ci(release): infer optional workflow tags (#4707)
* ci(release): infer optional workflow tags Signed-off-by: Simon Scatton <sscatton@nvidia.com> * fix(release): use resolved tag in success notifications Signed-off-by: Simon Scatton <sscatton@nvidia.com> * fix(release): pin prerelease notes to resolved candidate Signed-off-by: Simon Scatton <sscatton@nvidia.com> --------- Signed-off-by: Simon Scatton <sscatton@nvidia.com>
1 parent c4688aa commit 72bc3d3

9 files changed

Lines changed: 270 additions & 68 deletions

File tree

‎.agents/skills/release-openshell/SKILL.md‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ prerelease and changes since the previous stable release. Present the candidate
1212
to the maintainer. Once approved, dispatch Release from that branch:
1313

1414
```shell
15-
gh workflow run release.yml --ref main -f tag=vX.Y.Z
15+
gh workflow run release.yml --ref main
1616
```
1717

18-
Use the support branch instead of `main` for a previous minor version. Release
19-
selects the highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a
18+
Without `tag`, Release detects the highest prerelease version reachable from the
19+
selected branch, then selects its highest-numbered passing candidate. It stops
20+
if that version has no qualified candidate or already has a stable tag. Supply
21+
`-f tag=vX.Y.Z` to select a version explicitly or retry a stable release.
22+
23+
Use the support branch instead of `main` for a previous minor version. That
24+
branch must contain the updated release workflows. Release selects the
25+
highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a
2026
successful Pre-release run triggered by that tag or dispatched on the same branch,
2127
and matching passing `release-tag-v1` qualification evidence. Missing or expired evidence blocks
2228
promotion. Inspect the selected candidate in the promotion job summary.
@@ -39,6 +45,31 @@ created using `GITHUB_TOKEN`, dispatch Pre-release explicitly on the intended br
3945
gh workflow run release-prerelease.yml --ref main -f tag=vX.Y.Z-pre.1
4046
```
4147

48+
Once the first tag seeds a series, omit `tag` to create the next candidate at
49+
the selected branch's dispatch commit:
50+
51+
```shell
52+
gh workflow run release-prerelease.yml --ref main
53+
```
54+
55+
This selects the highest release series reachable from that branch and requires
56+
its `-pre.1` seed there. It stops if that version is already stable. New version
57+
series always need an explicit seed. Manual runs create a new candidate even
58+
without new commits; pass an existing tag explicitly to retry that candidate.
59+
Candidate creation is serialized with Release Auto-Tag, which still skips
60+
unchanged commits and inactive series on `main`.
61+
62+
For example, start a support patch series by tagging its intended branch commit
63+
as `v0.4.4-pre.1`. Subsequent candidates and promotion use the support branch:
64+
65+
```shell
66+
gh workflow run release-prerelease.yml --ref release/0.4
67+
gh workflow run release.yml --ref release/0.4
68+
```
69+
70+
Replace `release/0.4` with the actual support branch. Use explicit tag inputs if
71+
its history also contains a higher version series.
72+
4273
Wait for the entire Pre-release run to pass before promoting it. Stable tag pushes do not
4374
start Release. Release Auto-Tag creates and dispatches prereleases on `main`.
4475

‎.agents/skills/watch-github-actions/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ selects the latest passing prerelease for the requested version and branch,
176176
verifies its qualification evidence, creates the stable tag at its exact commit,
177177
then rebuilds and publishes without rerunning qualification. The promotion job
178178
summary and `stable-promotion-*` artifact record the candidate and evidence.
179+
Both workflows accept optional tags. Tagless Pre-release creates the next
180+
candidate in the selected branch's seeded series; inspect Resolve Prerelease Tag
181+
for the actual tag and commit. Tagless Release infers the highest reachable
182+
prerelease version and stops if it is already stable. Explicit tags select an
183+
existing candidate or stable version for retries. Release Auto-Tag and manual
184+
candidate creation share a concurrency group; pending tag-resolution jobs can
185+
be waiting for that lock.
179186
Failed, cancelled, or skipped required suites fail
180187
qualification; stable publication is blocked, while published pre-release
181188
artifacts remain available. The Pre-release failure notification includes the

‎.github/workflows/release-auto-tag.yml‎

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,69 +21,43 @@ jobs:
2121
create-tag:
2222
runs-on: ubuntu-latest
2323
timeout-minutes: 5
24+
concurrency:
25+
group: release-prerelease-tags
26+
cancel-in-progress: false
27+
queue: max
2428
steps:
2529
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2630
with:
2731
ref: main
2832
fetch-depth: 0
33+
persist-credentials: false
34+
35+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
36+
with:
37+
version: "0.10.12"
38+
python-version: "3.13"
39+
enable-cache: false
2940

3041
- name: Determine next pre-release version
3142
id: version
3243
run: |
3344
set -euo pipefail
34-
tags=$(git tag -l)
35-
latest=$(printf '%s\n' "$tags" | \
36-
{ grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[1-9][0-9]*)?$' || true; } | \
37-
sort -V | tail -1)
38-
if [[ ! "$latest" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+)-pre\.([1-9][0-9]*)$ ]]; then
39-
echo "No active pre-release series — skipping tag creation"
40-
echo "skip=true" >> "$GITHUB_OUTPUT"
41-
exit 0
42-
fi
43-
base=${BASH_REMATCH[1]}
44-
number=${BASH_REMATCH[2]}
45-
if ! git show-ref --verify --quiet "refs/tags/${base}-pre.1"; then
46-
echo "No ${base}-pre.1 seed — skipping tag creation"
45+
next=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py --scheduled)
46+
if [[ -z "${next}" ]]; then
4747
echo "skip=true" >> "$GITHUB_OUTPUT"
4848
exit 0
4949
fi
50-
if git show-ref --verify --quiet "refs/tags/${base}"; then
51-
echo "$base is already stable — skipping tag creation"
52-
echo "skip=true" >> "$GITHUB_OUTPUT"
53-
exit 0
54-
fi
55-
if ! git merge-base --is-ancestor "$latest" HEAD; then
56-
echo "::error::Latest pre-release $latest is not an ancestor of main"
57-
exit 1
58-
fi
59-
echo "Latest tag: $latest"
60-
61-
# Skip if no new commits since the latest tag
62-
commit_count=$(git rev-list "${latest}..HEAD" --count)
63-
echo "Commits since $latest: $commit_count"
64-
if [ "$commit_count" -eq 0 ]; then
65-
echo "No new commits since $latest — skipping tag creation"
66-
echo "skip=true" >> "$GITHUB_OUTPUT"
67-
exit 0
68-
fi
69-
70-
next="${base}-pre.$((number + 1))"
71-
72-
if git tag -l "$next" | grep -q .; then
73-
echo "::error::Tag $next already exists"
74-
exit 1
75-
fi
76-
7750
echo "next=$next" >> "$GITHUB_OUTPUT"
7851
echo "Next tag: $next"
7952
80-
- name: Create and push tag
53+
- name: Create tag
8154
if: steps.version.outputs.skip != 'true'
8255
env:
56+
GH_TOKEN: ${{ github.token }}
8357
NEXT_TAG: ${{ steps.version.outputs.next }}
8458
run: |
85-
git tag "$NEXT_TAG"
86-
git push origin "refs/tags/$NEXT_TAG"
59+
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
60+
-f "ref=refs/tags/${NEXT_TAG}" -f "sha=$(git rev-parse HEAD)"
8761
8862
- name: Trigger Pre-release workflow
8963
if: steps.version.outputs.skip != 'true'

‎.github/workflows/release-build.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
required: true
1212
type: string
1313
expected-source-sha:
14-
description: Qualified source commit that the release tag must resolve to
14+
description: Expected source commit that the release tag must resolve to
1515
required: false
1616
default: ""
1717
type: string
@@ -89,7 +89,7 @@ jobs:
8989
run: |
9090
set -euo pipefail
9191
if [[ -n "${EXPECTED_SOURCE_SHA}" && "$(git rev-parse HEAD)" != "${EXPECTED_SOURCE_SHA}" ]]; then
92-
echo "Release tag ${RELEASE_TAG} does not match the qualified commit ${EXPECTED_SOURCE_SHA}." >&2
92+
echo "Release tag ${RELEASE_TAG} does not match the expected commit ${EXPECTED_SOURCE_SHA}." >&2
9393
exit 1
9494
fi
9595
if [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then

‎.github/workflows/release-prerelease.yml‎

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,107 @@ on:
1010
workflow_dispatch:
1111
inputs:
1212
tag:
13-
description: "Pre-release tag to publish and qualify (e.g. v0.0.4-pre.1)"
14-
required: true
13+
description: "Existing prerelease tag (e.g. v0.0.4-pre.1); omit to create the next candidate on this branch"
14+
required: false
1515
type: string
1616

1717
permissions:
1818
contents: read
1919

2020
jobs:
21+
resolve:
22+
name: Resolve Prerelease Tag
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 5
25+
permissions:
26+
contents: write
27+
concurrency:
28+
group: release-prerelease-tags
29+
cancel-in-progress: false
30+
queue: max
31+
outputs:
32+
tag: ${{ steps.tag.outputs.tag }}
33+
source-sha: ${{ steps.tag.outputs.source_sha }}
34+
steps:
35+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36+
with:
37+
fetch-depth: 0
38+
persist-credentials: false
39+
40+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
41+
with:
42+
version: "0.10.12"
43+
python-version: "3.13"
44+
enable-cache: false
45+
46+
- name: Resolve or create candidate tag
47+
id: tag
48+
env:
49+
GH_TOKEN: ${{ github.token }}
50+
RELEASE_TAG: ${{ inputs.tag }}
51+
EVENT_NAME: ${{ github.event_name }}
52+
REF_TYPE: ${{ github.ref_type }}
53+
REF_NAME: ${{ github.ref_name }}
54+
run: |
55+
set -euo pipefail
56+
tag="${RELEASE_TAG}"
57+
if [[ "${EVENT_NAME}" == "push" ]]; then
58+
tag="${REF_NAME}"
59+
fi
60+
if [[ -z "${tag}" ]]; then
61+
test "${REF_TYPE}" = branch
62+
tag=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py)
63+
source_sha=$(git rev-parse HEAD)
64+
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
65+
-f "ref=refs/tags/${tag}" -f "sha=${source_sha}"
66+
else
67+
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then
68+
echo "Unsupported prerelease tag: ${tag}" >&2
69+
exit 1
70+
fi
71+
source_sha=$(git rev-parse "refs/tags/${tag}^{commit}")
72+
fi
73+
{
74+
echo "tag=${tag}"
75+
echo "source_sha=${source_sha}"
76+
} >> "${GITHUB_OUTPUT}"
77+
{
78+
echo "## Prerelease candidate"
79+
echo "- Tag: \`${tag}\`"
80+
echo "- Source commit: \`${source_sha}\`"
81+
} >> "${GITHUB_STEP_SUMMARY}"
82+
2183
build:
2284
name: Build
85+
needs: resolve
2386
permissions:
2487
actions: read
2588
contents: read
2689
packages: write
2790
uses: ./.github/workflows/release-build.yml
2891
with:
29-
release-tag: ${{ inputs.tag || github.ref_name }}
92+
release-tag: ${{ needs.resolve.outputs.tag }}
93+
expected-source-sha: ${{ needs.resolve.outputs.source-sha }}
3094
is-prerelease: true
3195
secrets:
3296
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
3397

3498
release-notes:
3599
name: Preview Release Notes
36-
needs: build
100+
needs: [resolve, build]
37101
permissions:
38102
contents: read
39103
pull-requests: read
40104
uses: ./.github/workflows/release-notes.yml
41105
with:
42-
release-tag: ${{ inputs.tag || github.ref_name }}
106+
release-tag: ${{ needs.resolve.outputs.tag }}
43107
advisory: true
44108
secrets:
45109
LLM_INFERENCE_API_KEY: ${{ secrets.LLM_INFERENCE_API_KEY }}
46110

47111
publish:
48112
name: Publish
49-
needs: build
113+
needs: [resolve, build]
50114
permissions:
51115
actions: read
52116
contents: write
@@ -57,7 +121,7 @@ jobs:
57121
artifact-metadata: write
58122
uses: ./.github/workflows/release-publish.yml
59123
with:
60-
release-tag: ${{ inputs.tag || github.ref_name }}
124+
release-tag: ${{ needs.resolve.outputs.tag }}
61125
source-sha: ${{ needs.build.outputs.source_sha }}
62126
semver: ${{ needs.build.outputs.semver }}
63127
python-version: ${{ needs.build.outputs.python_version }}
@@ -68,7 +132,7 @@ jobs:
68132

69133
qualify:
70134
name: Qualify
71-
needs: build
135+
needs: [resolve, build]
72136
permissions:
73137
actions: read
74138
contents: read
@@ -77,7 +141,7 @@ jobs:
77141
pull-requests: read
78142
uses: ./.github/workflows/release-qualification.yml
79143
with:
80-
release-tag: ${{ inputs.tag || github.ref_name }}
144+
release-tag: ${{ needs.resolve.outputs.tag }}
81145
source-sha: ${{ needs.build.outputs.source_sha }}
82146
is-prerelease: true
83147
secrets:
@@ -86,7 +150,7 @@ jobs:
86150

87151
notify-prerelease-success:
88152
name: Notify Prerelease Success
89-
needs: [build, publish, qualify]
153+
needs: [resolve, build, publish, qualify]
90154
if: success()
91155
runs-on: ubuntu-latest
92156
timeout-minutes: 2
@@ -95,10 +159,10 @@ jobs:
95159
- name: Send Slack notification
96160
continue-on-error: true
97161
env:
98-
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
162+
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
99163
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }}
100164
RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }}
101-
TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, inputs.tag || github.ref_name) }}
165+
TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, needs.resolve.outputs.tag) }}
102166
run: |
103167
set -euo pipefail
104168
if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then
@@ -114,7 +178,7 @@ jobs:
114178
115179
notify-prerelease-failure:
116180
name: Notify Prerelease Failure
117-
needs: [build, publish, qualify]
181+
needs: [resolve, build, publish, qualify]
118182
if: failure()
119183
runs-on: ubuntu-latest
120184
timeout-minutes: 2
@@ -127,7 +191,7 @@ jobs:
127191
- name: Send Slack notification
128192
continue-on-error: true
129193
env:
130-
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
194+
RELEASE_TAG: ${{ needs.resolve.outputs.tag || inputs.tag || github.ref_name }}
131195
PUBLICATION_RESULT: ${{ needs.publish.result }}
132196
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }}
133197
SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }}

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ on:
77
workflow_dispatch:
88
inputs:
99
tag:
10-
description: "Stable version to promote from its latest passing prerelease (e.g. v0.0.4)"
11-
required: true
10+
description: "Stable version (e.g. v0.0.4); omit to detect the highest prerelease version on this branch"
11+
required: false
1212
type: string
1313
notes-only:
1414
description: "Generate a release notes artifact without promoting or publishing"
@@ -87,7 +87,7 @@ jobs:
8787
- name: Retain promotion record
8888
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
8989
with:
90-
name: stable-promotion-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
90+
name: stable-promotion-${{ steps.candidate.outputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
9191
path: promotion.json
9292
retention-days: 90
9393
if-no-files-found: error

0 commit comments

Comments
 (0)