From 0fc1a8cc1ed6dd06d549e3316676b3f1466b440d Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 15 Jun 2026 13:41:50 -0700 Subject: [PATCH 1/2] Add release.yml: end-to-end docs release automation Detects new Salt point releases (polling saltstack/salt every 10 min), opens a topic/release/ PR with generated edits, waits for required PR checks, merges, and runs per-repo post-merge actions. --- .github/workflows/release.yml | 161 ++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cadc72a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,161 @@ +name: Release automation + +# End-to-end docs-release pipeline for salt-install-guide. +# Polls saltstack/salt every 10 min. On a new point release (X.Y where Y > 0): +# 1. Open `topic/release/` PR with version-bump edits +# 2. Wait for required PR checks +# 3. Merge +# 4. Push annotated tag (minor-bumped) so build-sphinx-docs.yml can build +# +# `.0` (LTS-bump) releases are skipped — they require manually-curated edits +# to lifecycle dates, downloads, etc. +# +# Caveat: PRs created with the default GITHUB_TOKEN do not trigger workflows +# that run on `pull_request`. If you want PR-level CI to run, replace +# GH_TOKEN with a PAT or GitHub App token stored as a secret. + +on: + schedule: + - cron: '*/10 * * * *' + workflow_dispatch: + inputs: + salt_version: + description: 'Override salt version (e.g. 3008.1). Blank = auto-detect.' + required: false + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: release-automation + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Detect target salt version + id: detect + env: + GH_TOKEN: ${{ github.token }} + INPUT_VERSION: ${{ inputs.salt_version }} + run: | + set -euo pipefail + if [[ -n "${INPUT_VERSION:-}" ]]; then + v="$INPUT_VERSION" + else + v=$(gh api repos/saltstack/salt/releases/latest --jq '.tag_name') + fi + v=${v#v} + IFS='.' read -r maj min <<<"$v" + if [[ "${min:-0}" -eq 0 ]]; then + echo "::notice::skipping $v — .0 LTS bumps require manual editing" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "salt_version=$v" >> "$GITHUB_OUTPUT" + echo "branch=topic/release/$v" >> "$GITHUB_OUTPUT" + echo "detected $v" + fi + + - name: Look up existing PR for this version + id: lookup + if: steps.detect.outputs.skip != 'true' + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.detect.outputs.branch }} + run: | + set -euo pipefail + pr=$(gh pr list --state all --head "$BRANCH" \ + --json number,state,mergedAt --jq '.[0] // empty') + if [[ -n "$pr" ]]; then + echo "pr_number=$(echo "$pr" | jq -r '.number')" >> "$GITHUB_OUTPUT" + echo "pr_state=$(echo "$pr" | jq -r '.state')" >> "$GITHUB_OUTPUT" + [[ "$(echo "$pr" | jq -r '.mergedAt // empty')" != "" ]] \ + && echo "merged=true" >> "$GITHUB_OUTPUT" + fi + echo "lookup: $pr" + + - name: Checkout main + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.pr_number == '' + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: Generate edits and open PR + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.pr_number == '' + id: create + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.detect.outputs.branch }} + SALT: ${{ steps.detect.outputs.salt_version }} + run: | + set -euo pipefail + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git checkout -b "$BRANCH" + + old=$(jq -r '.versions | to_entries[] + | select(.value.default == true).key' \ + tools/supported-versions.json) + new="$SALT" + echo "renaming key $old -> $new in tools/supported-versions.json" + sed -i "s/\"${old//./\\.}\":/\"${new}\":/" tools/supported-versions.json + + if git diff --quiet; then + echo "::error::no changes produced for $SALT — supported-versions.json may already be at $SALT" + exit 1 + fi + git add -A + git commit -m "Update to Salt $SALT" + git push -u origin "$BRANCH" + gh pr create --base main --head "$BRANCH" \ + --title "Update to Salt $SALT" \ + --body "Automated update of supported-versions.json for Salt $SALT point release." + + - name: Wait for required PR checks + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.detect.outputs.branch }} + run: | + set -euo pipefail + sleep 15 # let any checks register + gh pr checks "$BRANCH" --watch --required || true + + - name: Merge PR + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.detect.outputs.branch }} + run: | + set -euo pipefail + gh pr merge "$BRANCH" --merge + + - name: Push annotated release tag (idempotent) + if: steps.detect.outputs.skip != 'true' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SALT: ${{ steps.detect.outputs.salt_version }} + run: | + set -euo pipefail + latest=$(gh api "repos/$REPO/tags" --jq '.[0].name') + v=${latest#v} + IFS='.' read -r maj min _ <<<"$v" + next="v${maj}.$((min + 1)).0" + if gh api "repos/$REPO/git/refs/tags/$next" >/dev/null 2>&1; then + echo "tag $next already exists, skipping" + exit 0 + fi + sleep 5 + sha=$(gh api "repos/$REPO/commits/main" --jq '.sha') + tag_sha=$(gh api -X POST "repos/$REPO/git/tags" \ + -f tag="$next" \ + -f message="v${SALT} release" \ + -f object="$sha" -f type=commit --jq '.sha') + gh api -X POST "repos/$REPO/git/refs" \ + -f ref="refs/tags/$next" -f sha="$tag_sha" >/dev/null + echo "pushed $next -> $sha" From 3c5a7302f6e68fc7106d2e9130349176424444c3 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 15 Jun 2026 14:15:21 -0700 Subject: [PATCH 2/2] Gate downstream steps on annotation-based done check The previous tag-push step only checked whether the *next* computed tag name existed, which fails after success: latest=v1.34.0, next=v1.35.0, which doesn't exist, so we'd push v1.35.0 every poll. The repo's tag annotation convention ("v release") is the canonical end-state marker. Gate wait/merge/tag-push on it. --- .github/workflows/release.yml | 38 +++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cadc72a..e0c899d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,32 @@ jobs: fi echo "lookup: $pr" + - name: Check if release already complete + id: done_check + if: steps.detect.outputs.skip != 'true' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + SALT: ${{ steps.detect.outputs.salt_version }} + run: | + set -euo pipefail + # Canonical end-state marker: the repo's tag annotation convention is + # "v release" (e.g. "v3008.0 release"). If the latest + # tag already carries this annotation for our salt_version, the + # release is done and downstream steps short-circuit. + latest=$(gh api "repos/$REPO/tags" --jq '.[0].name // empty') + if [[ -n "$latest" ]]; then + obj=$(gh api "repos/$REPO/git/refs/tags/$latest" --jq '.object') + if [[ "$(echo "$obj" | jq -r '.type')" == "tag" ]]; then + sha=$(echo "$obj" | jq -r '.sha') + msg=$(gh api "repos/$REPO/git/tags/$sha" --jq '.message') + if [[ "$msg" == *"v${SALT} release"* ]]; then + echo "::notice::latest tag $latest annotated for Salt $SALT — done" + echo "done=true" >> "$GITHUB_OUTPUT" + fi + fi + fi + - name: Checkout main if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.pr_number == '' uses: actions/checkout@v4 @@ -116,7 +142,7 @@ jobs: --body "Automated update of supported-versions.json for Salt $SALT point release." - name: Wait for required PR checks - if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' && steps.done_check.outputs.done != 'true' env: GH_TOKEN: ${{ github.token }} BRANCH: ${{ steps.detect.outputs.branch }} @@ -126,7 +152,7 @@ jobs: gh pr checks "$BRANCH" --watch --required || true - name: Merge PR - if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' + if: steps.detect.outputs.skip != 'true' && steps.lookup.outputs.merged != 'true' && steps.done_check.outputs.done != 'true' env: GH_TOKEN: ${{ github.token }} BRANCH: ${{ steps.detect.outputs.branch }} @@ -134,8 +160,8 @@ jobs: set -euo pipefail gh pr merge "$BRANCH" --merge - - name: Push annotated release tag (idempotent) - if: steps.detect.outputs.skip != 'true' + - name: Push annotated release tag + if: steps.detect.outputs.skip != 'true' && steps.done_check.outputs.done != 'true' env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} @@ -146,10 +172,6 @@ jobs: v=${latest#v} IFS='.' read -r maj min _ <<<"$v" next="v${maj}.$((min + 1)).0" - if gh api "repos/$REPO/git/refs/tags/$next" >/dev/null 2>&1; then - echo "tag $next already exists, skipping" - exit 0 - fi sleep 5 sha=$(gh api "repos/$REPO/commits/main" --jq '.sha') tag_sha=$(gh api -X POST "repos/$REPO/git/tags" \