Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Show the fixed output if appropriate.
## Release

<!--
Merging a PR into main that bumps the version publishes it (docs/releasing.md). Pick one:
Merging a PR into dev that bumps the version releases it on dev; fast-forwarding stage and then prod promotes it (docs/releasing.md). Pick one:
-->

- [ ] Releases vX.Y.Z: ran `bin/bump …` and described it in CHANGELOG.md
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: CI

# Pull requests run it directly; the Release workflow calls it on every push to main, so the
# merge result is tested again before anything is published.
# Pull requests run it directly; the Release workflow calls it on every push to dev, stage, prod
# and main, so the merge result is tested again before anything is published or promoted.

on:
pull_request:
Expand Down Expand Up @@ -40,8 +40,9 @@ jobs:

version:
name: Version
# Only a PR into main cuts a release, so only there do the release rules apply.
if: github.event_name == 'pull_request' && github.base_ref == 'main'
# Only a PR into dev cuts a release (stage and prod are fast-forwarded from it), so only there
# do the release rules apply.
if: github.event_name == 'pull_request' && github.base_ref == 'dev'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
271 changes: 196 additions & 75 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
name: Release

# A merge into main that bumped the version publishes it to public npm, tags it with a GitHub
# Release, and mirrors it to GitHub Packages, as every @beplus/* repository does; any other merge
# finds nothing to do. Every step checks what is already done, so re-running a failed run is safe.
# See docs/releasing.md.
# One version per commit, built once, promoted — the model every @beplus library follows
# (beplus/setup-beplus's library-publish.yml), for a package that is one bash script and no Rush:
#
# npm trusts this file by name (Trusted Publisher: beplus/be, release.yml, environment npm), so
# there is no npm token anywhere: the publish job authenticates with its OIDC token. GitHub Packages
# takes the workflow's own GITHUB_TOKEN.
# dev A push that carries a new version packs it ONCE, tags it vX.Y.Z with a GitHub
# pre-release, and publishes that tarball to GitHub Packages (dist-tag `dev`), then to
# dev's CodeArtifact — npm.beplus.cloud, where the estates' builds install `be` from.
# stage Builds nothing: fetches the tarball dev published from GitHub Packages and publishes it
# byte for byte to stage's CodeArtifact. GitHub Packages tags it `stage`.
# prod The same into prod's CodeArtifact (GitHub Packages tags it `latest`), and the GitHub
# Release becomes a full release, marked latest.
# main The public channel, and the one thing `be` has that the libraries do not: the same
# bytes to the public npm registry (trusted publishing, with provenance).
#
# stage, prod and main are fast-forwarded (dev → stage → prod → main), never committed to; a
# version GitHub Packages does not have is one dev never released, and is refused there, and main
# publishes only what prod already has. Every step checks what is already done,
# so re-running a failed run is safe. See docs/releasing.md.
#
# Each environment's job assumes github-actions-beplus-be-<env>-npm-publishing in the account its
# GitHub environment names (vars.BE_AWS_ACCOUNT_ID) — the role name library-publish derives — and
# `beplus aws codeartifact auth --env <env>` picks that environment's CodeArtifact repository.
# npm trusts this file by name (Trusted Publisher: beplus/be, release.yml, environment npm), so no
# npm token exists anywhere. GitHub Packages takes the workflow's own GITHUB_TOKEN.

on:
push:
branches: [main]
branches: [dev, stage, prod, main]
workflow_dispatch:

permissions:
contents: read

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

jobs:
Expand All @@ -29,7 +44,7 @@ jobs:
plan:
name: Plan
needs: ci
if: github.ref == 'refs/heads/main'
if: contains(fromJSON('["dev", "stage", "prod", "main"]'), github.ref_name)
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -39,119 +54,225 @@ jobs:
tag: ${{ steps.verify.outputs.tag }}
dist_tag: ${{ steps.verify.outputs.dist_tag }}
source_sha: ${{ steps.verify.outputs.source_sha }}
need_publish: ${{ steps.verify.outputs.need_publish }}
need_release: ${{ steps.verify.outputs.need_release }}
need_github_packages: ${{ steps.verify.outputs.need_github_packages }}
need_npm: ${{ steps.verify.outputs.need_npm }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # main publishes only what prod has: HEAD must be on origin/prod
- id: verify
run: scripts/release/verify-version.sh release
run: scripts/release/verify-version.sh release "${GITHUB_REF_NAME}"
env:
GITHUB_PACKAGES_TOKEN: ${{ github.token }}

publish:
name: Publish ${{ needs.plan.outputs.tag }}
dev:
name: Release ${{ needs.plan.outputs.tag }} on dev
needs: plan
if: needs.plan.outputs.need_publish == 'true' || needs.plan.outputs.need_release == 'true'
if: github.ref_name == 'dev'
runs-on: ubuntu-latest
environment: npm
environment: dev
permissions:
contents: write # tag + GitHub Release
id-token: write # npm trusted publishing and provenance
contents: write # the tag and the GitHub pre-release
packages: write # GitHub Packages
id-token: write # the CodeArtifact role
env:
BE_ENVIRONMENT: ${{ vars.BE_ENVIRONMENT }}
BE_AWS_ACCOUNT_ID: ${{ vars.BE_AWS_ACCOUNT_ID }}
VERSION: ${{ needs.plan.outputs.version }}
TAG: ${{ needs.plan.outputs.tag }}
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}
SOURCE_SHA: ${{ needs.plan.outputs.source_sha }}
steps:
# The commit the version was cut from, which is its tag once it has one: a later push that
# bumps nothing still publishes exactly what was released.
- uses: actions/checkout@v7
with:
ref: ${{ needs.plan.outputs.source_sha }}

- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Make sure npm can publish with OIDC (>= 11.5.1)
run: |
if ! node -e '
const [a, b, c] = process.argv[1].split(".").map(Number);
process.exit(a > 11 || (a === 11 && (b > 5 || (b === 5 && c >= 1))) ? 0 : 1);
' "$(npm --version)"; then
npm install --global npm@11
fi
npm --version
- name: Check the prerequisites
run: scripts/release/check-environment.sh dev

- name: Publish to npm
if: needs.plan.outputs.need_publish == 'true'
run: npm publish --provenance --tag "${DIST_TAG}"
env:
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}
- name: Pack it once
run: |
mkdir -p npm
npm pack --pack-destination npm
ls -l npm

- name: Tag and create the GitHub Release
- name: Tag it and create the GitHub pre-release
if: needs.plan.outputs.need_release == 'true'
run: |
scripts/release/changelog-notes.sh "${TAG}" > "${RUNNER_TEMP}/notes.md"
if [[ "${DIST_TAG}" == "latest" ]]; then
channel=(--latest)
else
channel=(--prerelease --latest=false)
fi
gh release create "${TAG}" --target "${GITHUB_SHA}" --title "${TAG}" \
--notes-file "${RUNNER_TEMP}/notes.md" "${channel[@]}"
gh release create "${TAG}" --target "${SOURCE_SHA}" --title "${TAG}" \
--notes-file "${RUNNER_TEMP}/notes.md" --prerelease --latest=false
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.plan.outputs.tag }}
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}

# publishConfig pins the @beplus scope to npmjs, and only command-line flags outrank it, so
# every publish names its registry on the command line; the token stays in the environment.
- name: Publish to GitHub Packages
if: needs.plan.outputs.need_github_packages == 'true'
run: |
echo '//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}' > "${RUNNER_TEMP}/.npmrc-github"
# `./`: to npm, a bare `npm/<file>.tgz` is a GitHub `owner/repo` shorthand.
npm publish ./npm/*.tgz --userconfig "${RUNNER_TEMP}/.npmrc-github" \
--registry=https://npm.pkg.github.com/ --@beplus:registry=https://npm.pkg.github.com/ \
--tag dev
env:
NODE_AUTH_TOKEN: ${{ github.token }}

- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.BE_AWS_ACCOUNT_ID }}:role/github-actions-beplus-${{ github.event.repository.name }}-${{ github.ref_name }}-npm-publishing
aws-region: us-east-1

- uses: beplus/setup-beplus/cli@v2
with:
BE_CLI_VERSION: ${{ vars.BE_CLI_VERSION }}

- name: Publish to CodeArtifact
run: scripts/release/publish-codeartifact.sh ./npm/*.tgz

- name: Summary
run: |
{
echo "### Released ${TAG}"
echo "### Released ${TAG} on dev"
echo
echo "- npm: https://www.npmjs.com/package/@beplus/be/v/${VERSION}"
echo "- GitHub: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
echo "- GitHub (pre-release): ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
echo "- GitHub Packages: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pkgs/npm/be (dist-tag \`dev\`)"
echo "- dev's CodeArtifact (npm.beplus.cloud): dist-tag \`${DIST_TAG}\`"
} >> "${GITHUB_STEP_SUMMARY}"
env:
TAG: ${{ needs.plan.outputs.tag }}
VERSION: ${{ needs.plan.outputs.version }}

github-packages:
name: GitHub Packages ${{ needs.plan.outputs.tag }}
needs: [plan, publish]
# After npm, so the mirror never has a version npm refused; also when there was nothing left to
# do on npm (publish skipped), which is how a version missing only here gets published.
if: >-
always() && needs.plan.result == 'success'
&& needs.plan.outputs.need_github_packages == 'true'
&& (needs.publish.result == 'success' || needs.publish.result == 'skipped')

promote:
name: Promote ${{ needs.plan.outputs.tag }} to ${{ github.ref_name }}
needs: plan
if: github.ref_name == 'stage' || github.ref_name == 'prod'
runs-on: ubuntu-latest
environment: ${{ github.ref_name }}
permissions:
contents: read
packages: write
contents: write # prod: the GitHub Release becomes a full release
packages: write # the stage/latest dist-tag in GitHub Packages
id-token: write # the CodeArtifact role
env:
BE_ENVIRONMENT: ${{ vars.BE_ENVIRONMENT }}
BE_AWS_ACCOUNT_ID: ${{ vars.BE_AWS_ACCOUNT_ID }}
VERSION: ${{ needs.plan.outputs.version }}
TAG: ${{ needs.plan.outputs.tag }}
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}
steps:
# The commit the version was cut from, which is its tag once it has one: a later merge that
# bumps nothing still publishes exactly what npm has.
- uses: actions/checkout@v7
with:
ref: ${{ needs.plan.outputs.source_sha }}

- uses: actions/setup-node@v7
with:
node-version: 24

# publishConfig pins the @beplus scope to npmjs, and only command-line flags outrank it, so
# the .npmrc carries just the token, read from the environment rather than written to disk.
- name: Publish to GitHub Packages
- name: Check the prerequisites
run: scripts/release/check-environment.sh "${GITHUB_REF_NAME}"

- name: Fetch it from GitHub Packages
run: scripts/release/fetch-github-packages.sh "${VERSION}" npm
env:
NODE_AUTH_TOKEN: ${{ github.token }}

- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.BE_AWS_ACCOUNT_ID }}:role/github-actions-beplus-${{ github.event.repository.name }}-${{ github.ref_name }}-npm-publishing
aws-region: us-east-1

- uses: beplus/setup-beplus/cli@v2
with:
BE_CLI_VERSION: ${{ vars.BE_CLI_VERSION }}

- name: Publish to CodeArtifact
run: scripts/release/publish-codeartifact.sh ./npm/*.tgz

# How far a version got, in the one registry every environment shares: `stage`, and prod's
# `latest`. A registry that refuses a tag warns rather than stopping a promotion.
- name: Tag it in GitHub Packages
continue-on-error: true
run: |
echo '//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}' > "${RUNNER_TEMP}/.npmrc-github"
npm publish --userconfig "${RUNNER_TEMP}/.npmrc-github" \
--registry=https://npm.pkg.github.com/ --@beplus:registry=https://npm.pkg.github.com/ \
--tag "${DIST_TAG}"
npm dist-tag add "@beplus/be@${VERSION}" "${PROMOTED_TAG}" \
--userconfig "${RUNNER_TEMP}/.npmrc-github" --@beplus:registry=https://npm.pkg.github.com/ \
|| echo "::warning::GitHub Packages did not tag @beplus/be@${VERSION} ${PROMOTED_TAG}."
env:
NODE_AUTH_TOKEN: ${{ github.token }}
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}
PROMOTED_TAG: ${{ github.ref_name == 'prod' && 'latest' || github.ref_name }}

- name: Make the GitHub release a full release
if: github.ref_name == 'prod'
run: |
if [[ "$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json isPrerelease --jq .isPrerelease)" == "true" ]]; then
if [[ "${DIST_TAG}" == "latest" ]]; then latest=(--latest); else latest=(--latest=false); fi
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --prerelease=false "${latest[@]}"
fi
env:
GH_TOKEN: ${{ github.token }}

- name: Summary
run: |
{
echo "### Mirrored ${TAG} to GitHub Packages"
echo "### Promoted ${TAG} to ${GITHUB_REF_NAME}"
echo
echo "- ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pkgs/npm/be"
echo "- ${GITHUB_REF_NAME}'s CodeArtifact: dist-tag \`${DIST_TAG}\`, the bytes dev published"
} >> "${GITHUB_STEP_SUMMARY}"

# main only: the public registry. Its own job because npm's trusted publisher is bound to the
# `npm` environment, whose deployment branch is main.
public:
name: Publish ${{ needs.plan.outputs.tag }} to npm
needs: plan
if: github.ref_name == 'main'
runs-on: ubuntu-latest
environment: npm
permissions:
contents: read
packages: read # the tarball, from GitHub Packages
id-token: write # npm trusted publishing and provenance
env:
VERSION: ${{ needs.plan.outputs.version }}
TAG: ${{ needs.plan.outputs.tag }}
DIST_TAG: ${{ needs.plan.outputs.dist_tag }}
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Make sure npm can publish with OIDC (>= 11.5.1)
if: needs.plan.outputs.need_npm == 'true'
run: |
if ! node -e '
const [a, b, c] = process.argv[1].split(".").map(Number);
process.exit(a > 11 || (a === 11 && (b > 5 || (b === 5 && c >= 1))) ? 0 : 1);
' "$(npm --version)"; then
npm install --global npm@11
fi
npm --version

- name: Fetch it from GitHub Packages
if: needs.plan.outputs.need_npm == 'true'
run: scripts/release/fetch-github-packages.sh "${VERSION}" npm
env:
TAG: ${{ needs.plan.outputs.tag }}
NODE_AUTH_TOKEN: ${{ github.token }}

- name: Publish to npm
if: needs.plan.outputs.need_npm == 'true'
run: |
npm publish ./npm/*.tgz --provenance --tag "${DIST_TAG}" \
--registry=https://registry.npmjs.org/ --@beplus:registry=https://registry.npmjs.org/

- name: Summary
run: |
{
echo "### Released ${TAG}"
echo
echo "- npm: https://www.npmjs.com/package/@beplus/be/v/${VERSION}"
echo "- GitHub: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
} >> "${GITHUB_STEP_SUMMARY}"
Loading
Loading