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
88 changes: 69 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
name: Release

# Manual release: validates the repository, tags `main`, creates the GitHub
# release from the CHANGELOG section, and publishes the Studio packages to npm
# through npm Trusted Publishing (OIDC, no stored npm token).
# Releases the latest CHANGELOG version: validates the repository, tags `main`,
# creates the GitHub release from the CHANGELOG section, and publishes the
# Studio packages to npm through npm Trusted Publishing (OIDC, no stored token).
#
# On every push to main it releases automatically when the newest
# `## [X.Y.Z]` section of CHANGELOG.md has no `vX.Y.Z` tag yet, or publishes to
# npm when that version is tagged but the Studio package versions are not on npm.
# Otherwise it does nothing. It can also be run manually.
#
# Each step is idempotent: an existing tag or release is kept, and package
# versions already on npm are skipped, so a failed run can be re-run safely.

on:
push:
branches: [main]
workflow_dispatch:
inputs:
version:
Expand Down Expand Up @@ -41,44 +48,87 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
tag: ${{ steps.meta.outputs.tag }}
version: ${{ steps.plan.outputs.version }}
tag: ${{ steps.plan.outputs.tag }}
target: ${{ steps.plan.outputs.target }}
dry_run: ${{ steps.plan.outputs.dry_run }}
steps:
- name: Require main
if: github.ref != 'refs/heads/main'
run: |
echo "::error::Run the release workflow from main (got ${GITHUB_REF})."
exit 1
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Validate version and CHANGELOG
id: meta
- name: Plan release
id: plan
env:
VERSION: ${{ inputs.version }}
EVENT: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_TARGET: ${{ inputs.target }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
run: |
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Version '$VERSION' is not semver without a leading v."
exit 1
if [ "$EVENT" = "workflow_dispatch" ]; then
version="$INPUT_VERSION"
target="$INPUT_TARGET"
dry_run="$INPUT_DRY_RUN"
else
# Newest released section; [Unreleased] is not matched.
version=$(grep -m1 -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+[^]]*\]' CHANGELOG.md | sed -E 's/^## \[(.*)\]$/\1/')
dry_run=false
if [ -z "$version" ]; then
echo "No released CHANGELOG section; nothing to do."
target=none
elif ! git ls-remote --exit-code --tags origin "refs/tags/v$version" >/dev/null; then
echo "v$version is not tagged yet; releasing."
target=all
else
target=none
for dir in studio/packages/editor-core studio/packages/web-component; do
name=$(node -p "require('./$dir/package.json').name")
pkg_version=$(node -p "require('./$dir/package.json').version")
if ! npm view "$name@$pkg_version" version >/dev/null 2>&1; then
echo "$name@$pkg_version is not on npm; publishing."
target=npm
fi
done
if [ "$target" = "none" ]; then
echo "v$version is tagged and the Studio packages are on npm; nothing to do."
fi
fi
fi
if ! grep -qF "## [$VERSION]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## [$VERSION]' section."
exit 1
if [ "$target" != "none" ]; then
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Version '$version' is not semver without a leading v."
exit 1
fi
if ! grep -qF "## [$version]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## [$version]' section."
exit 1
fi
fi
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
{
echo "version=$version"
echo "tag=v$version"
echo "target=$target"
echo "dry_run=$dry_run"
} >> "$GITHUB_OUTPUT"

quality:
needs: prepare
if: needs.prepare.outputs.target != 'none'
uses: ./.github/workflows/quality.yml

github:
needs: [prepare, quality]
if: inputs.target == 'all' || inputs.target == 'github'
if: needs.prepare.outputs.target == 'all' || needs.prepare.outputs.target == 'github'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
env:
TAG: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
VERSION: ${{ needs.prepare.outputs.version }}
DRY_RUN: ${{ needs.prepare.outputs.dry_run }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down Expand Up @@ -132,15 +182,15 @@ jobs:
!cancelled()
&& needs.quality.result == 'success'
&& (needs.github.result == 'success' || needs.github.result == 'skipped')
&& (inputs.target == 'all' || inputs.target == 'npm')
&& (needs.prepare.outputs.target == 'all' || needs.prepare.outputs.target == 'npm')
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # npm Trusted Publishing and provenance
env:
TAG: ${{ needs.prepare.outputs.tag }}
DRY_RUN: ${{ inputs.dry_run }}
DRY_RUN: ${{ needs.prepare.outputs.dry_run }}
steps:
- name: Resolve source ref
id: source
Expand Down
25 changes: 13 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,21 +382,22 @@ For new features, include:

## Releasing

Releases are cut by maintainers with the **Release** workflow (`.github/workflows/release.yml`).
Releases are driven by `CHANGELOG.md` and the **Release** workflow (`.github/workflows/release.yml`).

1. Open a PR that moves the `[Unreleased]` entries in `CHANGELOG.md` to a `## [X.Y.Z] - YYYY-MM-DD`
section, updates the compare links, and bumps the Studio package versions when they changed.
Merge it to `main`.
2. Run **Actions → Release → Run workflow** on `main` with the version `X.Y.Z` and `dry_run` enabled.
Check the previewed notes and the npm dry-run output.
3. Run it again with `dry_run` disabled. The workflow runs the Quality checks, creates the annotated
tag `vX.Y.Z` on `main`, creates the GitHub release from the CHANGELOG section, and publishes the
Studio packages to npm with provenance.

`target` limits the run to `github` (tag and release only, for example a Go-only release) or `npm`
(publish an existing tag). Every step is idempotent: an existing tag or release is kept and package
versions already on npm are skipped, so a failed run can be re-run. The Go module needs no extra step;
the tag is enough for `go get`.
2. Merge it to `main`. On that push the workflow sees that the newest CHANGELOG version has no
`vX.Y.Z` tag, runs the Quality checks, creates the annotated tag on the merge commit, creates the
GitHub release from the CHANGELOG section, and publishes the Studio packages to npm with provenance.

On other pushes to `main` the workflow does nothing, unless the newest version is tagged but its
Studio package versions are missing from npm; then it only publishes them. Every step is idempotent:
an existing tag or release is kept and package versions already on npm are skipped. The Go module
needs no extra step; the tag is enough for `go get`.

The workflow can also be run manually (**Actions → Release → Run workflow**) with a `version`, a
`target` (`all`, `github` for tag and release only, or `npm`), and `dry_run` to preview the notes
and the npm packages without creating anything.

npm publishing uses [Trusted Publishing](https://docs.npmjs.com/trusted-publishers), so no npm token is
stored. Each package needs a one-time trusted publisher on npmjs.com (package **Settings → Trusted
Expand Down
Loading