Skip to content

Publish preview releases with changesets prerelease mode - #3888

Draft
graygilmore wants to merge 3 commits into
previewfrom
gg-preview-release-workflow
Draft

Publish preview releases with changesets prerelease mode#3888
graygilmore wants to merge 3 commits into
previewfrom
gg-preview-release-workflow

Conversation

@graygilmore

@graygilmore graygilmore commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Pushes to preview now run a changesets release flow that publishes @shopify/hydrogen@2026.10.0-preview.<n> to the preview npm dist-tag.

What and why

Previously, cutting a preview release meant manually dispatching main's release.yml, which checked out this branch and published a one-off 0.0.0-preview-<sha>-<timestamp> version. No changelog, no version PR, no record of what shipped in each cut.

This PR adds the same changesets flow main uses, adapted for this branch:

  • New release.yml (this branch had no release workflow): on every push to preview, the changesets action either updates a [ci] preview release version PR from pending changesets or, when that PR merges, publishes to npm via OIDC. The file has to keep the release.yml name because npm's Trusted Publishing config allows a single workflow filename, shared across branches.
  • Changesets prerelease mode: the committed .changeset/pre.json makes every version come out as -preview.<n> and makes changeset publish target the preview dist-tag instead of latest.
  • Version pinning: packages/hydrogen starts at 2026.10.0-preview.0. Patch changesets complete the prerelease back to the same 2026.10.0 base and only increment the counter, so cuts publish 2026.10.0-preview.1, .2, and so on — sorting below the eventual real 2026.10.0. CI and the release workflow both run scripts/assert-patch-changesets.ts to reject minor/major changesets, since those would move the base off 2026.10.0.

Reviewer notes

  • The changeset config changes are load-bearing: format: false (changesets v3 auto-detects oxfmt and feeds it changelog markdown, which hard-fails changeset version), @shopify/storefront-e2e in ignore (it's a workspace dependent of hydrogen and would get version churn on every cut), and baseBranch: preview.
  • The release job refuses to run outside prerelease mode. Without .changeset/pre.json, changeset publish would push a plain version to the latest dist-tag and hijack it from the real releases on main.
  • The committed -preview.0 never publishes (versioning increments before each cut), so the first published version is 2026.10.0-preview.1. This is intentional.
  • This can't be tophatted before merge since the workflow only triggers on pushes to preview. The changeset behavior (prerelease numbering, the oxfmt failure, dependent bumps, non-patch rejection) was all verified locally against the v3 CLI this branch uses.
  • There's a pending changeset on this branch, so the first version PR appears as soon as this merges. Merging that version PR publishes for real.

Companion PR that removes the old dispatch job from main: #3889

Comment thread .changeset/config.json
"updateInternalDependencies": "patch",
"ignore": ["@shopify/hydrogen-example-*", "hydrogen"]
"ignore": ["@shopify/hydrogen-example-*", "hydrogen", "@shopify/storefront-e2e"],
"format": false

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.

iirc format: false only avoids formatting changelog markdown, it doesn't format pre.json

let's also add .changeset/pre.json to oxfmt.config.ts's ignorePatterns?

Comment thread scripts/assert-patch-changesets.ts Outdated

for (const line of frontmatter.split(/\r?\n/)) {
const [, name, bump] = line.match(entryPattern) ?? [];
if (name === PREVIEW_PACKAGE && bump !== "patch") {

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.

there are two problems with this guard: minor keeps 2026.10.0-preview.N on the same base; only major moves it to 2027.0.0-preview.N. Also, this regex misses valid YAML forms like quoted values, inline comments, flow mappings, and block scalars. Let's add @changesets/parse as a direct dev dependency, use parseChangesetFile, and reject only release.name === PREVIEW_PACKAGE && release.type === 'major'. Keep the package filter - Phase 4 brings over plain-SemVer siblings where a major can be legitimate.

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.

TIL minor won't touch 2026.10.0!

The script now uses parseChangesetFile from @changesets/parse (added as a direct dev dependency) and only rejects major for @shopify/hydrogen, keeping the package filter.

Comment thread .github/workflows/release.yml Outdated

- name: Test OIDC token
run: |
TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_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.

nit: this reports OIDC working for a 401 JSON response because jq -r '.value' returns the non-empty string null.

dropping this probe altogether and letting publish validate OIDC is fine imo

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.

Dropped the probe. This was copy/pasted from main where I have vague recollections of us having trouble with it in the past but probably not needed now.

uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1.7.0
with:
version: pnpm changeset version
publish: pnpm changeset publish

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.

blocking: (threading) the root release script still runs changeset publish --tag preview, which the pinned CLI rejects while pre.json is in pre mode. The workflow bypasses the script, but it still reads as the supported release entrypoint and throws when used. Let's drop the explicit tag there, or remove the dead script.

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.

Good catch — the pinned CLI throws "Releasing under custom tag is not allowed in pre mode!" on the explicit flag. Dropped --tag preview from the script; pre.json supplies the tag now.

run: node scripts/assert-patch-changesets.ts

- name: Build packages
run: pnpm run build:pkgs

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.

blocking: publication isn't gated on the full CI suite. CI runs as a sibling workflow, and preview currently has empty required-check arrays, so this can publish while CI is red or still running. Let's require the actual CI check before merging the generated version PR, or run the same format, changeset validation, lint, full workspace build, typecheck, and test sequence here.

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.

I added the CI check to the preview branch protection required status checks, so the version PR cannot merge while CI is red or still running. I think that covers this without duplicating the whole suite inside the release job.


on:
push:
branches: [preview]

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.

non-blocking: (threading) this merge creates the version PR; it doesn't publish until that PR merges. Phase 0 can therefore land separately, but it must land before the generated version PR. The React Router template still needs the deploy flags that bypass the released CLI's 0.0.0-preview-* sniff, and the stale skill assertions need updating.

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.

Good call. I'm going to fire up another PR for this.

Edit: actually I'll do it in #3890

Comment thread .changeset/config.json
"updateInternalDependencies": "patch",
"ignore": ["@shopify/hydrogen-example-*", "hydrogen"]
"ignore": ["@shopify/hydrogen-example-*", "hydrogen", "@shopify/storefront-e2e"],
"format": false

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.

non-blocking: the declared 3.1.4 schema doesn't know about format, although the installed 4.0.0-next.6 runtime does and honours this value. Worth updating the schema URL so autocomplete and validation describe the config we're actually using.

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!

{
"name": "@shopify/hydrogen",
"version": "0.0.1",
"version": "2026.10.0-preview.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.

let's port main's Hydrogen changelog before the first version PR merges, otherwise 2026.10.0-preview.1 lands above the branch's unpublished 0.0.1 stub, and we'll need to retrofit the release history afterwards

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.

I would rather do this when it is time for the main release instead of for preview. Retrofitting is a one-file paste at that point, and preview's changelog can stay self-contained until then.

env:
npm_config_registry: https://registry.npmjs.org/
TURBO_TELEMETRY_DISABLED: "1"
outputs:

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.

nit: these outputs aren't consumed by any job in this workflow. Fine to keep if Phase we will use them soon, but for now they do nothing

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.

These are intentional — the upcoming dist-preview work hooks a compile job onto this one with needs: preview-release and gates on outputs.published, so keeping them here means that PR does not have to touch this job.

Comment thread .github/workflows/release.yml Outdated

concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: true

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.

Suggested change
cancel-in-progress: true
cancel-in-progress: false

release jobs shouldn't cancel each other.

A cancellation can advance past an unpublished version when a new changeset arrives, or publish to npm before the action creates the Git tag/release.

A rerun can't reliably reconstruct either split state.

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!

Preview cuts will publish @Shopify/hydrogen@<version>-preview.<n>
prereleases, with n incrementing on every cut. Changesets derives those
versions from the committed .changeset/pre.json created by `changeset
pre enter preview`, and `changeset publish` targets the preview npm
dist-tag while the file exists. Exit prerelease mode (`changeset pre
exit`) before this branch ever cuts a real release.

Config adjustments that support the flow:

- `format: false`: the formatter auto-detection in changesets v3 finds
  oxfmt in node_modules and feeds it the generated changelog markdown,
  which oxfmt cannot format, failing `changeset version` outright.
- Ignore @shopify/storefront-e2e: it depends on @Shopify/hydrogen via
  the workspace protocol, so every cut would otherwise bump and
  changelog it as a dependent. It is private and never publishes.
- `baseBranch: preview`: the changeset CLI compares against this ref
  to detect changed packages; main has diverged too far from this branch
  for that comparison to mean anything.
- The `$schema` URL now matches the installed config runtime
  (4.0.0-next.6), which also documents the `format` option.

The release script drops its explicit --tag flag because the pinned CLI
refuses custom tags in pre mode; the preview dist-tag comes from
pre.json instead. oxfmt now ignores pre.json since changesets owns and
rewrites that file on every version run, and any style disagreement
between the two tools would fail format:check on version PRs.
Preview releases were previously cut by manually dispatching main's
release.yml, which checked out this branch and published a one-off
0.0.0-preview-<sha>-<timestamp> version of @Shopify/hydrogen. Pushes to
preview now run the same changesets flow main uses: a version PR
accumulates pending changesets, and merging it publishes
@Shopify/hydrogen@<version>-preview.<n> to the preview npm dist-tag.

The workflow guards against ever leaving prerelease mode: without
.changeset/pre.json, changeset publish would publish a plain version
to the latest dist-tag and hijack it from the real releases cut from
main.

Concurrent runs queue instead of cancelling. A cancellation that lands
between npm publish and tag creation strands state a rerun cannot
rebuild, because npm refuses to publish the same version twice.
Starting packages/hydrogen at 2026.10.0-preview.0 makes every cut a
prerelease of the 2026.10.0 release itself: patch and minor changesets
complete the prerelease back to the same 2026.10.0 base and increment
only the counter, publishing 2026.10.0-preview.1, then
2026.10.0-preview.2, and so on. Published versions sort below the
eventual real 2026.10.0. The committed -preview.0 is never published
because versioning always increments first, so the first cut is
preview.1.

Minor changesets cannot move the base while its patch component is zero,
because semver completes the prerelease instead of bumping, so only a
major changeset threatens the pin (2026.10.0 to 2027.0.0). CI rejects
major changesets on pull requests for early feedback, and the release
workflow repeats the check before versioning as the hard stop. The check
parses changeset files with @changesets/parse instead of regex so quoted
values and other valid YAML forms cannot slip past it. Rejecting rather
than coercing keeps the declared bump types intact for whoever resolves
the failure.

Assisted-By: devx/c7a922dd-b56b-4d83-9238-b9e88e1011f0
@graygilmore
graygilmore force-pushed the gg-preview-release-workflow branch from a1be598 to f902a78 Compare July 31, 2026 23:22
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