Skip to content

docs + feat: document branch split and automate main's releases with Changesets#57

Open
toiroakr wants to merge 13 commits into
mainfrom
feat/changesets-release
Open

docs + feat: document branch split and automate main's releases with Changesets#57
toiroakr wants to merge 13 commits into
mainfrom
feat/changesets-release

Conversation

@toiroakr

@toiroakr toiroakr commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Documents the main/maintenance/v1 branch split, and automates main's releases
with Changesets (the
pnpm recommendation for this), minus the npm publish
step this repo doesn't need.

Branch split docs

  • CLAUDE.md: main is now the v2 line (tailor CLI); maintenance/v1 holds the
    pre-rename v1 line (tailor-sdk CLI) for v1.x.y patches, protected the same way as
    main. maintenance/v1 is likewise automated with its own Changesets release.yaml
    (not a manual process, as an earlier revision of this PR described).
  • README.md: short "Versioning" section pointing at the @v2/@v1 major tags and
    the maintenance/v1 branch.

Changesets release automation (main)

  • Adding a .changeset/*.md file to a PR (via pnpm changeset) describes the change
    and its bump type.
  • release.yaml opens/updates a "Version Packages" PR from accumulated changesets via
    changesets/action, bumping package.json and CHANGELOG.md. Its commits use
    commitMode: github-api so they're GPG-signed — the default git-cli mode produces
    unsigned commits that main's required-signatures ruleset would otherwise block from
    merging.
  • Once that PR merges, the same job runs changeset publish as the publish command.
    This package is "private": true, and .changeset/config.json's
    privatePackages.tag: true tells changeset publish to skip an actual npm publish
    but still create the git tag for the new version. changesets/action detects that
    and creates the GitHub release itself — the same tested path it uses for real npm
    publishes, rather than a hand-rolled gh release create.
  • The job deliberately has no concurrency: group: 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. 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.
  • The same job then updates the major version tag (v2), since a release created with
    the workflow's own GITHUB_TOKEN doesn't fire update-major-tag.yaml's
    release: published trigger. The shared tag-update logic lives in a local composite
    action (.github/actions/update-major-tag), used by both release.yaml and
    update-major-tag.yaml (kept as a fallback for manually created releases).

toiroakr added 4 commits July 22, 2026 21:17
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.
@toiroakr
toiroakr changed the base branch from docs/branch-split-notes to main July 22, 2026 12:56
@toiroakr toiroakr changed the title feat: automate main's releases with Changesets docs + feat: document branch split and automate main's releases with Changesets Jul 22, 2026
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).
@toiroakr
toiroakr requested a review from dqn July 23, 2026 01:04
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@toiroakr toiroakr Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/release.yaml Outdated
- name: Tag and create GitHub release
if: steps.changesets.outputs.hasChangesets == 'false'
env:
GH_TOKEN: ${{ github.token }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/release.yaml Outdated
' CHANGELOG.md)

if [ -n "$(echo "$NOTES" | tr -d '[:space:]')" ]; then
gh release create "$TAG" --title "$TAG" --notes "$NOTES"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@toiroakr toiroakr Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/release.yaml Outdated
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@toiroakr toiroakr Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread CLAUDE.md
`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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The current rules API reports no applicable rules for maintenance/v1; is branch protection being configured separately before this statement lands?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread CLAUDE.md Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

This comment was marked as outdated.

… 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.

This comment was marked as outdated.

- 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).

This comment was marked as outdated.

- 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.

This comment was marked as outdated.

…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.

This comment was marked as outdated.

…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.

This comment was marked as outdated.

… 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.

This comment was marked as outdated.

… 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.

This comment was marked as outdated.

…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.

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.

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

  • ref is 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 as ref).
description: Force-updates the major version tag (e.g. v2) to point at the given release tag.

@toiroakr toiroakr assigned dqn and unassigned dqn Jul 24, 2026

@dqn dqn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

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.

3 participants