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
35 changes: 27 additions & 8 deletions .github/scripts/release-workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ function releaseContext(env = process.env) {
githubRef: env.GITHUB_REF || "",
githubRepository: env.GITHUB_REPOSITORY || "",
githubSha: env.GITHUB_SHA || "",
refInput: env.REF_INPUT || "",
};
}

function effectiveRef(ctx) {
if (ctx.refInput) return ctx.refInput;
const match = ctx.githubRef.match(/^refs\/heads\/(.+)$/);
return match ? match[1] : null;
}

function validateDispatch(env = process.env) {
const ctx = releaseContext(env);

Expand All @@ -49,6 +56,11 @@ function validateDispatch(env = process.env) {
return ctx;
}

const ref = effectiveRef(ctx);
if (!ctx.prerelease && ref && ref !== "main") {
fail(`stable releases require ref 'main'; use a prerelease version for branch '${ref}'`);
}

if (ctx.prerelease) {
if (!ctx.githubRef.startsWith("refs/heads/") && ctx.githubRef !== ctx.tagRef) {
fail(`prerelease must be dispatched from a branch or ${ctx.tagRef}`);
Expand Down Expand Up @@ -179,23 +191,27 @@ function tagAndDispatch(env = process.env) {
if (!ctx.githubRef.startsWith("refs/heads/")) {
fail("tag-and-dispatch must be dispatched from a branch");
}
if (!ctx.prerelease && ctx.githubRef !== "refs/heads/main") {
fail("stable releases must be dispatched from refs/heads/main");
}
if (!ctx.githubRepository) {
fail("GITHUB_REPOSITORY must be set");
}

assertCheckoutSha(ctx);
if (!ctx.prerelease) {
assertOriginMainSha(ctx);
const isCustomRef = Boolean(ctx.refInput) && ctx.refInput !== "main";
const commitSha = isCustomRef
? read("git", ["rev-parse", "HEAD"])
: ctx.githubSha;

if (!isCustomRef) {
assertCheckoutSha(ctx);
if (!ctx.prerelease) {
assertOriginMainSha(ctx);
}
}
assertLocalTagDoesNotExist(ctx.tag);
if (remoteTagExists(ctx.tag)) {
fail(`tag ${ctx.tag} already exists on origin`);
}

run("git", ["tag", ctx.tag, ctx.githubSha]);
run("git", ["tag", ctx.tag, commitSha]);
run("git", ["push", "origin", `refs/tags/${ctx.tag}`]);
run("gh", [
"workflow",
Expand All @@ -217,7 +233,9 @@ function tagAndDispatch(env = process.env) {
function assertDispatchCheckout(env = process.env) {
const ctx = validateDispatch(env);

assertCheckoutSha(ctx);
if (!ctx.refInput || ctx.refInput === "main") {
assertCheckoutSha(ctx);
}
return ctx;
}

Expand Down Expand Up @@ -309,6 +327,7 @@ if (require.main === module) {

module.exports = {
releaseContext,
effectiveRef,
validateDispatch,
latestTag,
resolveVersion,
Expand Down
53 changes: 52 additions & 1 deletion .github/scripts/release-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const assert = require("assert");
const { normalizeVersion, bumpVersion } = require("./release-version");
const { validateDispatch, resolveVersion } = require("./release-workflow");
const { effectiveRef, validateDispatch, resolveVersion } = require("./release-workflow");

function env(overrides) {
return {
Expand Down Expand Up @@ -130,6 +130,57 @@ assert.throws(
/recovery must be dispatched/
);

// effectiveRef: returns refInput when set, otherwise branch from githubRef
assert.strictEqual(
effectiveRef({ refInput: "my-branch", githubRef: "refs/heads/main" }),
"my-branch"
);
assert.strictEqual(
effectiveRef({ refInput: "", githubRef: "refs/heads/main" }),
"main"
);
assert.strictEqual(
effectiveRef({ refInput: "", githubRef: "refs/tags/v1.2.3" }),
null
);

// ref input: stable version + non-main ref must fail
assert.throws(
() => validateDispatch(env({ REF_INPUT: "my-branch" })),
/stable releases require ref 'main'/
);
// ref input: stable version + main ref is fine
assert.doesNotThrow(() =>
validateDispatch(env({ REF_INPUT: "main" }))
);
// ref input: stable version + empty ref (default) from main is fine
assert.doesNotThrow(() =>
validateDispatch(env({ REF_INPUT: "" }))
);
// ref input: prerelease + non-main ref is fine
assert.doesNotThrow(() =>
validateDispatch(env({
VERSION_INPUT: "1.2.3-alpha.1",
REF_INPUT: "my-branch",
}))
);
// dispatch from non-main branch (via dropdown) without ref input: stable version must fail
assert.throws(
() => validateDispatch(env({
GITHUB_REF: "refs/heads/feature-branch",
REF_INPUT: "",
})),
/stable releases require ref 'main'/
);
// dispatch from non-main branch (via dropdown) with prerelease: fine
assert.doesNotThrow(() =>
validateDispatch(env({
VERSION_INPUT: "1.2.3-alpha.1",
GITHUB_REF: "refs/heads/feature-branch",
REF_INPUT: "",
}))
);

// bumpVersion: level handling and v-prefix tolerance
assert.strictEqual(bumpVersion("0.0.11", "patch"), "0.0.12");
assert.strictEqual(bumpVersion("v0.0.11", "patch"), "0.0.12");
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Release

run-name: >-
${{ inputs.dry_run && 'Release dry-run' || 'Release' }}
${{ inputs.dry_run && 'Release dry-run' || 'Release' }}${{ inputs.version && format(' {0}', inputs.version) || '' }}${{ inputs.ref && format(' (ref: {0})', inputs.ref) || '' }}

on:
workflow_dispatch:
Expand All @@ -11,6 +11,11 @@ on:
required: false
default: ""
type: string
ref:
description: "Branch to release from (default: dispatch branch). Non-main branches require a prerelease version."
required: false
default: ""
type: string
bump:
description: "Bump level when version is blank"
required: false
Expand Down Expand Up @@ -49,7 +54,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ github.sha }}
ref: ${{ inputs.ref || github.sha }}

- name: Resolve release version
id: resolve
Expand All @@ -63,6 +68,7 @@ jobs:
VERSION_INPUT: ${{ steps.resolve.outputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
RESUME_EXISTING_NPM: ${{ inputs.resume_existing_npm }}
REF_INPUT: ${{ inputs.ref }}
run: node .github/scripts/release-workflow.js assert-dispatch-checkout

tag_and_dispatch:
Expand All @@ -81,14 +87,15 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ github.sha }}
ref: ${{ inputs.ref || github.sha }}

- name: Create tag and dispatch release run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION_INPUT: ${{ needs.validate_dispatch.outputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
RESUME_EXISTING_NPM: ${{ inputs.resume_existing_npm }}
REF_INPUT: ${{ inputs.ref }}
run: node .github/scripts/release-workflow.js tag-and-dispatch

dry-run:
Expand All @@ -101,13 +108,14 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ github.sha }}
ref: ${{ inputs.ref || github.sha }}

- name: Assert checkout ref
env:
VERSION_INPUT: ${{ needs.validate_dispatch.outputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
RESUME_EXISTING_NPM: ${{ inputs.resume_existing_npm }}
REF_INPUT: ${{ inputs.ref }}
run: node .github/scripts/release-workflow.js assert-dispatch-checkout

- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
Expand Down