Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions .agents/skills/release-openshell/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ prerelease and changes since the previous stable release. Present the candidate
to the maintainer. Once approved, dispatch Release from that branch:

```shell
gh workflow run release.yml --ref main -f tag=vX.Y.Z
gh workflow run release.yml --ref main
```

Use the support branch instead of `main` for a previous minor version. Release
selects the highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a
Without `tag`, Release detects the highest prerelease version reachable from the
selected branch, then selects its highest-numbered passing candidate. It stops
if that version has no qualified candidate or already has a stable tag. Supply
`-f tag=vX.Y.Z` to select a version explicitly or retry a stable release.

Use the support branch instead of `main` for a previous minor version. That
branch must contain the updated release workflows. Release selects the
highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a
successful Pre-release run triggered by that tag or dispatched on the same branch,
and matching passing `release-tag-v1` qualification evidence. Missing or expired evidence blocks
promotion. Inspect the selected candidate in the promotion job summary.
Expand All @@ -39,6 +45,31 @@ created using `GITHUB_TOKEN`, dispatch Pre-release explicitly on the intended br
gh workflow run release-prerelease.yml --ref main -f tag=vX.Y.Z-pre.1
```

Once the first tag seeds a series, omit `tag` to create the next candidate at
the selected branch's dispatch commit:

```shell
gh workflow run release-prerelease.yml --ref main
```

This selects the highest release series reachable from that branch and requires
its `-pre.1` seed there. It stops if that version is already stable. New version
series always need an explicit seed. Manual runs create a new candidate even
without new commits; pass an existing tag explicitly to retry that candidate.
Candidate creation is serialized with Release Auto-Tag, which still skips
unchanged commits and inactive series on `main`.

For example, start a support patch series by tagging its intended branch commit
as `v0.4.4-pre.1`. Subsequent candidates and promotion use the support branch:

```shell
gh workflow run release-prerelease.yml --ref release/0.4
gh workflow run release.yml --ref release/0.4
```

Replace `release/0.4` with the actual support branch. Use explicit tag inputs if
its history also contains a higher version series.

Wait for the entire Pre-release run to pass before promoting it. Stable tag pushes do not
start Release. Release Auto-Tag creates and dispatches prereleases on `main`.

Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/watch-github-actions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ selects the latest passing prerelease for the requested version and branch,
verifies its qualification evidence, creates the stable tag at its exact commit,
then rebuilds and publishes without rerunning qualification. The promotion job
summary and `stable-promotion-*` artifact record the candidate and evidence.
Both workflows accept optional tags. Tagless Pre-release creates the next
candidate in the selected branch's seeded series; inspect Resolve Prerelease Tag
for the actual tag and commit. Tagless Release infers the highest reachable
prerelease version and stops if it is already stable. Explicit tags select an
existing candidate or stable version for retries. Release Auto-Tag and manual
candidate creation share a concurrency group; pending tag-resolution jobs can
be waiting for that lock.
Failed, cancelled, or skipped required suites fail
qualification; stable publication is blocked, while published pre-release
artifacts remain available. The Pre-release failure notification includes the
Expand Down
60 changes: 17 additions & 43 deletions .github/workflows/release-auto-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,69 +21,43 @@
create-tag:
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: release-prerelease-tags
cancel-in-progress: false
queue: max

Check failure on line 27 in .github/workflows/release-auto-tag.yml

View workflow job for this annotation

GitHub Actions / Actionlint (informational)

unexpected key "queue" for "concurrency" section. expected one of "cancel-in-progress", "group"
Comment thread
SDAChess marked this conversation as resolved.
Dismissed
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
persist-credentials: false

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.10.12"
python-version: "3.13"
enable-cache: false

- name: Determine next pre-release version
id: version
run: |
set -euo pipefail
tags=$(git tag -l)
latest=$(printf '%s\n' "$tags" | \
{ grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[1-9][0-9]*)?$' || true; } | \
sort -V | tail -1)
if [[ ! "$latest" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+)-pre\.([1-9][0-9]*)$ ]]; then
echo "No active pre-release series — skipping tag creation"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
base=${BASH_REMATCH[1]}
number=${BASH_REMATCH[2]}
if ! git show-ref --verify --quiet "refs/tags/${base}-pre.1"; then
echo "No ${base}-pre.1 seed — skipping tag creation"
next=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py --scheduled)
if [[ -z "${next}" ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git show-ref --verify --quiet "refs/tags/${base}"; then
echo "$base is already stable — skipping tag creation"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! git merge-base --is-ancestor "$latest" HEAD; then
echo "::error::Latest pre-release $latest is not an ancestor of main"
exit 1
fi
echo "Latest tag: $latest"

# Skip if no new commits since the latest tag
commit_count=$(git rev-list "${latest}..HEAD" --count)
echo "Commits since $latest: $commit_count"
if [ "$commit_count" -eq 0 ]; then
echo "No new commits since $latest — skipping tag creation"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

next="${base}-pre.$((number + 1))"

if git tag -l "$next" | grep -q .; then
echo "::error::Tag $next already exists"
exit 1
fi

echo "next=$next" >> "$GITHUB_OUTPUT"
echo "Next tag: $next"

- name: Create and push tag
- name: Create tag
if: steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
NEXT_TAG: ${{ steps.version.outputs.next }}
run: |
git tag "$NEXT_TAG"
git push origin "refs/tags/$NEXT_TAG"
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
-f "ref=refs/tags/${NEXT_TAG}" -f "sha=$(git rev-parse HEAD)"

- name: Trigger Pre-release workflow
if: steps.version.outputs.skip != 'true'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
required: true
type: string
expected-source-sha:
description: Qualified source commit that the release tag must resolve to
description: Expected source commit that the release tag must resolve to
required: false
default: ""
type: string
Expand Down Expand Up @@ -89,7 +89,7 @@ jobs:
run: |
set -euo pipefail
if [[ -n "${EXPECTED_SOURCE_SHA}" && "$(git rev-parse HEAD)" != "${EXPECTED_SOURCE_SHA}" ]]; then
echo "Release tag ${RELEASE_TAG} does not match the qualified commit ${EXPECTED_SOURCE_SHA}." >&2
echo "Release tag ${RELEASE_TAG} does not match the expected commit ${EXPECTED_SOURCE_SHA}." >&2
exit 1
fi
if [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then
Expand Down
88 changes: 76 additions & 12 deletions .github/workflows/release-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,87 @@
workflow_dispatch:
inputs:
tag:
description: "Pre-release tag to publish and qualify (e.g. v0.0.4-pre.1)"
required: true
description: "Existing prerelease tag (e.g. v0.0.4-pre.1); omit to create the next candidate on this branch"
required: false
type: string

permissions:
contents: read

jobs:
resolve:
name: Resolve Prerelease Tag
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
concurrency:
group: release-prerelease-tags
cancel-in-progress: false
queue: max

Check failure on line 30 in .github/workflows/release-prerelease.yml

View workflow job for this annotation

GitHub Actions / Actionlint (informational)

unexpected key "queue" for "concurrency" section. expected one of "cancel-in-progress", "group"
Comment thread
SDAChess marked this conversation as resolved.
Dismissed
outputs:
tag: ${{ steps.tag.outputs.tag }}
source-sha: ${{ steps.tag.outputs.source_sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.10.12"
python-version: "3.13"
enable-cache: false

- name: Resolve or create candidate tag
id: tag
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
EVENT_NAME: ${{ github.event_name }}
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
tag="${RELEASE_TAG}"
if [[ "${EVENT_NAME}" == "push" ]]; then
tag="${REF_NAME}"
fi
if [[ -z "${tag}" ]]; then
test "${REF_TYPE}" = branch
tag=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py)
source_sha=$(git rev-parse HEAD)
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
-f "ref=refs/tags/${tag}" -f "sha=${source_sha}"
else
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then
echo "Unsupported prerelease tag: ${tag}" >&2
exit 1
fi
source_sha=$(git rev-parse "refs/tags/${tag}^{commit}")
fi
{
echo "tag=${tag}"
echo "source_sha=${source_sha}"
} >> "${GITHUB_OUTPUT}"
{
echo "## Prerelease candidate"
echo "- Tag: \`${tag}\`"
echo "- Source commit: \`${source_sha}\`"
} >> "${GITHUB_STEP_SUMMARY}"

build:
name: Build
needs: resolve
permissions:
actions: read
contents: read
packages: write
uses: ./.github/workflows/release-build.yml
with:
release-tag: ${{ inputs.tag || github.ref_name }}
release-tag: ${{ needs.resolve.outputs.tag }}
expected-source-sha: ${{ needs.resolve.outputs.source-sha }}
is-prerelease: true
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
Expand All @@ -46,7 +110,7 @@

publish:
name: Publish
needs: build
needs: [resolve, build]
permissions:
actions: read
contents: write
Expand All @@ -56,7 +120,7 @@
artifact-metadata: write
uses: ./.github/workflows/release-publish.yml
with:
release-tag: ${{ inputs.tag || github.ref_name }}
release-tag: ${{ needs.resolve.outputs.tag }}
source-sha: ${{ needs.build.outputs.source_sha }}
semver: ${{ needs.build.outputs.semver }}
python-version: ${{ needs.build.outputs.python_version }}
Expand All @@ -65,7 +129,7 @@

qualify:
name: Qualify
needs: build
needs: [resolve, build]
permissions:
actions: read
contents: read
Expand All @@ -74,7 +138,7 @@
pull-requests: read
uses: ./.github/workflows/release-qualification.yml
with:
release-tag: ${{ inputs.tag || github.ref_name }}
release-tag: ${{ needs.resolve.outputs.tag }}
source-sha: ${{ needs.build.outputs.source_sha }}
is-prerelease: true
secrets:
Expand All @@ -83,7 +147,7 @@

notify-prerelease-success:
name: Notify Prerelease Success
needs: [build, publish, qualify]
needs: [resolve, build, publish, qualify]
if: success()
runs-on: ubuntu-latest
timeout-minutes: 2
Expand All @@ -92,10 +156,10 @@
- name: Send Slack notification
continue-on-error: true
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }}
RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }}
TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, inputs.tag || github.ref_name) }}
TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, needs.resolve.outputs.tag) }}
run: |
set -euo pipefail
if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then
Expand All @@ -111,7 +175,7 @@

notify-prerelease-failure:
name: Notify Prerelease Failure
needs: [build, publish, qualify]
needs: [resolve, build, publish, qualify]
if: failure()
runs-on: ubuntu-latest
timeout-minutes: 2
Expand All @@ -124,7 +188,7 @@
- name: Send Slack notification
continue-on-error: true
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
RELEASE_TAG: ${{ needs.resolve.outputs.tag || inputs.tag || github.ref_name }}
PUBLICATION_RESULT: ${{ needs.publish.result }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }}
SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
workflow_dispatch:
inputs:
tag:
description: "Stable version to promote from its latest passing prerelease (e.g. v0.0.4)"
required: true
description: "Stable version (e.g. v0.0.4); omit to detect the highest prerelease version on this branch"
required: false
type: string
notes-only:
description: "Generate a release notes artifact without promoting or publishing"
Expand All @@ -22,7 +22,7 @@
concurrency:
group: ${{ inputs.notes-only && format('release-notes-preview-{0}', github.ref) || 'release-stable' }}
cancel-in-progress: false
queue: max

Check failure on line 25 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Actionlint (informational)

unexpected key "queue" for "concurrency" section. expected one of "cancel-in-progress", "group"

jobs:
preview-notes:
Expand Down Expand Up @@ -86,7 +86,7 @@
- name: Retain promotion record
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stable-promotion-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
name: stable-promotion-${{ steps.candidate.outputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
path: promotion.json
retention-days: 90
if-no-files-found: error
Expand Down
Loading
Loading