From 275426a4943842e43e4e28608ae45ffda4ead074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A5=E5=AF=85?= Date: Thu, 10 Sep 2026 09:03:37 +0800 Subject: [PATCH 1/3] docs: update release process guide Document the separate PyPI workflow, remove obsolete per-agent ZIP steps, and add recovery guidance for incomplete releases. Closes #4501 Assisted-by: Codex (model: GPT-5, autonomous) --- .github/workflows/RELEASE-PROCESS.md | 79 +++++++++++++++++++++------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/.github/workflows/RELEASE-PROCESS.md b/.github/workflows/RELEASE-PROCESS.md index 18fe40e858..a6b72acce0 100644 --- a/.github/workflows/RELEASE-PROCESS.md +++ b/.github/workflows/RELEASE-PROCESS.md @@ -4,12 +4,13 @@ This document describes the automated release process for Spec Kit. ## Overview -The release process is split into two workflows to ensure version consistency: +The release process is split into three workflows: -1. **Release Trigger Workflow** (`release-trigger.yml`) - Manages versioning and triggers release -2. **Release Workflow** (`release.yml`) - Builds and publishes artifacts +1. **Release Trigger Workflow** (`release-trigger.yml`) - Manages versioning, creates the tag, and opens the release PR +2. **Release Workflow** (`release.yml`) - Creates the GitHub Release and release notes +3. **Publish to PyPI Workflow** (`publish-pypi.yml`) - Builds and publishes the Python package from an exact tag -This separation ensures that git tags always point to commits with the correct version in `pyproject.toml`. +This separation ensures that git tags point to commits with the correct version in `pyproject.toml` and lets maintainers recover the GitHub and PyPI publishing steps independently. ## Before Creating a Release @@ -20,6 +21,7 @@ This separation ensures that git tags always point to commits with the correct v The CHANGELOG is **automatically generated** from your git commit messages: 1. **During Development**: Write clear, descriptive commit messages: + ```bash git commit -m "feat: Add new authentication feature" git commit -m "fix: Resolve timeout issue in API client (#123)" @@ -35,13 +37,15 @@ The CHANGELOG is **automatically generated** from your git commit messages: ### Commit Message Best Practices Good commit messages make good changelogs: + - **Be descriptive**: "Add user authentication" not "Update files" - **Reference issues/PRs**: Include `(#123)` for automated linking - **Use conventional commits** (optional): `feat:`, `fix:`, `docs:`, `chore:` - **Keep it concise**: One line is ideal, details go in commit body **Example commits that become good changelog entries:** -``` + +```text fix: prepend YAML frontmatter to Cursor .mdc files (#1699) feat: add generic agent support with customizable command directories (#1639) docs: document dual-catalog system for extensions (#1689) @@ -57,11 +61,13 @@ docs: document dual-catalog system for extensions (#1689) 4. Click **Run workflow** The workflow will: + - Auto-increment the patch version (e.g., `0.1.10` → `0.1.11`) - Update `pyproject.toml` - Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag - Commit changes to a `chore/release-vX.Y.Z` branch - Create and push the git tag from that branch +- Bump the release branch to the next patch development version (for example, `1.2.3` → `1.2.4.dev0`) - Open a PR to merge the version bump into `main` - Trigger the release workflow automatically via the tag push @@ -73,11 +79,13 @@ The workflow will: 4. Click **Run workflow** The workflow will: + - Use your specified version - Update `pyproject.toml` - Update `CHANGELOG.md` by adding a new section for the release based on commits since the last tag - Commit changes to a `chore/release-vX.Y.Z` branch - Create and push the git tag from that branch +- Bump the release branch to the next patch development version (for example, `1.2.3` → `1.2.4.dev0`) - Open a PR to merge the version bump into `main` - Trigger the release workflow automatically via the tag push @@ -86,13 +94,14 @@ The workflow will: Once the release trigger workflow completes: 1. A `chore/release-vX.Y.Z` branch is pushed with the version bump commit -2. The git tag is pushed, pointing to that commit +2. The git tag is pushed, pointing to that release commit 3. The **Release Workflow** is automatically triggered by the tag push -4. Release artifacts are built for all supported agents -5. A GitHub Release is created with all assets -6. A PR is opened to merge the version bump branch into `main` +4. A GitHub Release is created with generated release notes. No per-agent ZIP assets are built or uploaded; GitHub still provides its standard source archives +5. The release branch is bumped to the next patch development version +6. A PR is opened to merge both version commits into `main` +7. Run the **Publish to PyPI Workflow** manually with the same tag to build and publish the wheel and source distribution -> **Note**: Merge the auto-opened PR after the release is published to keep `main` in sync. +> **Note**: The GitHub Release and PyPI workflows do not depend on each other. Waiting for the GitHub Release to complete before publishing to PyPI makes the release state easier to verify. Merge the auto-opened PR after publishing to keep `main` on the next development version. ## Workflow Details @@ -102,9 +111,10 @@ Once the release trigger workflow completes: **Trigger**: Manual (`workflow_dispatch`) -**Permissions Required**: `contents: write` +**Permissions Required**: `contents: write`, `pull-requests: write` **Steps**: + 1. Checkout repository 2. Determine version (manual or auto-increment) 3. Check if tag already exists (prevents duplicates) @@ -113,7 +123,8 @@ Once the release trigger workflow completes: 6. Update `CHANGELOG.md` from git commits 7. Commit changes 8. Push branch and tag -9. Open PR to merge version bump into `main` +9. Bump `pyproject.toml` to the next patch development version and push the branch +10. Open PR to merge the release and development version commits into `main` ### Release Workflow @@ -124,12 +135,29 @@ Once the release trigger workflow completes: **Permissions Required**: `contents: write` **Steps**: + 1. Checkout repository at tag 2. Extract version from tag name 3. Check if release already exists -4. Build release package variants (all agents × shell/powershell) -5. Generate release notes from commits -6. Create GitHub Release with all assets +4. Generate release notes from commits +5. Create the GitHub Release without additional uploaded assets + +### Publish to PyPI Workflow + +**File**: `.github/workflows/publish-pypi.yml` + +**Trigger**: Manual (`workflow_dispatch`) with the exact release tag + +**Permissions Required**: `contents: read`, `actions: write` for the build job; `actions: read`, `id-token: write` for the publish job + +**Steps**: + +1. Validate that the input uses the strict `vX.Y.Z` tag format +2. Checkout `refs/tags/vX.Y.Z` +3. Verify that the tag version exactly matches the version in `pyproject.toml` +4. Build the wheel and source distribution with `uv build` +5. Transfer the distributions between jobs as a GitHub Actions artifact +6. Publish them to PyPI with `uv publish` and Trusted Publishing ## Version Constraints @@ -146,7 +174,7 @@ Once the release trigger workflow completes: ✅ **Prevents Drift**: No more manual version synchronization needed -✅ **Clean Separation**: Versioning logic separate from artifact building +✅ **Independent Publishing**: GitHub Release creation and PyPI publishing can be verified and recovered separately ✅ **Flexibility**: Supports both auto-increment and manual versioning @@ -155,6 +183,7 @@ Once the release trigger workflow completes: ### No Commits Since Last Release If you run the release trigger workflow when there are no new commits since the last tag: + - The workflow will still succeed - The CHANGELOG will show "- Initial release" if it's the first release - Or it will be empty if there are no commits @@ -164,13 +193,23 @@ If you run the release trigger workflow when there are no new commits since the ### Tag Already Exists -If you see "Error: Tag vX.Y.Z already exists!", you need to: -- Choose a different version number, or -- Delete the existing tag if it was created in error +If you see "Error: Tag vX.Y.Z already exists!", inspect the existing tag, GitHub Release, and PyPI version before taking action. Do not move or delete a tag for a version that has already been published to PyPI. PyPI does not allow an uploaded distribution filename to be reused, even after deletion. + +Choose a new version if the existing tag or PyPI publication is valid. Only consider removing an erroneous tag when you have confirmed that the version was not published to PyPI and that no consumers rely on it. + +### Recovering an Incomplete Release + +| State | Recovery | +|---|---| +| Tag exists, GitHub Release is missing | Confirm that the tag points to the intended release commit, then create the GitHub Release from that existing tag with the same notes format used by `release.yml`. | +| GitHub Release exists, PyPI version is missing | Run **Publish to PyPI** manually with the exact release tag. | +| PyPI version exists, GitHub Release is missing | Keep the existing tag unchanged and create the GitHub Release from it. | +| PyPI contains only some expected distributions | Inspect the files already published before retrying. Previously used filenames cannot be replaced or reused. | ### Release Workflow Didn't Trigger Check that: + - The release trigger workflow completed successfully - The tag was pushed (check repository tags) - The release workflow is enabled in Actions settings @@ -178,12 +217,14 @@ Check that: ### Version Mismatch If `pyproject.toml` doesn't match the latest tag: + - Run the release trigger workflow to sync versions - Or manually update `pyproject.toml` and push changes before running the release trigger ## Legacy Behavior (Pre-v0.1.10) Before this change, the release workflow: + - Created tags automatically on main branch pushes - Updated `pyproject.toml` AFTER creating the tag - Resulted in tags pointing to commits with outdated versions From 4dae159cc6d2aed0428b958f744a741a395ec25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A5=E5=AF=85?= Date: Fri, 11 Sep 2026 07:38:48 +0800 Subject: [PATCH 2/3] docs: clarify release version and recovery guidance Assisted-by: Codex (model: GPT-6, autonomous) --- .github/workflows/RELEASE-PROCESS.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/RELEASE-PROCESS.md b/.github/workflows/RELEASE-PROCESS.md index a6b72acce0..1ae22f8fe0 100644 --- a/.github/workflows/RELEASE-PROCESS.md +++ b/.github/workflows/RELEASE-PROCESS.md @@ -199,12 +199,18 @@ Choose a new version if the existing tag or PyPI publication is valid. Only cons ### Recovering an Incomplete Release +Verify the release branch/tag, GitHub Release, wheel (`.whl`), and source distribution (`.tar.gz`) separately. A PyPI version page alone does not confirm that both distributions were uploaded. + | State | Recovery | |---|---| +| Tag exists, release branch development bump or PR is incomplete | Keep the tag unchanged. Inspect `chore/release-vX.Y.Z` and any existing PR; restore the branch from the release commit if missing, apply the next patch development bump if needed, and open or reuse the PR into `main`. Merge after publication is complete. Do not rerun Release Trigger for the existing tag. | | Tag exists, GitHub Release is missing | Confirm that the tag points to the intended release commit, then create the GitHub Release from that existing tag with the same notes format used by `release.yml`. | -| GitHub Release exists, PyPI version is missing | Run **Publish to PyPI** manually with the exact release tag. | +| Valid tag exists, neither wheel nor sdist is published to PyPI | Run **Publish to PyPI** manually with the exact release tag, independently of GitHub Release creation. | | PyPI version exists, GitHub Release is missing | Keep the existing tag unchanged and create the GitHub Release from it. | -| PyPI contains only some expected distributions | Inspect the files already published before retrying. Previously used filenames cannot be replaced or reused. | +| Wheel is published, sdist is missing | Verify the published wheel and recover the matching sdist from the original run's `dist` artifact. Use an authorized publishing path to upload only the missing sdist; do not replace the wheel. | +| Sdist is published, wheel is missing | Verify the published sdist and recover the matching wheel from the original run's `dist` artifact. Use an authorized publishing path to upload only the missing wheel; do not replace the sdist. | + +For partial PyPI uploads, inspect the original run and published files before retrying. The current workflow rebuilds and publishes all of `dist/`; it is not a missing-file-only recovery command. If the original artifact is unavailable, investigate how to reproduce the matching distribution before publishing. Previously used filenames cannot be replaced or reused. ### Release Workflow Didn't Trigger @@ -216,10 +222,11 @@ Check that: ### Version Mismatch -If `pyproject.toml` doesn't match the latest tag: +After a release PR is merged, `pyproject.toml` on `main` is expected to differ from the latest release tag. The tag `vX.Y.Z` points to a commit whose package version is exactly `X.Y.Z`; the release branch is subsequently bumped to the next patch development version (for example, `1.2.3` → `1.2.4.dev0`). Do not sync `main` back to the released version. + +The **Publish to PyPI** workflow checks out the release tag and verifies that its version, without the `v` prefix, exactly matches `pyproject.toml` at that commit. A mismatch at the tagged commit is an error; the next development version on `main` is expected. -- Run the release trigger workflow to sync versions -- Or manually update `pyproject.toml` and push changes before running the release trigger +If that check fails, inspect the tag, GitHub Release, and PyPI publication state before choosing a recovery path (see **Tag Already Exists** and **Recovering an Incomplete Release** above). Do not move or recreate a published tag. ## Legacy Behavior (Pre-v0.1.10) From cab314ea0a28531e9bf50b37dd410f8bde720e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A5=E5=AF=85?= Date: Fri, 11 Sep 2026 08:00:40 +0800 Subject: [PATCH 3/3] docs: refine partial release recovery and publishing retries Assisted-by: Codex (autonomous) --- .github/workflows/RELEASE-PROCESS.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/RELEASE-PROCESS.md b/.github/workflows/RELEASE-PROCESS.md index 1ae22f8fe0..47a4dbb8f2 100644 --- a/.github/workflows/RELEASE-PROCESS.md +++ b/.github/workflows/RELEASE-PROCESS.md @@ -91,15 +91,12 @@ The workflow will: ## What Happens Next -Once the release trigger workflow completes: +The release trigger pushes a `chore/release-vX.Y.Z` branch with the release version commit, then pushes the tag pointing to that commit. After the tag push, two paths proceed independently: -1. A `chore/release-vX.Y.Z` branch is pushed with the version bump commit -2. The git tag is pushed, pointing to that release commit -3. The **Release Workflow** is automatically triggered by the tag push -4. A GitHub Release is created with generated release notes. No per-agent ZIP assets are built or uploaded; GitHub still provides its standard source archives -5. The release branch is bumped to the next patch development version -6. A PR is opened to merge both version commits into `main` -7. Run the **Publish to PyPI Workflow** manually with the same tag to build and publish the wheel and source distribution +- The **Release Workflow** creates a GitHub Release with generated release notes. No per-agent ZIP assets are built or uploaded; GitHub still provides its standard source archives. +- The **Release Trigger Workflow** continues by bumping the release branch to the next patch development version and opening a PR to merge both version commits into `main`. + +GitHub Release creation may finish before or after the development bump and PR creation. Run the **Publish to PyPI Workflow** manually with the same tag to build and publish the wheel and source distribution. > **Note**: The GitHub Release and PyPI workflows do not depend on each other. Waiting for the GitHub Release to complete before publishing to PyPI makes the release state easier to verify. Merge the auto-opened PR after publishing to keep `main` on the next development version. @@ -195,7 +192,7 @@ If you run the release trigger workflow when there are no new commits since the If you see "Error: Tag vX.Y.Z already exists!", inspect the existing tag, GitHub Release, and PyPI version before taking action. Do not move or delete a tag for a version that has already been published to PyPI. PyPI does not allow an uploaded distribution filename to be reused, even after deletion. -Choose a new version if the existing tag or PyPI publication is valid. Only consider removing an erroneous tag when you have confirmed that the version was not published to PyPI and that no consumers rely on it. +If the existing tag identifies the intended release, keep it and recover any missing outputs using the guidance below. Choose a new version when publishing different release contents under a version that is already in use. Only consider removing an erroneous tag when you have confirmed that the version was not published to PyPI and that no consumers rely on it. ### Recovering an Incomplete Release @@ -203,14 +200,17 @@ Verify the release branch/tag, GitHub Release, wheel (`.whl`), and source distri | State | Recovery | |---|---| +| Release branch exists, tag is missing | Inspect the failed run and confirm the intended release commit has package version `X.Y.Z`. Create or reuse the local `vX.Y.Z` tag only if it points to that commit, then push it. Do not tag a subsequent `.dev0` commit. Complete any missing development bump or PR as described below. | | Tag exists, release branch development bump or PR is incomplete | Keep the tag unchanged. Inspect `chore/release-vX.Y.Z` and any existing PR; restore the branch from the release commit if missing, apply the next patch development bump if needed, and open or reuse the PR into `main`. Merge after publication is complete. Do not rerun Release Trigger for the existing tag. | | Tag exists, GitHub Release is missing | Confirm that the tag points to the intended release commit, then create the GitHub Release from that existing tag with the same notes format used by `release.yml`. | | Valid tag exists, neither wheel nor sdist is published to PyPI | Run **Publish to PyPI** manually with the exact release tag, independently of GitHub Release creation. | | PyPI version exists, GitHub Release is missing | Keep the existing tag unchanged and create the GitHub Release from it. | -| Wheel is published, sdist is missing | Verify the published wheel and recover the matching sdist from the original run's `dist` artifact. Use an authorized publishing path to upload only the missing sdist; do not replace the wheel. | -| Sdist is published, wheel is missing | Verify the published sdist and recover the matching wheel from the original run's `dist` artifact. Use an authorized publishing path to upload only the missing wheel; do not replace the sdist. | +| Wheel is published, sdist is missing | Prefer retrying the failed `publish` job with the original `dist` artifact (see below). `uv publish` skips the identical existing wheel and uploads the missing sdist. | +| Sdist is published, wheel is missing | Prefer retrying the failed `publish` job with the original `dist` artifact (see below). `uv publish` skips the identical existing sdist and uploads the missing wheel. | + +For partial PyPI uploads, inspect the original run and published files before retrying. If only the `publish` job failed and the original `dist` artifact is still available, rerun that failed job to reuse the successful build. With PyPI, [repeating `uv publish` skips existing identical files](https://docs.astral.sh/uv/guides/package/#publishing-your-package) and uploads missing files; existing files must match exactly. -For partial PyPI uploads, inspect the original run and published files before retrying. The current workflow rebuilds and publishes all of `dist/`; it is not a missing-file-only recovery command. If the original artifact is unavailable, investigate how to reproduce the matching distribution before publishing. Previously used filenames cannot be replaced or reused. +Rerunning the entire workflow also reruns `uv build`. Before retrying publication with rebuilt artifacts, verify that artifacts corresponding to already-published files match those files exactly. The same tag alone does not guarantee identical build output. If they differ, stop and investigate; do not replace published files or move the tag. Previously used distribution filenames cannot be replaced or reused for different contents, even after deletion. ### Release Workflow Didn't Trigger