Skip to content

Comments

fix: integrate reusable workflow into release proposal pipeline#1265

Open
RonnyPfannschmidt wants to merge 3 commits intopypa:developfrom
RonnyPfannschmidt:fix/integrate-reusable-release-workflow
Open

fix: integrate reusable workflow into release proposal pipeline#1265
RonnyPfannschmidt wants to merge 3 commits intopypa:developfrom
RonnyPfannschmidt:fix/integrate-reusable-release-workflow

Conversation

@RonnyPfannschmidt
Copy link
Contributor

Summary

  • Rewrites the release-proposal workflow to call the reusable-towncrier-release workflow for each project in parallel, instead of relying on a monolithic Python script
  • Fixes the broken CLI invocation in the reusable workflow (used nonexistent --version-scheme/--local-scheme flags)
  • Makes fragment checking a soft output (has_fragments=false) rather than a hard failure, so monorepo callers can handle projects with no fragments gracefully

Flow after this change

push to develop/main
  ├── setuptools-scm (reusable workflow) ─┐
  │   ├── determine-version               │  parallel
  │   └── build-changelog → artifact      │
  ├── vcs-versioning (reusable workflow) ──┘
  │   ├── determine-version
  │   └── build-changelog → artifact
  └── create-release-pr
      ├── downloads changelog artifacts
      ├── commits to release/<branch>
      └── creates/updates PR with labels

On PRs, a lightweight validate job shows planned releases in the step summary.

The create_release_proposal.py script is unchanged and continues to work for local development.

Test plan

  • CI passes on this PR (the reusable workflow runs as part of release-proposal)
  • Validate step summary shows correct version info on this PR
  • After merge: verify release-proposal workflow creates/updates the release PR on develop push

Made with Cursor

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>
Copilot AI review requested due to automatic review settings February 20, 2026 14:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -c config file flag instead of nonexistent --version-scheme/--local-scheme flags
  • 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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +59 to +62
if: |
github.event_name == 'push' &&
(needs.setuptools-scm.outputs.has_fragments == 'true' ||
needs.vcs-versioning.outputs.has_fragments == 'true')
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +159
const prTitle = `Release: ${releases}`;
const prBody = [
'## Release Proposal',
'',
'This PR prepares the following releases:',
releases,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +132 to 138
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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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'

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 22
- ✅ 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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +125
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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +125
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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
# 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 "")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
--strip-dev 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "")
--strip-dev 2>/dev/null | head -n1 | tr -d '[:space:]')

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +80
- 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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 138 to 139
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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
--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 \
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
-c ${{ inputs.project_directory }}/pyproject.toml \
-c ${{ inputs.project_directory }}/pyproject.toml \
--local-scheme no-local-version \

Copilot uses AI. Check for mistakes.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant