Compare commits

...
Author SHA1 Message Date
Brent Vatne 6ffce49f61 Add a prerelease release process
There was no way to ship a version for people to try. Running the existing release
with a prerelease version would have been actively harmful: `lerna publish
from-package` passed no --dist-tag and npm has not special-cased prerelease versions
since v7, so `latest` would have pointed at the prerelease and every `npx eas-cli`
user would have gotten it. The run would also have moved `latest-eas-build` and
`latest-eas-build-staging` — which select the CLI version for EAS Build, so a
prerelease would have reached production builds — and consumed the unreleased
CHANGELOG.md section, committing it to main and Slack-announcing it as a release.
The stale `next` (7.1.0) and `alpha` (0.16.1-alpha.0) dist-tags show how long it has
been since anyone tried.

Detect a SemVer prerelease identifier on the pushed tag and branch on it: publish
under `next`, skip both EAS Build dist-tag steps, and skip the changelog job so the
entries stay unreleased for the stable release that follows. The GitHub release is
published immediately and flagged as a prerelease instead of waiting for the
changelog job to un-draft it.

The dist-tag is now always passed explicitly. `latest` is what npm defaulted to, so
stable releases are unchanged — but that silent default is what made this unsafe, so
the intent is stated rather than inherited.

Documents the process, what differs from a stable release, how it interacts with the
other packages in the monorepo (fixed mode versions only what changed; `next` covers
every package published in the run), and that prereleases are cut from main so they
are not the tool for validating an unmerged branch.
2026-07-26 19:22:30 -07:00
3 changed files with 124 additions and 5 deletions

No files matched your search

+48 -4
View File
@@ -6,9 +6,34 @@ on:
name: Release workflow
jobs:
# A prerelease tag (any version carrying a SemVer prerelease identifier, e.g.
# v21.3.0-beta.0) must NOT be treated like a stable release: npm does not
# special-case prerelease versions, so publishing one without an explicit dist-tag
# would point `latest` at it and hand it to every `npx eas-cli` user. Every job below
# branches on this one output instead of re-deriving it from the tag.
resolve:
name: Resolve release kind
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
prerelease: ${{ steps.resolve.outputs.prerelease }}
steps:
- id: resolve
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# SemVer puts the prerelease identifier after a hyphen: 21.3.0-beta.0.
if [[ "$VERSION" == *-* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
echo "::notice::$VERSION is a prerelease: publishing under the 'next' dist-tag, leaving 'latest', the EAS Build tags and CHANGELOG.md untouched."
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
release:
name: Create a release
runs-on: ubuntu-latest
needs: resolve
permissions:
contents: write
outputs:
@@ -21,11 +46,15 @@ jobs:
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
# A stable release stays a draft until the changelog job publishes it with
# notes. A prerelease skips that job, so publish it immediately and flag it so
# GitHub never presents it as the repo's latest release.
draft: ${{ needs.resolve.outputs.prerelease == 'false' }}
prerelease: ${{ needs.resolve.outputs.prerelease == 'true' }}
publish-to-npm:
name: Publish to npm
runs-on: ubuntu-latest
needs: release
needs: [resolve, release]
permissions:
id-token: write
steps:
@@ -35,9 +64,18 @@ jobs:
run: yarn install --immutable
- name: Build
run: yarn build
# Always pass the dist-tag explicitly. `latest` is what npm would have defaulted
# to, so stable releases are unchanged — but relying on that default is what made
# a prerelease publish unsafe, so the intent is stated rather than inherited.
- name: Publish packages to npm
run: yarn run -T lerna publish from-package --yes --no-verify-access
env:
DIST_TAG: ${{ needs.resolve.outputs.prerelease == 'true' && 'next' || 'latest' }}
run: yarn run -T lerna publish from-package --yes --no-verify-access --dist-tag "$DIST_TAG"
# These two tags select the CLI version used by EAS Build. A prerelease must never
# move them (that would put untested code in production builds); `latest-eas-build`
# also has its own gated workflow for moving it deliberately.
- name: Add latest-eas-build-staging tag
if: needs.resolve.outputs.prerelease == 'false'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
run: |
@@ -45,16 +83,22 @@ jobs:
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
- name: Add latest-eas-build tag
if: needs.resolve.outputs.prerelease == 'false'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build
# Stable releases only. A prerelease must leave CHANGELOG.md alone: its entries stay
# under "unreleased" so the eventual stable release still announces them. Skipping this
# job also keeps a prerelease from committing to main and from Slack-announcing itself
# as a release — the GitHub prerelease is its announcement.
release-changelog:
name: Update changelog and publish release
if: needs.resolve.outputs.prerelease == 'false'
runs-on: ubuntu-latest
needs: publish-to-npm
needs: [resolve, publish-to-npm]
permissions:
contents: write
steps:
+1 -1
View File
@@ -4,7 +4,7 @@ on:
version:
type: string
required: false
description: 'Version number for the release'
description: 'Version for the release, e.g. "21.3.0". Prerelease: "21.3.0-beta.0" (publishes under the `next` dist-tag; see RELEASING.md). Leave empty to derive the next stable version from the changelog.'
dry_run:
type: boolean
required: true
+75
View File
@@ -12,3 +12,78 @@ The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH version.
## Prereleases
A prerelease publishes a real version to npm that nobody gets by accident: it goes out
under the `next` dist-tag, so `latest` keeps pointing at the current stable release and
`npx eas-cli` is unaffected. Use one to get a risky change in front of users (or to dogfood
it in a real project) before committing to a stable release.
**Cutting one:** invoke the same ["Manually trigger a release"](https://github.com/expo/eas-cli/actions/workflows/trigger-release.yml)
workflow, but pass an explicit prerelease version — e.g. `21.3.0-beta.0`. Any version with a
SemVer prerelease identifier (the part after the hyphen) is treated as a prerelease; the
identifier itself is free-form, so `-beta.0`, `-rc.1` and `-canary.0` all work. You must pass
the version explicitly: the changelog-based algorithm above only ever produces stable versions.
**Installing one:**
```sh
npm i -g eas-cli@next # whatever the current prerelease is
npm i -g [email protected] # a specific one
```
**What a prerelease does differently.** Everything is driven off the pushed tag by
`.github/workflows/release.yml`, which detects the prerelease identifier and then:
| | Stable | Prerelease |
| ------------------------------------ | ----------------- | -------------------------------- |
| npm dist-tag | `latest` | `next` |
| `latest-eas-build` / `-staging` tags | moved | **left alone** |
| CHANGELOG.md | cut and committed | **left alone** |
| GitHub release | draft → published | published, flagged as prerelease |
| Slack announcement in #eas-cli | yes | no |
The EAS Build tags are deliberately untouched: they select the CLI version used by EAS
Build, so moving them to a prerelease would put untested code into production builds.
Move them on purpose with the ["Move NPM tags used by EAS builds and workflows"](https://github.com/expo/eas-cli/actions/workflows/move-eas-build-tag.yml)
workflow if that is what you actually want.
CHANGELOG.md is also untouched, so a prerelease's entries stay under "unreleased" and are
announced with the stable release that follows.
### Prereleases in this monorepo
Prereleases version and publish packages exactly like stable releases do — lerna runs in
fixed mode, so it applies the single version from `lerna.json` to the packages that changed
since the last release and leaves the rest alone (which is why the packages sit at different
versions). `lerna publish from-package` then publishes whichever of those aren't on the
registry yet, and the `next` dist-tag applies to every package published in that run.
`latest` is untouched for all of them, so nothing that depends on `@expo/eas-json`,
`@expo/steps`, `@expo/eas-build-job` or friends picks up a prerelease implicitly.
Two consequences worth knowing:
- **The version line is shared.** After `21.3.0-beta.0`, the next stable release from main
is `21.3.0` — the prerelease consumes that version number for the whole repo. Cutting a
`21.2.1` patch afterwards means passing that version explicitly.
- **main carries the prerelease version.** `lerna version` commits the bump, so `lerna.json`
and the changed packages sit at `21.3.0-beta.0` until the next release. The changelog-based
algorithm still resolves correctly from there (a MINOR or PATCH bump off `21.3.0-beta.0`
both land on `21.3.0`).
### Testing an unmerged branch
Prereleases are cut from `main`, so they are not the tool for validating a PR before it
merges. For that, build the branch locally and run it directly:
```sh
yarn && yarn build
./packages/eas-cli/bin/run --version
```
or pack a tarball to install elsewhere:
```sh
cd packages/eas-cli && yarn pack # then: npm i -g ./eas-cli-*.tgz
```