From 7742ecae5ded51bdbe879df132bb9fb21920d5a7 Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Wed, 6 May 2026 12:29:31 -0400 Subject: [PATCH 1/3] feat: adopt Auto-based release strategy from redis-vl-python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the manual GitHub-Release-triggered publish workflow with an automated label-driven release pipeline using Intuit Auto. - Add .autorc with 11 semantic versioning labels (major/minor/patch/skip/release + changelog-only labels) - Add .github/release-drafter-config.yml for PR auto-labeling and categorized release notes - Add .github/workflows/auto-release.yml: gate โ†’ canary-build โ†’ release โ†’ build-and-publish - Triggers on push to main; skips if merged PR lacks auto:release label - Uses GitHub App token (RELEASE_BOT_APP_ID + RELEASE_BOT_PRIVATE_KEY secrets) to commit version bumps and push tags - Auto-updates pyproject.toml version and creates GitHub Release with generated changelog - Builds and publishes to PyPI via uv at the release tag - Remove .github/workflows/release.yml (superseded by auto-release.yml) - Add version auto-management comment to pyproject.toml Requires two new repo secrets: RELEASE_BOT_APP_ID and RELEASE_BOT_PRIVATE_KEY (existing PYPI secret is reused). Co-Authored-By: Claude Sonnet 4.6 --- .autorc | 99 ++++++++++ .github/release-drafter-config.yml | 47 +++++ .github/workflows/auto-release.yml | 283 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 83 --------- pyproject.toml | 2 +- 5 files changed, 430 insertions(+), 84 deletions(-) create mode 100644 .autorc create mode 100644 .github/release-drafter-config.yml create mode 100644 .github/workflows/auto-release.yml delete mode 100644 .github/workflows/release.yml diff --git a/.autorc b/.autorc new file mode 100644 index 0000000..d65240d --- /dev/null +++ b/.autorc @@ -0,0 +1,99 @@ +{ + "plugins": [ + "git-tag", + "all-contributors", + "conventional-commits", + "released" + ], + "owner": "redis-developer", + "repo": "sql-redis", + "onlyPublishWithReleaseLabel": true, + "labels": [ + { + "name": "auto:major", + "changelogTitle": "๐Ÿ’ฅ Breaking Change", + "description": "Increment the major version when merged", + "releaseType": "major", + "color": "#C5000B", + "overwrite": true + }, + { + "name": "auto:minor", + "changelogTitle": "๐Ÿš€ Enhancement", + "description": "Increment the minor version when merged", + "releaseType": "minor", + "color": "#F1A60E", + "overwrite": true + }, + { + "name": "auto:patch", + "changelogTitle": "๐Ÿ› Bug Fix", + "description": "Increment the patch version when merged", + "releaseType": "patch", + "color": "#870048", + "overwrite": true + }, + { + "name": "auto:skip-release", + "description": "Preserve the current version when merged", + "releaseType": "skip", + "color": "#bf5416", + "overwrite": true + }, + { + "name": "auto:release", + "description": "Create a release when this PR is merged", + "releaseType": "release", + "color": "#007f70", + "overwrite": true + }, + { + "name": "auto:internal", + "changelogTitle": "๐Ÿ  Internal", + "description": "Changes only affect the internal API", + "releaseType": "none", + "color": "#696969", + "overwrite": true + }, + { + "name": "auto:documentation", + "changelogTitle": "๐Ÿ“ Documentation", + "description": "Changes only affect the documentation", + "releaseType": "none", + "color": "#cfd3d7", + "overwrite": true + }, + { + "name": "auto:tests", + "changelogTitle": "๐Ÿงช Tests", + "description": "Add or improve existing tests", + "releaseType": "none", + "color": "#ffd3cc", + "overwrite": true + }, + { + "name": "auto:dependencies", + "changelogTitle": "๐Ÿ”ฉ Dependency Updates", + "description": "Update one or more dependencies version", + "releaseType": "none", + "color": "#8732bc", + "overwrite": true + }, + { + "name": "auto:performance", + "changelogTitle": "๐ŸŽ Performance", + "description": "Improve performance of an existing feature", + "releaseType": "patch", + "color": "#f4b2d8", + "overwrite": true + }, + { + "name": "auto:ci", + "changelogTitle": "CI/CD Changes", + "description": "Updates to CI/CD workflows and processes", + "releaseType": "none", + "color": "#E5A3DD", + "overwrite": true + } + ] +} diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml new file mode 100644 index 0000000..b1cabd4 --- /dev/null +++ b/.github/release-drafter-config.yml @@ -0,0 +1,47 @@ +name-template: '$NEXT_MINOR_VERSION' +tag-template: 'v$NEXT_MINOR_VERSION' +autolabeler: + - label: 'maintenance' + files: + - '*.md' + - '.github/*' + - label: 'bug' + branch: + - '/bug-.+' + - label: 'maintenance' + branch: + - '/maintenance-.+' + - label: 'feature' + branch: + - '/feature-.+' +categories: + - title: 'Breaking Changes' + labels: + - 'breakingchange' + - title: '๐Ÿงช Experimental Features' + labels: + - 'experimental' + - title: '๐Ÿš€ New Features' + labels: + - 'feature' + - 'enhancement' + - title: '๐Ÿ› Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'BUG' + - title: '๐Ÿงฐ Maintenance' + label: 'maintenance' +change-template: '- $TITLE (#$NUMBER)' +exclude-labels: + - 'skip-changelog' +template: | + # Changes + + $CHANGES + + ## Contributors + We'd like to thank all the contributors who worked on this release! + + $CONTRIBUTORS diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..d543c62 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,283 @@ +name: Publish Release + +on: + push: + branches: [main] + workflow_dispatch: {} + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: publish-release-${{ github.ref }} + cancel-in-progress: false + +env: + PYTHON_VERSION: "3.11" + UV_VERSION: "0.7.13" + AUTO_VERSION: "11.3.6" + RELEASE_BOT_NAME: "applied-ai-releases[bot]" + RELEASE_BOT_EMAIL: "applied-ai-releases[bot]@users.noreply.github.com" + +jobs: + gate: + name: Gate on merged PR label + runs-on: ubuntu-latest + + # Prevent infinite loops from the bot's "chore(release)" commit. + if: github.actor != 'applied-ai-releases[bot]' + + outputs: + should_release: ${{ steps.find_pr.outputs.should_release }} + pr_number: ${{ steps.find_pr.outputs.pr_number }} + + steps: + - name: Find merged PR for this commit and check labels + id: find_pr + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const sha = context.sha; + const maxAttempts = 6; + let pulls; + + // GitHub can briefly lag in associating the merge commit on main back to its PR, + // so retry for a short window before concluding there is no releasable PR. + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, repo, commit_sha: sha + }); + + if (pulls.data.length) { + core.notice( + `Found ${pulls.data.length} PR association(s) for commit ${sha} on attempt ${attempt}/${maxAttempts}.` + ); + break; + } + + if (attempt < maxAttempts) { + core.notice( + `No PR associated with commit ${sha} on attempt ${attempt}/${maxAttempts}. Retrying in 10 seconds...` + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + } + } + + if (!pulls.data.length) { + core.setOutput("should_release", "false"); + core.setOutput("pr_number", ""); + core.notice( + `No PR associated with commit ${sha} after ${maxAttempts} attempts. Not releasing.` + ); + return; + } + + // Pick the merged PR targeting main (most common). + const pr = pulls.data.find(p => p.merged_at && p.base?.ref === "main") ?? pulls.data[0]; + const labels = (pr.labels || []).map(l => l.name); + + core.setOutput("pr_number", String(pr.number)); + const should = labels.includes("auto:release"); + core.setOutput("should_release", should ? "true" : "false"); + + core.notice(`PR #${pr.number} labels: ${labels.join(", ")}`); + core.notice(`should_release=${should}`); + + canary-build: + runs-on: ubuntu-latest + needs: gate + if: needs.gate.outputs.should_release == 'true' + + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + python-version: ${{ env.PYTHON_VERSION }} + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install dependencies + run: uv sync --frozen + + - name: Check that package can be built + run: uv build + + release: + runs-on: ubuntu-latest + needs: [gate, canary-build] + if: needs.gate.outputs.should_release == 'true' + outputs: + version: ${{ steps.latest_release.outputs.version }} + + steps: + - name: Generate GitHub App token + id: app_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ steps.app_token.outputs.token }} + + - name: Download and install auto + run: | + curl -L https://github.com/intuit/auto/releases/download/v${{ env.AUTO_VERSION }}/auto-linux.gz -o auto-linux.gz + gunzip auto-linux.gz + chmod +x auto-linux + sudo mv auto-linux /usr/local/bin/auto + auto --version + + - name: Sanity check + env: + GH_TOKEN: ${{ steps.app_token.outputs.token }} + run: | + set -euo pipefail + auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run -v + + - name: Capture previous tag + id: previous_tag + run: | + set -euo pipefail + echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT" + + - name: Resolve release version + id: latest_release + env: + GH_TOKEN: ${{ steps.app_token.outputs.token }} + run: | + set -euo pipefail + RAW_VERSION="$(auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run --quiet | tail -n1 | tr -d '\r')" + VERSION="${RAW_VERSION#v}" + if [ -z "$VERSION" ]; then + echo "Could not resolve release version" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Resolved version: $VERSION" + + - name: Apply release version to pyproject.toml + run: | + set -euo pipefail + VERSION="${{ steps.latest_release.outputs.version }}" + sed -i "s/^version = \".*\"$/version = \"${VERSION}\"/" pyproject.toml + grep '^version = ' pyproject.toml + + - name: Commit and push version bump + env: + GH_TOKEN: ${{ steps.app_token.outputs.token }} + run: | + set -euo pipefail + if git diff --quiet -- pyproject.toml; then + echo "No pyproject version change to commit." + else + git config user.name "${RELEASE_BOT_NAME}" + git config user.email "${RELEASE_BOT_EMAIL}" + git add pyproject.toml + # Include [skip ci] to avoid running the workflow again on this bot commit. + git commit -m "chore(release): set pyproject version to ${{ steps.latest_release.outputs.version }} [skip ci]" + git push origin HEAD:main + fi + + - name: Capture release commit SHA + id: release_commit + run: | + set -euo pipefail + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Create labels + env: + GH_TOKEN: ${{ steps.app_token.outputs.token }} + run: auto create-labels + + - name: Create and push tag + run: | + set -euo pipefail + TAG="v${{ steps.latest_release.outputs.version }}" + TARGET_SHA="${{ steps.release_commit.outputs.sha }}" + + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + EXISTING_SHA="$(git rev-list -n1 "$TAG")" + if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then + echo "Tag $TAG already exists at $TARGET_SHA. Skipping creation." + exit 0 + fi + + echo "Tag $TAG already exists at $EXISTING_SHA but expected $TARGET_SHA." + exit 1 + fi + + git tag "$TAG" "$TARGET_SHA" + git push origin "$TAG" + + - name: Create GitHub release with auto notes + env: + GH_TOKEN: ${{ steps.app_token.outputs.token }} + run: | + set -euo pipefail + auto release \ + --from "${{ steps.previous_tag.outputs.tag }}" \ + --to "${{ steps.release_commit.outputs.sha }}" \ + --use-version "v${{ steps.latest_release.outputs.version }}" + + build-and-publish: + runs-on: ubuntu-latest + needs: [gate, release] + if: needs.gate.outputs.should_release == 'true' + + steps: + - name: Check out repository + uses: actions/checkout@v6 + with: + ref: v${{ needs.release.outputs.version }} + fetch-depth: 0 + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + python-version: ${{ env.PYTHON_VERSION }} + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install dependencies + run: uv sync --frozen + + - name: Build package + run: uv build + + - name: Publish to PyPI + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI }} + run: uv publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index ed0dcdc..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Publish Release - -on: - release: - types: [published] - -env: - PYTHON_VERSION: "3.11" - UV_VERSION: "0.7.13" - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v6 - - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: ${{ env.UV_VERSION }} - enable-cache: true - python-version: ${{ env.PYTHON_VERSION }} # sets UV_PYTHON - cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Install dependencies - run: | - uv sync --frozen - - - name: Build package - run: uv build - - - name: Upload build - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - - publish: - needs: build - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v6 - - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: ${{ env.UV_VERSION }} - enable-cache: true - python-version: ${{ env.PYTHON_VERSION }} # sets UV_PYTHON - cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Install dependencies - run: | - uv sync --frozen - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Publish to PyPI - env: - UV_PUBLISH_TOKEN: ${{ secrets.PYPI }} - run: uv publish \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ac92ccc..aef3a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sql-redis" -version = "0.4.0" +version = "0.4.0" # NOTE: This version value is automatically incremented by the release workflow - do not manually adjust it. description = "SQL to Redis command translation utility" authors = [{ name = "Redis Inc.", email = "applied.ai@redis.com" }] requires-python = ">=3.9,<3.14" From 9f0d08780e6eaab3408a630a91bd08f098da06fd Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Wed, 6 May 2026 12:36:36 -0400 Subject: [PATCH 2/3] refactor(release): align with redis-agent-kit (drop GitHub App requirement) Switch the auto-release workflow to use the built-in github-actions[bot] identity with GITHUB_TOKEN (and optional RELEASE_PAT fallback for protected branches), removing the need for a custom GitHub App and its two secrets. - Drop RELEASE_BOT_APP_ID / RELEASE_BOT_PRIVATE_KEY usage - Use github-actions[bot] as the release bot - Token: secrets.RELEASE_PAT || secrets.GITHUB_TOKEN - Add workflow_dispatch first_release input for bootstrap publishing - Combine canary build into the release job ("Sanity build (pre-tag)") - Tolerate missing previous tag for the very first release - Remove release-drafter-config.yml (not used by this strategy) - Drop auto:performance label to match redis-agent-kit's 10-label set Now the only required secret is the existing PYPI token. Co-Authored-By: Claude Sonnet 4.6 --- .autorc | 9 -- .github/release-drafter-config.yml | 47 ------ .github/workflows/auto-release.yml | 238 ++++++++++++----------------- 3 files changed, 95 insertions(+), 199 deletions(-) delete mode 100644 .github/release-drafter-config.yml diff --git a/.autorc b/.autorc index d65240d..2e7740c 100644 --- a/.autorc +++ b/.autorc @@ -1,7 +1,6 @@ { "plugins": [ "git-tag", - "all-contributors", "conventional-commits", "released" ], @@ -79,14 +78,6 @@ "color": "#8732bc", "overwrite": true }, - { - "name": "auto:performance", - "changelogTitle": "๐ŸŽ Performance", - "description": "Improve performance of an existing feature", - "releaseType": "patch", - "color": "#f4b2d8", - "overwrite": true - }, { "name": "auto:ci", "changelogTitle": "CI/CD Changes", diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml deleted file mode 100644 index b1cabd4..0000000 --- a/.github/release-drafter-config.yml +++ /dev/null @@ -1,47 +0,0 @@ -name-template: '$NEXT_MINOR_VERSION' -tag-template: 'v$NEXT_MINOR_VERSION' -autolabeler: - - label: 'maintenance' - files: - - '*.md' - - '.github/*' - - label: 'bug' - branch: - - '/bug-.+' - - label: 'maintenance' - branch: - - '/maintenance-.+' - - label: 'feature' - branch: - - '/feature-.+' -categories: - - title: 'Breaking Changes' - labels: - - 'breakingchange' - - title: '๐Ÿงช Experimental Features' - labels: - - 'experimental' - - title: '๐Ÿš€ New Features' - labels: - - 'feature' - - 'enhancement' - - title: '๐Ÿ› Bug Fixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' - - 'BUG' - - title: '๐Ÿงฐ Maintenance' - label: 'maintenance' -change-template: '- $TITLE (#$NUMBER)' -exclude-labels: - - 'skip-changelog' -template: | - # Changes - - $CHANGES - - ## Contributors - We'd like to thank all the contributors who worked on this release! - - $CONTRIBUTORS diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index d543c62..db54e05 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -1,9 +1,14 @@ -name: Publish Release +name: Auto Release on: push: branches: [main] - workflow_dispatch: {} + workflow_dispatch: + inputs: + first_release: + description: "Bootstrap: publish the version currently in pyproject.toml. Bypasses the auto:release label gate and skips version bumping. Use only for the first release or manual recovery." + type: boolean + default: false permissions: contents: write @@ -11,93 +16,87 @@ permissions: pull-requests: write concurrency: - group: publish-release-${{ github.ref }} + group: auto-release-${{ github.ref }} cancel-in-progress: false env: PYTHON_VERSION: "3.11" UV_VERSION: "0.7.13" AUTO_VERSION: "11.3.6" - RELEASE_BOT_NAME: "applied-ai-releases[bot]" - RELEASE_BOT_EMAIL: "applied-ai-releases[bot]@users.noreply.github.com" + RELEASE_BOT_NAME: "github-actions[bot]" + RELEASE_BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com" jobs: gate: name: Gate on merged PR label runs-on: ubuntu-latest - # Prevent infinite loops from the bot's "chore(release)" commit. - if: github.actor != 'applied-ai-releases[bot]' - + if: github.actor != 'github-actions[bot]' outputs: should_release: ${{ steps.find_pr.outputs.should_release }} pr_number: ${{ steps.find_pr.outputs.pr_number }} - steps: - - name: Find merged PR for this commit and check labels + - name: Find merged PR and check labels id: find_pr uses: actions/github-script@v7 + env: + FIRST_RELEASE: ${{ github.event.inputs.first_release }} with: script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const sha = context.sha; + // Manual bootstrap: bypass the PR label check entirely. + if (context.eventName === 'workflow_dispatch' && process.env.FIRST_RELEASE === 'true') { + core.notice('Manual first_release=true; bypassing PR label check.'); + core.setOutput('should_release', 'true'); + core.setOutput('pr_number', ''); + return; + } + const { owner, repo } = context.repo; + const sha = context.sha; const maxAttempts = 6; let pulls; - - // GitHub can briefly lag in associating the merge commit on main back to its PR, - // so retry for a short window before concluding there is no releasable PR. + // GitHub can briefly lag in associating a merge commit with its PR, so retry. for (let attempt = 1; attempt <= maxAttempts; attempt++) { pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner, repo, commit_sha: sha + owner, repo, commit_sha: sha, }); - - if (pulls.data.length) { - core.notice( - `Found ${pulls.data.length} PR association(s) for commit ${sha} on attempt ${attempt}/${maxAttempts}.` - ); - break; - } - + if (pulls.data.length) break; if (attempt < maxAttempts) { - core.notice( - `No PR associated with commit ${sha} on attempt ${attempt}/${maxAttempts}. Retrying in 10 seconds...` - ); - await new Promise((resolve) => setTimeout(resolve, 10000)); + await new Promise((r) => setTimeout(r, 10000)); } } - - if (!pulls.data.length) { - core.setOutput("should_release", "false"); - core.setOutput("pr_number", ""); - core.notice( - `No PR associated with commit ${sha} after ${maxAttempts} attempts. Not releasing.` - ); + if (!pulls.data || !pulls.data.length) { + core.notice(`No PR associated with ${sha}. Not releasing.`); + core.setOutput('should_release', 'false'); + core.setOutput('pr_number', ''); return; } - - // Pick the merged PR targeting main (most common). - const pr = pulls.data.find(p => p.merged_at && p.base?.ref === "main") ?? pulls.data[0]; - const labels = (pr.labels || []).map(l => l.name); - - core.setOutput("pr_number", String(pr.number)); - const should = labels.includes("auto:release"); - core.setOutput("should_release", should ? "true" : "false"); - - core.notice(`PR #${pr.number} labels: ${labels.join(", ")}`); + const pr = pulls.data.find((p) => p.merged_at && p.base?.ref === 'main') ?? pulls.data[0]; + const labels = (pr.labels || []).map((l) => l.name); + const should = labels.includes('auto:release'); + core.setOutput('pr_number', String(pr.number)); + core.setOutput('should_release', should ? 'true' : 'false'); + core.notice(`PR #${pr.number} labels: ${labels.join(', ')}`); core.notice(`should_release=${should}`); - canary-build: + release: + name: Tag, release, and publish runs-on: ubuntu-latest needs: gate if: needs.gate.outputs.should_release == 'true' - + env: + # 'true' for manual bootstrap runs; 'false' or empty otherwise. + FIRST_RELEASE: ${{ github.event.inputs.first_release || 'false' }} steps: - - name: Check out repository + - name: Checkout uses: actions/checkout@v6 with: ref: main fetch-depth: 0 + fetch-tags: true + # RELEASE_PAT (a PAT with repo + workflow scopes) lets the bot push to + # protected branches and trigger other workflows. Falls back to + # GITHUB_TOKEN for repos without branch protection. + token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} - name: Install Python uses: actions/setup-python@v6 @@ -114,82 +113,52 @@ jobs: pyproject.toml uv.lock - - name: Install dependencies - run: uv sync --frozen - - - name: Check that package can be built - run: uv build - - release: - runs-on: ubuntu-latest - needs: [gate, canary-build] - if: needs.gate.outputs.should_release == 'true' - outputs: - version: ${{ steps.latest_release.outputs.version }} - - steps: - - name: Generate GitHub App token - id: app_token - uses: actions/create-github-app-token@v2 - with: - app-id: ${{ secrets.RELEASE_BOT_APP_ID }} - private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: main - fetch-depth: 0 - fetch-tags: true - token: ${{ steps.app_token.outputs.token }} - - - name: Download and install auto + - name: Install auto run: | - curl -L https://github.com/intuit/auto/releases/download/v${{ env.AUTO_VERSION }}/auto-linux.gz -o auto-linux.gz + set -euo pipefail + curl -L "https://github.com/intuit/auto/releases/download/v${AUTO_VERSION}/auto-linux.gz" -o auto-linux.gz gunzip auto-linux.gz chmod +x auto-linux sudo mv auto-linux /usr/local/bin/auto auto --version - - name: Sanity check - env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} - run: | - set -euo pipefail - auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run -v + - name: Sanity build (pre-tag) + run: uv build - name: Capture previous tag id: previous_tag run: | set -euo pipefail - echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT" + echo "tag=$(git describe --tags --abbrev=0 2>/dev/null || echo '')" >> "$GITHUB_OUTPUT" - - name: Resolve release version - id: latest_release + - name: Resolve next version (auto) + id: resolve_version + if: env.FIRST_RELEASE != 'true' env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} run: | set -euo pipefail RAW_VERSION="$(auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run --quiet | tail -n1 | tr -d '\r')" VERSION="${RAW_VERSION#v}" if [ -z "$VERSION" ]; then - echo "Could not resolve release version" + echo "Could not resolve release version from auto." exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Resolved version: $VERSION" - - name: Apply release version to pyproject.toml + - name: Apply version to pyproject.toml + if: env.FIRST_RELEASE != 'true' run: | set -euo pipefail - VERSION="${{ steps.latest_release.outputs.version }}" + VERSION="${{ steps.resolve_version.outputs.version }}" sed -i "s/^version = \".*\"$/version = \"${VERSION}\"/" pyproject.toml grep '^version = ' pyproject.toml - - name: Commit and push version bump + - name: Commit version bump + if: env.FIRST_RELEASE != 'true' env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} run: | set -euo pipefail if git diff --quiet -- pyproject.toml; then @@ -198,84 +167,67 @@ jobs: git config user.name "${RELEASE_BOT_NAME}" git config user.email "${RELEASE_BOT_EMAIL}" git add pyproject.toml - # Include [skip ci] to avoid running the workflow again on this bot commit. - git commit -m "chore(release): set pyproject version to ${{ steps.latest_release.outputs.version }} [skip ci]" + # [skip ci] avoids re-running this workflow on the bot's own commit. + git commit -m "chore(release): v${{ steps.resolve_version.outputs.version }} [skip ci]" git push origin HEAD:main fi + - name: Read final version from pyproject.toml + id: final_version + run: | + set -euo pipefail + VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "([^"]+)".*/\1/')" + if [ -z "$VERSION" ]; then + echo "Could not read version from pyproject.toml" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Final version: $VERSION" + - name: Capture release commit SHA id: release_commit run: | set -euo pipefail echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - name: Create labels + - name: Create labels (idempotent) env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} run: auto create-labels - name: Create and push tag run: | set -euo pipefail - TAG="v${{ steps.latest_release.outputs.version }}" + TAG="v${{ steps.final_version.outputs.version }}" TARGET_SHA="${{ steps.release_commit.outputs.sha }}" - if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then EXISTING_SHA="$(git rev-list -n1 "$TAG")" if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then - echo "Tag $TAG already exists at $TARGET_SHA. Skipping creation." + echo "Tag $TAG already exists at $TARGET_SHA. Skipping." exit 0 fi - - echo "Tag $TAG already exists at $EXISTING_SHA but expected $TARGET_SHA." + echo "Tag $TAG exists at $EXISTING_SHA but expected $TARGET_SHA." exit 1 fi - git tag "$TAG" "$TARGET_SHA" git push origin "$TAG" - name: Create GitHub release with auto notes env: - GH_TOKEN: ${{ steps.app_token.outputs.token }} + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} run: | set -euo pipefail - auto release \ - --from "${{ steps.previous_tag.outputs.tag }}" \ - --to "${{ steps.release_commit.outputs.sha }}" \ - --use-version "v${{ steps.latest_release.outputs.version }}" - - build-and-publish: - runs-on: ubuntu-latest - needs: [gate, release] - if: needs.gate.outputs.should_release == 'true' - - steps: - - name: Check out repository - uses: actions/checkout@v6 - with: - ref: v${{ needs.release.outputs.version }} - fetch-depth: 0 - - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: ${{ env.UV_VERSION }} - enable-cache: true - python-version: ${{ env.PYTHON_VERSION }} - cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Install dependencies - run: uv sync --frozen + args=(--to "${{ steps.release_commit.outputs.sha }}" --use-version "v${{ steps.final_version.outputs.version }}") + if [ -n "${{ steps.previous_tag.outputs.tag }}" ]; then + args=(--from "${{ steps.previous_tag.outputs.tag }}" "${args[@]}") + fi + auto release "${args[@]}" - - name: Build package - run: uv build + - name: Build final artifacts + run: | + set -euo pipefail + rm -rf dist + uv build - name: Publish to PyPI env: From ebf58e540b78f2d69b54e6236d25c1c9c2a4486e Mon Sep 17 00:00:00 2001 From: Robert Shelton Date: Wed, 6 May 2026 12:41:44 -0400 Subject: [PATCH 3/3] fix(release): move version comment above the version line The auto-release workflow uses sed with the regex \`^version = ".*"\$\` to bump the version. An inline trailing comment broke the \`\$\` anchor, causing sed to silently no-op. Without the version bump, the workflow would tag v0.4.0 again, fail to create a duplicate GitHub release, and \`uv publish\` would error because 0.4.0 already exists on PyPI. Verified by simulating the sed against the file for patch, minor, and major bumps. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aef3a52..b0093be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "sql-redis" -version = "0.4.0" # NOTE: This version value is automatically incremented by the release workflow - do not manually adjust it. +# NOTE: The version below is automatically incremented by the release workflow - do not manually adjust it. +version = "0.4.0" description = "SQL to Redis command translation utility" authors = [{ name = "Redis Inc.", email = "applied.ai@redis.com" }] requires-python = ">=3.9,<3.14"