fix: integrate reusable workflow into release proposal pipeline#1265
fix: integrate reusable workflow into release proposal pipeline#1265RonnyPfannschmidt wants to merge 3 commits intopypa:developfrom
Conversation
The release-proposal workflow now calls the reusable-towncrier-release workflow for each project instead of using a monolithic Python script. This eliminates the broken CLI invocation in the reusable workflow and makes the release pipeline properly composable. Changes: - reusable-towncrier-release.yml: fix CLI to read version scheme from pyproject.toml (-c flag + --strip-dev) instead of nonexistent --version-scheme/--local-scheme flags; soft-fail on missing fragments (output has_fragments=false) instead of hard exit 1 - release-proposal.yml: rewritten to call reusable workflow per project in parallel, download changelog artifacts, and manage the release PR via actions/github-script (no Python/uv needed in PR creation job) - README.md: document pyproject.toml version scheme prerequisite Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude <claude@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the release-proposal workflow to use reusable workflows for each project in parallel, replacing a monolithic Python script. The main changes fix a broken CLI invocation that used nonexistent flags and introduce graceful handling of projects without changelog fragments.
Changes:
- Rewrote release-proposal workflow to call reusable-towncrier-release workflow for each project in parallel
- Fixed CLI invocation to use
-cconfig file flag instead of nonexistent--version-scheme/--local-schemeflags - Changed fragment checking from hard failure to soft output (
has_fragments=false) for graceful handling
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| .github/workflows/reusable-towncrier-release.yml | Fixed CLI invocation and made fragment checking return soft output instead of failing |
| .github/workflows/release-proposal.yml | Completely restructured to use parallel reusable workflows and handle PR creation/updates |
| .github/workflows/README.md | Updated documentation to reflect new CLI-based configuration reading approach |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Ensure release labels are added even when updating an existing PR, so newly added project fragments get their corresponding label. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude <claude@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if: | | ||
| github.event_name == 'push' && | ||
| (needs.setuptools-scm.outputs.has_fragments == 'true' || | ||
| needs.vcs-versioning.outputs.has_fragments == 'true') |
There was a problem hiding this comment.
The job condition uses a multi-line YAML syntax that checks if either project has fragments. However, if the condition is false (neither project has fragments), the workflow will show the setuptools-scm and vcs-versioning jobs as successful (they both return has_fragments=false), but the create-release-pr job will be skipped.
This is correct behavior, but might be confusing in the workflow UI. Consider adding a summary step that explicitly states "No releases needed" when both projects have no fragments, to make the workflow outcome clearer to users.
| const prTitle = `Release: ${releases}`; | ||
| const prBody = [ | ||
| '## Release Proposal', | ||
| '', | ||
| 'This PR prepares the following releases:', | ||
| releases, |
There was a problem hiding this comment.
The shell script building the PR body uses an array join pattern with join('\n'), but the releases variable is just a string that might be empty. If both SCM_FRAGMENTS and VCS_FRAGMENTS are false (which shouldn't happen due to the job condition, but could in edge cases), the PR would be created with "Release: " as the title with no project names.
While the job condition on lines 59-62 should prevent this, consider adding an additional safety check or ensuring the metadata step outputs a meaningful default.
| const prTitle = `Release: ${releases}`; | |
| const prBody = [ | |
| '## Release Proposal', | |
| '', | |
| 'This PR prepares the following releases:', | |
| releases, | |
| const safeReleases = (releases && releases.trim().length > 0) | |
| ? releases | |
| : 'Unknown releases'; | |
| const prTitle = `Release: ${safeReleases}`; | |
| const prBody = [ | |
| '## Release Proposal', | |
| '', | |
| 'This PR prepares the following releases:', | |
| safeReleases, |
| run: | | ||
| git checkout -B "${{ steps.meta.outputs.release_branch }}" | ||
| git add -A | ||
| git commit -m "Prepare release: ${{ steps.meta.outputs.releases }}" || echo "No changes to commit" | ||
| git push origin "${{ steps.meta.outputs.release_branch }}" --force | ||
|
|
||
| - name: Create or update PR |
There was a problem hiding this comment.
The git commit command uses || echo "No changes to commit" to handle the case where there are no changes. However, this silently continues even when no changes were made, which could lead to pushing an empty commit or pushing a branch that hasn't actually changed.
While this behavior may be intentional (to allow the workflow to succeed even without changes), consider adding logic to detect whether changes were actually committed and skip the PR creation/update if no changes were made. This would prevent unnecessary PR updates when there are no actual changelog changes.
| run: | | |
| git checkout -B "${{ steps.meta.outputs.release_branch }}" | |
| git add -A | |
| git commit -m "Prepare release: ${{ steps.meta.outputs.releases }}" || echo "No changes to commit" | |
| git push origin "${{ steps.meta.outputs.release_branch }}" --force | |
| - name: Create or update PR | |
| id: release_branch_push | |
| run: | | |
| git checkout -B "${{ steps.meta.outputs.release_branch }}" | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit" | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| git commit -m "Prepare release: ${{ steps.meta.outputs.releases }}" | |
| git push origin "${{ steps.meta.outputs.release_branch }}" --force | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create or update PR | |
| if: steps.release_branch_push.outputs.has_changes == 'true' |
| - ✅ Strict validation - workflow fails if changelog fragments or version data is missing | ||
| - ✅ No fallback values - ensures data integrity for releases | ||
| - ✅ Clear error messages to guide troubleshooting |
There was a problem hiding this comment.
The documentation states "✅ Strict validation - workflow fails if changelog fragments or version data is missing" in line 20, but this is now outdated. After the changes in this PR, the reusable workflow now returns has_fragments=false as a soft output rather than failing when fragments or the changelog.d directory are missing.
Update the documentation to reflect the new behavior where missing fragments result in has_fragments=false rather than a workflow failure.
| RELEASES="" | ||
| LABELS="" | ||
|
|
||
| # Checkout release branch (force) | ||
| git checkout -B "$RELEASE_BRANCH" | ||
| if [ "$SCM_FRAGMENTS" == "true" ]; then | ||
| RELEASES="setuptools-scm v${SCM_VERSION}" | ||
| LABELS="release:setuptools-scm" | ||
| fi | ||
|
|
||
| # Commit towncrier changes | ||
| git add -A | ||
| git commit -m "Prepare release: $RELEASES" || echo "No changes to commit" | ||
| if [ "$VCS_FRAGMENTS" == "true" ]; then | ||
| VCS="vcs-versioning v${VCS_VERSION}" | ||
| if [ -n "$RELEASES" ]; then | ||
| RELEASES="${RELEASES}, ${VCS}" | ||
| LABELS="${LABELS},release:vcs-versioning" | ||
| else | ||
| RELEASES="$VCS" | ||
| LABELS="release:vcs-versioning" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
In the "Prepare release metadata" step, the LABELS variable is built by concatenating with commas. However, when both projects have fragments, a trailing comma is not added after "release:setuptools-scm" before appending "release:vcs-versioning" on line 120.
This is actually correct (no trailing comma needed), but the inconsistent pattern of checking if [ -n "$RELEASES" ] and then appending with a comma separator could be error-prone if this logic is extended to more projects.
Consider using a more robust approach like building an array and joining with commas, or at least add a comment explaining the comma concatenation pattern.
| if [ "$SCM_FRAGMENTS" == "true" ]; then | ||
| echo "- **setuptools-scm**: v${SCM_VERSION}" | ||
| else | ||
| echo "- **setuptools-scm**: no fragments" | ||
| fi | ||
| if [ "$VCS_FRAGMENTS" == "true" ]; then | ||
| echo "- **vcs-versioning**: v${VCS_VERSION}" | ||
| else | ||
| echo "- **vcs-versioning**: no fragments" | ||
| fi | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| # On push: download built changelogs, commit to release branch, create/update PR | ||
| create-release-pr: | ||
| needs: [setuptools-scm, vcs-versioning] | ||
| if: | | ||
| github.event_name == 'push' && | ||
| (needs.setuptools-scm.outputs.has_fragments == 'true' || | ||
| needs.vcs-versioning.outputs.has_fragments == 'true') | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup Python | ||
| uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: '3.11' | ||
| # Remove directories that will be replaced by towncrier artifacts | ||
| - name: Prepare for artifact overlay | ||
| env: | ||
| SCM_FRAGMENTS: ${{ needs.setuptools-scm.outputs.has_fragments }} | ||
| VCS_FRAGMENTS: ${{ needs.vcs-versioning.outputs.has_fragments }} | ||
| run: | | ||
| if [ "$SCM_FRAGMENTS" == "true" ]; then | ||
| rm -rf setuptools-scm/changelog.d setuptools-scm/CHANGELOG.md | ||
| fi | ||
| if [ "$VCS_FRAGMENTS" == "true" ]; then | ||
| rm -rf vcs-versioning/changelog.d vcs-versioning/CHANGELOG.md | ||
| fi | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v7 | ||
| - name: Download setuptools-scm changelog | ||
| if: needs.setuptools-scm.outputs.has_fragments == 'true' | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: changelog-setuptools-scm | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| uv sync --all-packages --all-groups | ||
| - name: Download vcs-versioning changelog | ||
| if: needs.vcs-versioning.outputs.has_fragments == 'true' | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: changelog-vcs-versioning | ||
|
|
||
| - name: Configure git | ||
| if: github.event_name == 'push' | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Run release proposal | ||
| id: release | ||
| run: | | ||
| uv run create-release-proposal \ | ||
| --event "${{ github.event_name }}" \ | ||
| --branch "${{ github.ref_name }}" | ||
| - name: Prepare release metadata | ||
| id: meta | ||
| env: | ||
| GITHUB_TOKEN: ${{ github.token }} | ||
|
|
||
| - name: Create or update release branch | ||
| if: github.event_name == 'push' | ||
| SCM_FRAGMENTS: ${{ needs.setuptools-scm.outputs.has_fragments }} | ||
| SCM_VERSION: ${{ needs.setuptools-scm.outputs.version }} | ||
| VCS_FRAGMENTS: ${{ needs.vcs-versioning.outputs.has_fragments }} | ||
| VCS_VERSION: ${{ needs.vcs-versioning.outputs.version }} | ||
| BRANCH: ${{ github.ref_name }} | ||
| run: | | ||
| # Get release branch from script output | ||
| RELEASE_BRANCH="${{ steps.release.outputs.release_branch }}" | ||
| RELEASES="${{ steps.release.outputs.releases }}" | ||
| RELEASES="" | ||
| LABELS="" | ||
|
|
||
| # Checkout release branch (force) | ||
| git checkout -B "$RELEASE_BRANCH" | ||
| if [ "$SCM_FRAGMENTS" == "true" ]; then | ||
| RELEASES="setuptools-scm v${SCM_VERSION}" | ||
| LABELS="release:setuptools-scm" | ||
| fi | ||
|
|
||
| # Commit towncrier changes | ||
| git add -A | ||
| git commit -m "Prepare release: $RELEASES" || echo "No changes to commit" | ||
| if [ "$VCS_FRAGMENTS" == "true" ]; then | ||
| VCS="vcs-versioning v${VCS_VERSION}" | ||
| if [ -n "$RELEASES" ]; then | ||
| RELEASES="${RELEASES}, ${VCS}" | ||
| LABELS="${LABELS},release:vcs-versioning" | ||
| else | ||
| RELEASES="$VCS" | ||
| LABELS="release:vcs-versioning" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
The shell script uses string comparison [ "$SCM_FRAGMENTS" == "true" ] with == instead of =. While == works in bash and is more readable, the POSIX-compliant operator is =.
Since GitHub Actions uses bash, this is not a bug, but using = would be more portable if the scripts are ever reused elsewhere. Consider using = for consistency with POSIX standards.
| # Version scheme is read from the project's pyproject.toml config | ||
| NEXT_VERSION=$(uv run python -m vcs_versioning \ | ||
| -c ${{ inputs.project_directory }}/pyproject.toml \ | ||
| --strip-dev 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "") |
There was a problem hiding this comment.
The grep pattern ^\d+\.\d+\.\d+ may not match all valid semantic version formats. If the version scheme returns versions with pre-release identifiers (e.g., "1.0.0-alpha") or other valid formats, the grep will fail and NEXT_VERSION will be empty, causing the workflow to fail.
While the current setup with --strip-dev should produce clean versions, consider using a more robust extraction method or verifying that the version scheme always produces the expected format. Alternatively, consider removing the grep filter entirely and using the full output from vcs_versioning, as long as the version scheme is configured correctly.
| --strip-dev 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "") | |
| --strip-dev 2>/dev/null | head -n1 | tr -d '[:space:]') |
| - name: Prepare for artifact overlay | ||
| env: | ||
| SCM_FRAGMENTS: ${{ needs.setuptools-scm.outputs.has_fragments }} | ||
| VCS_FRAGMENTS: ${{ needs.vcs-versioning.outputs.has_fragments }} | ||
| run: | | ||
| if [ "$SCM_FRAGMENTS" == "true" ]; then | ||
| rm -rf setuptools-scm/changelog.d setuptools-scm/CHANGELOG.md | ||
| fi | ||
| if [ "$VCS_FRAGMENTS" == "true" ]; then | ||
| rm -rf vcs-versioning/changelog.d vcs-versioning/CHANGELOG.md | ||
| fi |
There was a problem hiding this comment.
The preparation step removes setuptools-scm/CHANGELOG.md and vcs-versioning/CHANGELOG.md files before downloading artifacts. However, this removal happens even if the artifact download fails or is skipped.
Consider moving these removal commands into the respective download steps using a multi-line run command, or ensure that the removal only happens when artifacts will definitely be downloaded. This would make the workflow more resilient to failures.
| 6. **Fail fast** - No fallback values; workflows fail explicitly if required data is missing | ||
| 7. **No custom scripts** - Uses PR title parsing and built-in tools only |
There was a problem hiding this comment.
The architecture documentation states "Fail fast" and "No fallback values; workflows fail explicitly if required data is missing" in lines 138-139, but this contradicts the changes made in this PR. The reusable workflow now uses soft failures with has_fragments=false output instead of hard failures when fragments are missing.
Update this section to reflect the new behavior where the workflow gracefully handles missing fragments rather than failing fast.
| --local-scheme no-local-version 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "") | ||
| # Version scheme is read from the project's pyproject.toml config | ||
| NEXT_VERSION=$(uv run python -m vcs_versioning \ | ||
| -c ${{ inputs.project_directory }}/pyproject.toml \ |
There was a problem hiding this comment.
The --strip-dev flag only removes the .devN suffix from the version string (by partitioning on ".dev"), but doesn't remove local version identifiers (the + part). For example, a version like 1.0.0.dev5+g1234567 would become 1.0.0 (correct), but a version like 1.0.0+g1234567 would remain 1.0.0+g1234567.
If the version scheme returns versions with local identifiers without dev suffixes, the grep pattern ^\d+\.\d+\.\d+ will still extract just the public version part (e.g., "1.0.0" from "1.0.0+g1234567"), but this is fragile and depends on the grep working as expected.
Consider using the local_scheme="no-local-version" configuration override via CLI if that option exists, or document this assumption about the version format.
| -c ${{ inputs.project_directory }}/pyproject.toml \ | |
| -c ${{ inputs.project_directory }}/pyproject.toml \ | |
| --local-scheme no-local-version \ |
Tags created by GITHUB_TOKEN don't trigger push-based workflows due to GitHub Actions security restrictions. Work around this by having create-release-tags.yml explicitly dispatch python-tests.yml via workflow_dispatch for each created tag. workflow_dispatch events dispatched by GITHUB_TOKEN DO trigger new workflow runs. Changes: - Add workflow_dispatch trigger to python-tests.yml - Update upload conditions to use github.ref (works for both push and workflow_dispatch with tag refs) - Add dispatch loop in create-release-tags.yml after tag creation - Add actions:write permission for workflow dispatch Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude <claude@anthropic.com>
Summary
--version-scheme/--local-schemeflags)has_fragments=false) rather than a hard failure, so monorepo callers can handle projects with no fragments gracefullyFlow after this change
On PRs, a lightweight
validatejob shows planned releases in the step summary.The
create_release_proposal.pyscript is unchanged and continues to work for local development.Test plan
Made with Cursor