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
6 changes: 5 additions & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ on:
push:
branches: [main]
workflow_dispatch:
# Reused by the release workflow so releases run the same checks.
workflow_call:

permissions:
contents: read

concurrency:
group: quality-${{ github.ref }}
# github.workflow is the caller's name when reused, so a release never
# shares (and gets cancelled with) the group of a regular push run.
group: ${{ github.workflow }}-quality-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down
198 changes: 198 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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).
#
# 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:
workflow_dispatch:
inputs:
version:
description: Release version without the leading v (for example 3.4.0). Must have a CHANGELOG section.
required: true
type: string
target:
description: What to release.
required: true
default: all
type: choice
options:
- all
- github
- npm
dry_run:
description: Validate and preview only; create nothing and publish nothing.
required: true
default: true
type: boolean

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
prepare:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
tag: ${{ steps.meta.outputs.tag }}
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
env:
VERSION: ${{ inputs.version }}
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
fi
if ! grep -qF "## [$VERSION]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## [$VERSION]' section."
exit 1
fi
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"

quality:
needs: prepare
uses: ./.github/workflows/quality.yml

github:
needs: [prepare, quality]
if: inputs.target == 'all' || inputs.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 }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
- name: Extract release notes
run: |
awk -v header="## [$VERSION]" '
index($0, header) == 1 { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md | sed -e '/./,$!d' > "$RUNNER_TEMP/notes.md"
compare=$(grep -F "[$VERSION]: " CHANGELOG.md | head -1 | sed 's/^[^:]*: //')
if [ -n "$compare" ]; then
printf '\n**Full Changelog**: %s\n' "$compare" >> "$RUNNER_TEMP/notes.md"
fi
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/notes.md"; then
echo "::error::The CHANGELOG section for $VERSION is empty."
exit 1
fi
cat "$RUNNER_TEMP/notes.md"
- name: Create tag
run: |
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists; keeping it."
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo "[dry run] Would tag $GITHUB_SHA as $TAG."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" "$GITHUB_SHA" -m "$TAG"
git push origin "refs/tags/$TAG"
- name: Create GitHub release
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists; keeping it."
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo "[dry run] Would create release $TAG with the notes above."
exit 0
fi
gh release create "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag

npm:
needs: [prepare, quality, github]
if: >-
!cancelled()
&& needs.quality.result == 'success'
&& (needs.github.result == 'success' || needs.github.result == 'skipped')
&& (inputs.target == 'all' || inputs.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 }}
steps:
- name: Resolve source ref
id: source
run: |
# Publish exactly what was tagged; a dry run before tagging uses main.
if git ls-remote --exit-code --tags "https://github.com/${GITHUB_REPOSITORY}.git" "refs/tags/$TAG" >/dev/null; then
echo "ref=refs/tags/$TAG" >> "$GITHUB_OUTPUT"
elif [ "$DRY_RUN" = "true" ]; then
echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
else
echo "::error::Tag $TAG does not exist. Run with target 'all' or 'github' first."
exit 1
fi
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ steps.source.outputs.ref }}
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 10.11.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "24"
registry-url: https://registry.npmjs.org
cache: pnpm
cache-dependency-path: studio/pnpm-lock.yaml
- name: Use an npm that supports Trusted Publishing
run: npm install -g npm@^11.5.1
- name: Build Studio packages
working-directory: studio
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Pack and publish
run: |
mkdir -p "$RUNNER_TEMP/packages"
# editor-core first: the Web Component depends on it.
for dir in studio/packages/editor-core studio/packages/web-component; do
name=$(node -p "require('./$dir/package.json').name")
version=$(node -p "require('./$dir/package.json').version")
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "$name@$version is already on npm; skipping."
continue
fi
# pnpm pack rewrites workspace:* to the real version.
tarball=$(cd "$dir" && pnpm pack --pack-destination "$RUNNER_TEMP/packages" | tail -1)
if tar -xOzf "$tarball" package/package.json | grep -q '"workspace:'; then
echo "::error::$name@$version still has a workspace: dependency."
exit 1
fi
if [ "$DRY_RUN" = "true" ]; then
npm publish "$tarball" --access public --dry-run
else
npm publish "$tarball" --access public --provenance
fi
done
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Thank you for your interest in contributing to StatePro! This guide outlines how
- [Documentation](#documentation)
- [Pull Request Process](#pull-request-process)
- [Reporting Issues](#reporting-issues)
- [Releasing](#releasing)
- [Getting Help](#getting-help)
- [Recognition](#recognition)

Expand Down Expand Up @@ -379,6 +380,29 @@ For new features, include:
- `help wanted`: Community contribution needed
- `question`: Questions and discussions

## Releasing

Releases are cut by maintainers with 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`.

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
Publisher → GitHub Actions**) with organization `rendis`, repository `statepro`, and workflow
`release.yml`.

## Getting Help

- **Issues**: Use GitHub issues for bugs and feature requests
Expand Down
Loading