diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fcabad17ca..c7dc7497d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/.github/workflows/trigger-release.yml b/.github/workflows/trigger-release.yml index 09ff440525..d9f53f328f 100644 --- a/.github/workflows/trigger-release.yml +++ b/.github/workflows/trigger-release.yml @@ -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 diff --git a/RELEASING.md b/RELEASING.md index 47e736630a..f46097fc29 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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 eas-cli@21.3.0-beta.0 # 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 +```