Skip to content

ci: bump plugin versions from conventional PR titles - #14

Open
z4o4z wants to merge 1 commit into
mainfrom
evgeny/release-automation
Open

z4o4z wants to merge 1 commit into
mainfrom
evgeny/release-automation

Conversation

@z4o4z

@z4o4z z4o4z commented Nov 12, 2026

Copy link
Copy Markdown
Member

What this does

After this merges, every merge to main sets the next plugin version from the pull request title, like the other Voiceflow repositories. feat is a minor release, fix is a patch, and a ! or a BREAKING CHANGE: footer is a major release. A new required check keeps the title, the version files, and the changelog consistent. A new workflow pushes the release commit and tag, then starts the existing draft release.

Why

Today someone edits four manifests, the skill metadata, and the changelog by hand for each release. Nothing ties a pull request to the version change it needs. No release has a tag yet, so 1.0.0 is on main but untagged.

Decisions that need your approval

These change written policy in docs/releasing.md. GitHub settings stay as they are until an admin applies the steps below.

1. One App may bypass the pull request rule.
Change: force-push and deletion rules keep an empty bypass list. The pull request rule and required checks move to a second ruleset whose only bypass is a new release App.
Recommended: approve, so the workflow can push chore(release): X.Y.Z to main.
Alternative: the workflow opens a version pull request that someone merges.
Trade-off: the workflow only pushes version files, but the App token itself could push anything if the workflow were changed. The alternative keeps the bypass list empty but adds one pull request per release.

2. Required checks go from four to five.
Change: Conventional PR title becomes required. The release guard now requires all five.
Recommended: approve. Otherwise a title can skip the check and release the wrong version.
Alternative: keep the check advisory.
Trade-off: open pull requests must update from main once to report it.

Other written changes: pull requests no longer edit versions. They add a customer-facing line under ## Unreleased. Tags exist before publication, and drafts start on their own. "No workflow publishes a release automatically" stays true.

What I am most likely to have got wrong

  • Nothing ran on GitHub. I tested the workflow logic against a local bare repository. The App token, the ruleset bypass, and the dispatch are unverified until the App exists.
  • The first run tags 1.0.0 at b0adc80, the merge of feat!: move the skills to MCP v3 tool names and release 1.0.0 #11 that set 1.0.0, not at this pull request's merge. That stays correct if other pull requests merge before the App is ready. The package content is the same either way.
  • After each feat or fix merge, the bot adds one commit to main. A pull request updated in the minute before that push must update again.

Admin setup (not done by this PR)

Apply in this order.

  1. Create a GitHub App in the Voiceflow organization. Repository permissions: Contents read and write. Metadata read is added on its own. No webhook. Install it on voiceflow/agent-plugins only.
  2. Create the environment release, with deployment branches limited to main. Add the secrets RELEASE_APP_CLIENT_ID (the App's client ID) and RELEASE_APP_PRIVATE_KEY. Do this before merging; otherwise the first run creates an empty, unrestricted environment.
  3. Allow squash merging only in Settings > General, with the default message "Pull request title and description". In the ruleset's pull request rule, set the allowed merge methods to squash only.
  4. Split the branch ruleset. First create a new ruleset with "Restrict deletions" and "Block force pushes" and an empty bypass list. Only then remove those two rules from the existing ruleset. That order leaves no moment without force-push protection. Add the App to the existing ruleset's bypass list, mode "Always allow". It keeps the pull request rule and the strict required checks.
  5. Create two tag rulesets for refs/tags/v*. The first restricts updates and deletions, with an empty bypass list. The second restricts creations, with only the App as bypass. A bypass covers a whole ruleset, so creation needs its own.
  6. Right before merging, add Conventional PR title (source: GitHub Actions) to the required checks. This pull request already reports it. Other open pull requests wait on it until they update from main after the merge.
  7. Squash-merge this pull request. The Version workflow tags v1.0.0 at b0adc80 and starts Prepare draft package release. Review the draft and publish it by hand.

If this merges before steps 1 and 2, the write job fails with "The release environment needs RELEASE_APP_CLIENT_ID and RELEASE_APP_PRIVATE_KEY. Nothing was changed." After setup, run Version by hand from main.

How it works

Workflows and scripts
  • Conventional PR title (.github/workflows/pr-title.yml). Uses pull_request, not pull_request_target. Read-only token, no secrets. It checks out the merge commit like the existing checks, and scripts/check_pr_title.py compares it with its base. The title and body come from the event file, never from shell interpolation. It fails when:
    • the title is not type(scope)!: summary with a known type;
    • the title uses chore(release);
    • files under voiceflow/ change but the title is not feat, fix, or breaking;
    • a version field changes;
    • a feat, fix, or breaking change adds no line under ## Unreleased;
    • a breaking change adds no line with "migrate" or "migration";
    • the body has a BREAKING CHANGE: footer but the title has no !.
  • Version (.github/workflows/version.yml). Runs on push to main or by hand from main, one run at a time.
    • The compute job is read-only. scripts/next_version.py reads first-parent commits since the last v* tag, skips chore(release) commits, and takes the largest bump. If nothing bumps, the run stops. Otherwise it rewrites the version files, renames ## Unreleased to a bare ## X.Y.Z, adds a new empty ## Unreleased, runs validate.py, and uploads a patch.
    • The write job runs in the release environment and runs no repository code. It mints a token with the SHA-pinned actions/create-github-app-token v3.3.0, limited to contents write. It refuses a patch that touches any other file, or adds, deletes, renames, or changes the mode of a file. It pushes the commit and tag in one atomic push. If main moved, it pushes nothing and exits cleanly. Then it dispatches release.yml with the job token, which has actions: write.
  • Draft release. release.yml and scripts/verify_release_source.py now require tag vX.Y.Z at the release commit, following annotated tags. They still never create, move, or delete a tag, never replace a release or draft, and never publish.
  • One version list. VERSION_FILES in scripts/versioning.py lists the four manifests and the skill documentation metadata. validate.py and the bump script both use it. The write job repeats the list, and a test keeps them equal. MCP Registry server.json versions are not included. validate.py also requires the changelog to start with ## Unreleased, then ## <manifest version>.
  • Other changes. Dependabot titles now start with ci:, so its pull requests pass the check. docs/compatibility.md no longer names a current candidate version. CONTRIBUTING.md and docs/releasing.md describe the new flow.

Verification

  • python3 scripts/validate.py: passes.
  • python3 -m unittest discover -s tests -v: 141 tests pass, including the Gitleaks scanner tests.
  • python3 scripts/build.py: passes.
  • python3 scripts/scan_secrets.py with Gitleaks 8.30.1: 0 findings in history, files, and packages.
  • actionlint 1.7.12 with shellcheck 0.11.0 on every workflow: no findings.
  • A local dry run of version.yml against a bare clone. It used the write job's shell steps taken from the YAML, with a stub for the bot user lookup.
    • First run: tagged v1.0.0 at b0adc80.
    • After a feat merge: pushed chore(release): 1.1.0 and v1.1.0 together. prepare_release.py at the tag produced the right notes.
    • Next run: nothing to release.
    • main moved before the push: nothing pushed, clean exit. The next run released 1.1.1 for both merges.
    • A patch touching README.md, or changing a file mode: refused, nothing pushed.
    • A tag target that is not on main: refused.
  • The title check on a simulated depth-2 merge commit failed a docs: title that changed voiceflow/, edited a version, and had a footer without !. It passed a ci: title.
  • Not run: anything on GitHub. That includes the App token, the ruleset bypass, the environment, the dispatch, and the race where main moves during the push itself.

🤖 Generated with Claude Code

A new Conventional PR title check ties each pull request title to what it
changes. After merge, the Version workflow reads the titles since the last
v* tag, writes the next version into every version file and the changelog,
then pushes the release commit and tag with a GitHub App token and starts
the existing draft release workflow. No workflow publishes a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The bootstrap recovery run only tags the historical baseline without triggering a follow-up Version run, leaving pending feature and fix commits unreleased until another push or manual run.

1 open issue
What changed in this PR

Automates plugin versioning from conventional pull request titles, replacing manual version edits while keeping release publication manual.

Changes:

  • Adds a required title check covering version changes, changelog entries, and migration notes.
  • Adds version computation and atomic commit/tag pushes, including bootstrap tagging of the existing 1.0.0 release commit.
  • Automatically starts draft preparation and updates release guards, tests, and contributor guidance.
File Description
tests/​test_versioning.py Tests title rules, version planning, rewrites, and changelog handling.
tests/​test_release_source.py Tests the fifth required check and release-tag verification.
scripts/​versioning.py Centralizes version files, title parsing, bump rules, and changelog updates.
scripts/​verify_release_source.py Requires the title check and a matching release tag.
scripts/​validate.py Checks shared version files and changelog heading order.
scripts/​next_version.py Computes release plans and applies version updates.
scripts/​check_pr_title.py Validates titles against package changes and release-note requirements.
docs/​releasing.md Documents automated versioning, protections, and draft-release procedures.
docs/​compatibility.md Points to the changelog for the current version while preserving dated evidence.
CONTRIBUTING.md Explains title conventions and contributor changelog responsibilities.
CHANGELOG.md Adds the Unreleased section and entry guidance.
.github/​workflows/​version.yml Computes updates, pushes release commits and tags, and dispatches drafts.
.github/​workflows/​release.yml Requires an existing matching tag before creating a draft.
.github/​workflows/​pr-title.yml Runs the read-only pull request title check.
.github/​dependabot.yml Gives dependency update titles the conventional ci: prefix.

Review effort: Balanced

fi
echo "Pushed $tag at $commit."
echo "commit=$commit" >> "$GITHUB_OUTPUT"
- name: Start the draft release for the new tag
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.

2 participants