docs + feat: document branch split and automate main's releases with Changesets#57
docs + feat: document branch split and automate main's releases with Changesets#57toiroakr wants to merge 13 commits into
Conversation
Explain in CLAUDE.md that main is now the v2 line (tailor CLI) and maintenance/v1 holds the pre-rename v1 line (tailor-sdk CLI) for patch releases, including how to target it with gh release create --target. Add a short Versioning section to README.md pointing users at the v2/v1 major tags and the maintenance/v1 branch.
maintenance/v1 gets the same branch protection as main, not the direct-push workflow the previous commit described.
Add changesets/action-based release automation for main (the v2 line): a PR that adds a .changeset/*.md file gets picked up by release.yaml, which opens/updates a Version Packages PR bumping package.json and CHANGELOG.md. Once that merges, the same workflow tags the new version and creates a GitHub release from the changelog entry, which update-major-tag.yaml then picks up as usual. - .changeset/config.json + README.md: standard changesets scaffold, single private package (root), no npm publish step. - package.json: add name/version (2.0.0, matching the current v2.0.0 release) so changesets has something to version, @changesets/cli devDependency, and a changeset script. - .github/workflows/release.yaml: runs changesets/action with no publish-script (so it only ever opens/updates the Version PR), then a custom step gated on hasChangesets == 'false' that tags and releases the bumped version, guarded against re-releasing an already-released version on a later unrelated push to main. - CLAUDE.md: document the new automated flow for main; maintenance/vN branches keep the manual gh release create --target flow.
…ag object SHA v1.9.0 is an annotated tag; the previous SHA was the tag object's own SHA rather than the commit it points to, which zizmor's hash-pin version-comment check correctly flagged.
pnpm/action-setup needs a version specified (via input, packageManager field, or devEngines.packageManager) — it has none of those otherwise and would fail at runtime with 'No pnpm version is specified.' (found via the maintenance/v1 copy of this workflow actually running).
| # nothing else — no tag, no release. That happens below, once that PR merges. | ||
| - name: Create Release Pull Request | ||
| id: changesets | ||
| uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 |
There was a problem hiding this comment.
The action defaults to commitMode: git-cli, which produces an unsigned Version Packages commit, so main's required-signatures ruleset will block this PR from being merged.
There was a problem hiding this comment.
Update (250f999) — the commit-mode key added in fae4362 was actually a no-op the whole time: the action pinned here (v1.9.0) uses camelCase inputs, so the correct key is commitMode, not commit-mode (its own main branch docs use the newer kebab-case naming, which is where I got it from — GitHub Actions silently ignores a with: key an action doesn't declare, so it never errored). Fixed to commitMode: github-api in 250f999, so signing is now actually in effect.
| - name: Tag and create GitHub release | ||
| if: steps.changesets.outputs.hasChangesets == 'false' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} |
There was a problem hiding this comment.
This release is created with GITHUB_TOKEN, so its published event does not start update-major-tag.yaml and the major tag remains on the prior release.
There was a problem hiding this comment.
Fixed in fae4362 — release.yaml now updates the major tag itself right after creating the release, instead of relying on update-major-tag.yaml's release: published trigger. Shared logic extracted into .github/actions/update-major-tag, used by both workflows.
| ' CHANGELOG.md) | ||
|
|
||
| if [ -n "$(echo "$NOTES" | tr -d '[:space:]')" ]; then | ||
| gh release create "$TAG" --title "$TAG" --notes "$NOTES" |
There was a problem hiding this comment.
If another main commit lands during this job, gh release create creates the missing tag from the newer default-branch tip rather than this run's version commit, so the release can contain code not represented by its version or changelog.
There was a problem hiding this comment.
Update (250f999) — no longer a hand-rolled gh release create --target. changeset publish now creates the git tag directly, and changesets/action's own pushTag/createRelease push it and create the release, always targeting github.context.sha (this run's own commit) — same outcome, using the action's own tested code path instead.
| - main | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} |
There was a problem hiding this comment.
If the Version Packages merge run is pending behind an active run and a later changeset push arrives, the default single-slot concurrency queue cancels the release run while its replacement skips publishing, so that version is never released.
There was a problem hiding this comment.
Update (250f999) — settled on a single release job with no concurrency: group at all, rather than the two-job split mentioned above. GitHub's concurrency queue has no way to keep more than one pending run per group (it always cancels older pending runs in favor of newer ones), so this job is never silently skipped regardless of how close together pushes to main land. The (rare) alternative risk — two truly concurrent runs racing to push to the same Version Packages PR branch — is self-evident (one push fails loudly) and self-corrects on the next push, unlike a silently skipped release.
| `main` is the current major line (v2 — actions target the Tailor Platform SDK's | ||
| `tailor` CLI). `maintenance/v1` holds the pre-v2 line (actions target the removed | ||
| `tailor-sdk` CLI) so the v1.x.y release series can still receive patches. It predates | ||
| the `v1` -> `v2` rename and is not merged into `main`. `maintenance/v1` is protected |
There was a problem hiding this comment.
The current rules API reports no applicable rules for maintenance/v1; is branch protection being configured separately before this statement lands?
There was a problem hiding this comment.
You're right — the rules API doesn't return anything for maintenance/v1 yet. Branch protection there is being set up separately and isn't live yet; the doc's description is intentionally forward-looking for now. No change made here.
|
|
||
| This repository uses GitHub Releases with automatic major version tag updates. | ||
| Releases on `main` (the v2 line) are automated with [Changesets](https://github.com/changesets/changesets). | ||
| `maintenance/v1` and any future `maintenance/vN` branch still release manually — see |
There was a problem hiding this comment.
maintenance/v1 now contains its own Changesets workflow and documents automated releases, so this line still directs maintainers to a manual procedure that no longer applies.
There was a problem hiding this comment.
You're right — maintenance/v1 already has its own automated Changesets release.yaml. Reworded this PR's CLAUDE.md to reflect that instead of describing it as manual (fae4362). Planning to sync these fixes over to maintenance/v1 as well.
… major-tag update self-hosted - Use changesets/action's commit-mode: github-api so the Version Packages PR's commits are GPG-signed; the default git-cli mode produces unsigned commits that main's required-signatures ruleset blocks from being merged. - Pass --target to gh release create so the release/tag point at the commit this job actually built, not whatever is at the tip of main if another push lands during the same run. - Backfill a release/tag for every un-released CHANGELOG.md version instead of only the current package.json version, so a run that gets cancelled by main's single-pending-slot concurrency queue doesn't leave that version permanently un-tagged. - Move the major-tag update into release.yaml itself, since a release created with GITHUB_TOKEN doesn't fire update-major-tag.yaml's release: published trigger. Extract the shared logic into a local composite action (.github/actions/update-major-tag) so update-major-tag.yaml (kept as a fallback for manually created releases) and release.yaml don't duplicate it. - Correct CLAUDE.md: maintenance/v1 already has its own automated Changesets release.yaml, not a manual process.
- node-version-file: package.json can't resolve a version since package.json has no engines.node; use an explicit node-version like every other workflow. - Bump the pnpm/action-setup pin from 11.14.0 to 11.15.0 to match every other workflow in the repo (it was a stray value from when the version input was first added, not an intentional divergence).
- A failure in gh release create for one version (e.g. a transient API error) no longer aborts the whole step: it's recorded and the loop keeps backfilling the remaining versions, so a rerun only needs to retry what actually failed. - Only advance the major version tag to a release that's confirmed to exist (pre-existing or just created successfully) rather than to a commit that merely resolved but whose release creation failed.
…its own release, not a re-guess Re-running `git log -S"## $VERSION"` for a version that already has a release risks drifting from whatever commit that release actually targets (e.g. if a past run resolved a different commit for some reason). Read the existing release's own targetCommitish instead, so the major tag can only ever be pointed at a commit some release genuinely targets. The git log -S guess is now only ever used the one time a version's release doesn't exist yet.
…HA, not a search The commit that introduced the current version's CHANGELOG.md section is, by definition, this run's own commit (that's exactly why hasChangesets is false) — use $GITHUB_SHA directly instead of `git log -S`. This removes the one failure mode that mattered most: a resolution failure on the current version would otherwise skip its release and block the major tag update entirely, and unlike a transient gh release create error it wouldn't be fixed by a plain rerun (git log -S returns the same result against the same commit and CHANGELOG.md content every time). git log -S is now only used for backfilling older versions, where a resolution failure just means that one older version stays un-tagged this run without affecting the current release.
… own custom-publishing shape Only tag/release the current package.json version, matching changesets/action's documented "Custom Publishing" pattern (a single hasChangesets == 'false' step) and how changesets/action's own native pushTag always targets context.sha — it never backfills either. If main's concurrency queue cancels a run before it gets here, that version's release is simply skipped; the next Version Packages merge tags/releases normally at its own version. Traded the backfill guarantee for much simpler code, since changesets/action itself offers no built-in way to backfill anyway. Keeps the fixes that don't depend on backfilling: signed commits (commit-mode: github-api), pinning --target to $GITHUB_SHA, and updating the major tag from release.yaml itself via the update-major-tag composite action.
… tagging is never cancelled GitHub Actions concurrency groups have no configurable way to keep more than one pending run per group — an older queued run is always cancelled in favor of a newer one, and there's no way to opt out of that while still sharing a group. Move the tag/release/major-tag-update logic into its own tag-release job with no concurrency group of its own (instead of sharing one with the changesets job), so it always runs to completion for every push to main regardless of how close together pushes land. It checks .changeset/*.md directly rather than depending on the changesets job's hasChangesets output, so it doesn't need that job to succeed (or even run) first. The changesets job keeps its concurrency group, since concurrent runs pushing to the same Version Packages PR branch could otherwise race — but that's harmless to skip, since a later run recomputes the PR from the same accumulated changesets anyway.
…se mechanism Replace the hand-rolled gh release create step (and the earlier tag-release job split) with the tested, first-party path: set .changeset/config.json's privatePackages.tag: true so changeset publish (this package is private and has no npm registry to publish to) still creates a git tag for the version without attempting an actual npm publish, and pass that as the publish command to changesets/action, which detects the tag and creates the GitHub release itself, sourcing notes from CHANGELOG.md via its own well-tested parser instead of a hand-rolled awk extraction. Also fixes a real bug this uncovered: the changesets/action inputs/outputs used so far (commit-mode, publish-script, published-packages) were kebab-case, matching that action's main branch docs, but the commit actually pinned here (v1.9.0) uses camelCase (commitMode, publish, publishedPackages). GitHub Actions silently ignores a with: key an action doesn't declare, so commit-mode github-api has been a no-op since it was added earlier in this branch's history: commits and the release tag were never actually being signed. Fixed to commitMode, matching the pinned commit's actual action.yml. No concurrency group on this job (same reasoning as before): GitHub's concurrency queue keeps only the latest pending run per group and cancels older ones, which would silently skip a version's release if pushes to main land close together.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (2)
.github/workflows/update-major-tag.yaml:23
refis being passed as the release tag name, but this workflow checks out the default branch without ensuring the release tag exists locally. If the tag object isn’t present in the checkout,git tag -f "$MAJOR" "$REF"in the composite action can fail to resolve the ref. Consider checking out the release tag (or explicitly fetching tags) so the ref is guaranteed to exist.
- uses: ./.github/actions/update-major-tag
with:
release-tag: ${{ github.event.release.tag_name }}
ref: ${{ github.event.release.tag_name }}
github-token: ${{ github.token }}
.github/actions/update-major-tag/action.yml:2
- The composite action’s top-level description says it points the major tag at the “given release tag”, but the implementation actually points it at
inputs.ref(the release tag is only used for validation/deriving the major prefix). This mismatch can confuse callers (and the new release workflow passes a commit SHA asref).
description: Force-updates the major version tag (e.g. v2) to point at the given release tag.
Summary
Documents the
main/maintenance/v1branch split, and automatesmain's releaseswith Changesets (the
pnpmrecommendation for this), minus the npm publishstep this repo doesn't need.
Branch split docs
CLAUDE.md:mainis now the v2 line (tailorCLI);maintenance/v1holds thepre-rename v1 line (
tailor-sdkCLI) forv1.x.ypatches, protected the same way asmain.maintenance/v1is likewise automated with its own Changesetsrelease.yaml(not a manual process, as an earlier revision of this PR described).
README.md: short "Versioning" section pointing at the@v2/@v1major tags andthe
maintenance/v1branch.Changesets release automation (main)
.changeset/*.mdfile to a PR (viapnpm changeset) describes the changeand its bump type.
release.yamlopens/updates a "Version Packages" PR from accumulated changesets viachangesets/action, bumpingpackage.jsonandCHANGELOG.md. Its commits usecommitMode: github-apiso they're GPG-signed — the defaultgit-climode producesunsigned commits that main's required-signatures ruleset would otherwise block from
merging.
changeset publishas thepublishcommand.This package is
"private": true, and.changeset/config.json'sprivatePackages.tag: truetellschangeset publishto skip an actual npm publishbut still create the git tag for the new version.
changesets/actiondetects thatand creates the GitHub release itself — the same tested path it uses for real npm
publishes, rather than a hand-rolled
gh release create.concurrency:group: GitHub's concurrency queue keepsonly the latest pending run per group and cancels older ones, which would silently
skip a version's release if pushes to
mainland close together. The alternative(rare) risk — two truly concurrent runs racing to push to the same Version Packages
PR branch — is self-evident (one push fails loudly) and self-corrects on the next
push.
v2), since a release created withthe workflow's own
GITHUB_TOKENdoesn't fireupdate-major-tag.yaml'srelease: publishedtrigger. The shared tag-update logic lives in a local compositeaction (
.github/actions/update-major-tag), used by bothrelease.yamlandupdate-major-tag.yaml(kept as a fallback for manually created releases).