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
100 changes: 48 additions & 52 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,90 +1,86 @@
name: Publish

on:
pull_request_target:
types: [closed]

concurrency:
group: release-main
cancel-in-progress: false
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: Tag to publish (for example v0.7.1)
required: true
type: string

jobs:
publish:
if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write

steps:
- name: Determine release bump from labels
id: release_meta
uses: actions/github-script@v7
with:
script: |
const labels = (context.payload.pull_request.labels || []).map((label) => label.name);
const candidates = ["major", "minor", "patch"];
const matched = candidates.filter((candidate) => labels.includes(candidate));

if (matched.length === 0) {
core.notice("No release label found (major|minor|patch). Skipping publish.");
core.setOutput("should_release", "false");
return;
}

if (matched.length > 1) {
core.setFailed(`Multiple release labels found: ${matched.join(", ")}. Keep exactly one of major|minor|patch.`);
return;
}

core.info(`Selected release bump: ${matched[0]}`);
core.setOutput("should_release", "true");
core.setOutput("bump", matched[0]);
- name: Resolve tag to publish
id: publish_meta
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi
case "$TAG" in
v*) ;;
*)
echo "Tag must start with v: $TAG"
exit 1
;;
esac
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v4
if: steps.release_meta.outputs.should_release == 'true'
with:
ref: main
ref: ${{ steps.publish_meta.outputs.tag }}
fetch-depth: 0

- uses: actions/setup-node@v4
if: steps.release_meta.outputs.should_release == 'true'
with:
node-version: 20
cache: npm
registry-url: https://registry.npmjs.org

- run: npm ci
if: steps.release_meta.outputs.should_release == 'true'

- run: npm run check
if: steps.release_meta.outputs.should_release == 'true'
env:
ARENA_VCR_MODE: replay
ARENA_API_URL: https://staging-api.are.na

- name: Configure git author
if: steps.release_meta.outputs.should_release == 'true'
- name: Read package version
id: package_meta
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Bump version and create tag
id: version
if: steps.release_meta.outputs.should_release == 'true'
- name: Check whether version is already on npm
id: npm_state
shell: bash
run: |
NEW_TAG="$(npm version "${{ steps.release_meta.outputs.bump }}" -m "release: %s")"
echo "tag=${NEW_TAG}" >> "$GITHUB_OUTPUT"

- name: Push release commit and tags
if: steps.release_meta.outputs.should_release == 'true'
run: git push origin HEAD:main --follow-tags
if npm view "@aredotna/cli@${{ steps.package_meta.outputs.version }}" version >/dev/null 2>&1; then
echo "already_published=true" >> "$GITHUB_OUTPUT"
else
echo "already_published=false" >> "$GITHUB_OUTPUT"
fi

- run: npm publish --provenance --access public
if: steps.release_meta.outputs.should_release == 'true'
if: steps.npm_state.outputs.already_published != 'true'

- name: Create GitHub Release
if: steps.release_meta.outputs.should_release == 'true'
- name: Ensure GitHub Release exists
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${{ steps.version.outputs.tag }}" --generate-notes
shell: bash
run: |
if gh release view "${{ steps.publish_meta.outputs.tag }}" >/dev/null 2>&1; then
echo "GitHub Release already exists for ${{ steps.publish_meta.outputs.tag }}"
else
gh release create "${{ steps.publish_meta.outputs.tag }}" --generate-notes
fi
111 changes: 111 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Release

on:
pull_request_target:
types: [closed]
workflow_dispatch:
inputs:
bump:
description: Version bump type
required: true
type: choice
options: [patch, minor, major]

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

jobs:
release:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main')
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Determine release bump from labels or input
id: release_meta
uses: actions/github-script@v7
with:
script: |
if (context.eventName === "workflow_dispatch") {
const bump = context.payload.inputs.bump;
core.info(`Manual dispatch: releasing with bump=${bump}`);
core.setOutput("should_release", "true");
core.setOutput("bump", bump);
return;
}

const labels = (context.payload.pull_request.labels || []).map((label) => label.name);
const candidates = ["major", "minor", "patch"];
const matched = candidates.filter((candidate) => labels.includes(candidate));

if (matched.length === 0) {
core.notice("No release label found (major|minor|patch). Skipping release.");
core.setOutput("should_release", "false");
return;
}

if (matched.length > 1) {
core.setFailed(`Multiple release labels found: ${matched.join(", ")}. Keep exactly one of major|minor|patch.`);
return;
}

core.info(`Selected release bump: ${matched[0]}`);
core.setOutput("should_release", "true");
core.setOutput("bump", matched[0]);

- uses: actions/checkout@v4
if: steps.release_meta.outputs.should_release == 'true'
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN != '' && secrets.RELEASE_TOKEN || github.token }}

- name: Ensure main has not advanced past merged PR
if: >-
steps.release_meta.outputs.should_release == 'true' &&
github.event_name == 'pull_request_target'
run: |
CURRENT_HEAD="$(git rev-parse HEAD)"
EXPECTED_HEAD="${{ github.event.pull_request.merge_commit_sha }}"
if [ "$CURRENT_HEAD" != "$EXPECTED_HEAD" ]; then
echo "main advanced after this PR merged."
echo "Expected: $EXPECTED_HEAD"
echo "Current: $CURRENT_HEAD"
echo "Re-run the Release workflow manually after reviewing current main."
exit 1
fi

- uses: actions/setup-node@v4
if: steps.release_meta.outputs.should_release == 'true'
with:
node-version: 20
cache: npm

- run: npm ci
if: steps.release_meta.outputs.should_release == 'true'

- run: npm run check
if: steps.release_meta.outputs.should_release == 'true'
env:
ARENA_VCR_MODE: replay
ARENA_API_URL: https://staging-api.are.na

- name: Configure git author
if: steps.release_meta.outputs.should_release == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Bump version and create tag
if: steps.release_meta.outputs.should_release == 'true'
run: |
npm version "${{ steps.release_meta.outputs.bump }}" -m "release: %s"

- name: Push release commit and tags
if: steps.release_meta.outputs.should_release == 'true'
run: git push origin HEAD:main --follow-tags
15 changes: 12 additions & 3 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Use this checklist to cut a new npm release for `@aredotna/cli`.

Publishing is automated by GitHub Actions when a PR with a release label is merged to `main`.
Use exactly one label on the PR: `major`, `minor`, or `patch`.
The workflow bumps the version, publishes to npm with trusted publishing (OIDC), pushes the release commit and tag, and creates a GitHub Release.
The `Release` workflow bumps the version and pushes the release commit and tag.
That tag then triggers the `Publish` workflow, which publishes to npm with trusted publishing (OIDC) and creates the GitHub Release.

## 1) Preflight

Expand All @@ -26,11 +27,17 @@ npm run check

- [ ] Merge the labeled PR.

This triggers `.github/workflows/publish.yml`.
This triggers `.github/workflows/release.yml`.

## 3) Confirm publish workflow success

- [ ] Wait for the "Publish" workflow on the pushed tag to complete successfully:
- [ ] Wait for the `Release` workflow to complete successfully:

```bash
gh run list --workflow release.yml --limit 5
```

- [ ] Wait for the `Publish` workflow on the created tag to complete successfully:

```bash
gh run list --workflow publish.yml --limit 5
Expand Down Expand Up @@ -71,3 +78,5 @@ arena whoami --json
- Publish job fails with OIDC/trusted publishing error: verify trusted publisher settings on npm exactly match `aredotna/cli` and workflow file `publish.yml`.
- Workflow skips publishing: merged PR did not contain one of `major`, `minor`, or `patch` labels.
- Workflow fails with multiple release labels: keep exactly one of `major|minor|patch` on the PR.
- Release job says `main` advanced after the PR merged: re-run the `Release` workflow manually after reviewing current `main`.
- Publish job fails after the tag already exists: re-run the `Publish` workflow with the existing tag instead of creating a new version bump.
Loading