diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 745714d..a386ef5 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -9,7 +9,7 @@ jobs: release_doctor: name: release doctor runs-on: ubuntu-latest - if: github.repository == 'fw-ai-external/python-sdk' && (github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + if: github.repository == 'fw-ai-external/python-sdk' && (github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'autorelease/') || github.head_ref == 'next') steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 3e0b966..0f043f0 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -1,28 +1,23 @@ name: Release Tag -# Fires when a "release: X.Y.Z" commit lands on main (typically via -# squash-merge of an upstream-prepared release PR; subject has no "v" -# prefix, the tag does). Creates the matching git tag vX.Y.Z and a -# GitHub Release, which is what publish-pypi.yml listens for. The -# GitHub Release MUST be created with FW_AI_BOT_TOKEN (a PAT) rather -# than github.token, because Release events triggered by github.token -# do not cascade into other workflows — using github.token would -# silently break the publish chain. Idempotent — the tag/release won't -# be recreated if they already exist. +# Fires on every push to main. If pyproject.toml carries a version that has +# not yet been tagged, this workflow creates the matching git tag vX.Y.Z and a +# GitHub Release, which is what publish-pypi.yml listens for. pyproject.toml +# is the single source of truth for the released version — no commit-subject +# regex, no manifest, no workflow_dispatch override. The GitHub Release MUST +# be created with FW_AI_BOT_TOKEN (a PAT) rather than github.token, because +# Release events triggered by github.token do not cascade into other +# workflows. Idempotent — the tag/release won't be recreated if they already +# exist. on: push: branches: - main - workflow_dispatch: - inputs: - version: - description: Version to tag (e.g. 1.2.0-alpha.72); leave blank to read manifest - type: string - required: false permissions: contents: write + pull-requests: write jobs: tag: @@ -32,62 +27,56 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.FW_AI_BOT_TOKEN || github.token }} + token: ${{ secrets.FW_AI_BOT_TOKEN }} - name: Determine version id: version - env: - DISPATCH_VERSION: ${{ github.event.inputs.version }} run: | set -euo pipefail - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -n "${DISPATCH_VERSION}" ]]; then - version="${DISPATCH_VERSION}" - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - # Manual recovery dispatch with no version: read the manifest. - version="$(python3 -c 'import json; print(json.load(open(".release-please-manifest.json"))["."])')" - else - # Push to main: the commit subject is the source of truth. - # The optional " (#NNN)" suffix is what GitHub appends on - # squash-merge; capture only the version token. - subject="$(git log -1 --format=%s)" - if [[ ! "${subject}" =~ ^release:\ +([0-9][0-9A-Za-z._-]*)(\ +\(#[0-9]+\))?$ ]]; then - echo "Most recent commit is not a release commit; nothing to tag." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - version="${BASH_REMATCH[1]}" - fi + # Single source of truth: pyproject.toml [project] version on the + # merged commit. No commit-subject parsing, no manifest, no fallback. + version="$(python3 -c ' + import sys + try: + import tomllib + except ModuleNotFoundError: + import tomli as tomllib + with open("pyproject.toml", "rb") as f: + print(tomllib.load(f)["project"]["version"]) + ')" - # Belt-and-suspenders: validate the version regardless of source - # (push subject, explicit dispatch input, or manifest fallback). if [[ ! "${version}" =~ ^[0-9][0-9A-Za-z._-]*$ ]]; then - echo "::error::Invalid release version: ${version}" + echo "::error::Invalid version in pyproject.toml: ${version}" exit 1 fi - tag="v${version}" - if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then - echo "Tag ${tag} already exists; skipping." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - { - echo "skip=false" echo "version=${version}" - echo "tag=${tag}" + echo "tag=v${version}" } >> "$GITHUB_OUTPUT" + - name: Check if tag exists + id: tag + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Require FW_AI_BOT_TOKEN - if: steps.version.outputs.skip != 'true' env: BOT_TOKEN: ${{ secrets.FW_AI_BOT_TOKEN }} run: | [[ -n "${BOT_TOKEN}" ]] || { echo "::error::FW_AI_BOT_TOKEN is required"; exit 1; } - name: Create tag and GitHub Release - if: steps.version.outputs.skip != 'true' + if: steps.tag.outputs.exists == 'false' env: GH_TOKEN: ${{ secrets.FW_AI_BOT_TOKEN }} VERSION: ${{ steps.version.outputs.version }} @@ -121,22 +110,33 @@ jobs: ${prerelease_flag} - name: Mark release PR as tagged - if: steps.version.outputs.skip != 'true' + # Runs unconditionally so that recovering from a partial prior failure + # (tag created, label flip failed) just needs a workflow rerun. The + # step is a no-op on commits that aren't release-PR merges (no PR + # found) and on PRs already flipped to 'tagged' (no pending label). + # Loud failure only when this run just created the tag but no PR is + # associated — that indicates a misconfigured release commit. env: GH_TOKEN: ${{ secrets.FW_AI_BOT_TOKEN }} + TAG_JUST_CREATED: ${{ steps.tag.outputs.exists == 'false' }} run: | set -euo pipefail - # Find the PR that was squash-merged to produce this release commit - # and flip the autorelease labels: pending -> tagged. - # Failure here is informational only — the tag and GitHub Release - # are already created and the next workflow listening on Release - # has fired. pr_number="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \ - --jq '.[0].number // empty' 2>/dev/null || true)" + --jq '.[0].number // empty')" if [[ -z "${pr_number}" ]]; then - echo "No PR association for ${GITHUB_SHA}; skipping label update." + if [[ "${TAG_JUST_CREATED}" == "true" ]]; then + echo "::error::Tag was created this run but no PR is associated with ${GITHUB_SHA}." + exit 1 + fi + echo "No PR associated with ${GITHUB_SHA}; nothing to flip." + exit 0 + fi + labels="$(gh pr view "${pr_number}" --repo "${GITHUB_REPOSITORY}" \ + --json labels --jq '[.labels[].name] | join(",")')" + if [[ ",${labels}," != *",autorelease: pending,"* ]]; then + echo "PR #${pr_number} does not carry 'autorelease: pending'; nothing to flip." exit 0 fi gh pr edit "${pr_number}" --repo "${GITHUB_REPOSITORY}" \ --remove-label "autorelease: pending" \ - --add-label "autorelease: tagged" || true + --add-label "autorelease: tagged" diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 1481190..0000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.2.0-alpha.72" -} diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index c24a393..0000000 --- a/release-please-config.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "packages": { - ".": {} - }, - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "include-v-in-tag": true, - "include-component-in-tag": false, - "versioning": "prerelease", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": false, - "pull-request-header": "Automated Release PR", - "pull-request-title-pattern": "release: ${version}", - "changelog-sections": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "chore", - "section": "Chores" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "style", - "section": "Styles" - }, - { - "type": "refactor", - "section": "Refactors" - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System" - }, - { - "type": "ci", - "section": "Continuous Integration", - "hidden": true - } - ], - "release-type": "python", - "extra-files": [ - "src/fireworks/_version.py" - ] -}