Skip to content

Manual Release

Manual Release #31

Workflow file for this run

name: Manual Release
on:
workflow_dispatch:
inputs:
version:
description: Version number (e.g. 4.2.1)
required: true
type: string
release_notes:
description: "Optional release notes (fallback: auto-generated notes)"
required: false
type: string
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache-dependency-path: package-lock.json
cache: npm
- name: Install dependencies
run: npm ci
- name: Normalize and validate version
id: version
shell: bash
run: |
RAW_VERSION="${{ github.event.inputs.version }}"
NORMALIZED="${RAW_VERSION#v}"
TAG="v$NORMALIZED"
if [[ ! "$NORMALIZED" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid version format: $RAW_VERSION"
echo "Expected format: 1.2.3 or 1.2.3-beta.1"
exit 1
fi
CURRENT_VERSION="$(node -p "require('./package.json').version")"
IS_GREATER="$(node - "$NORMALIZED" "$CURRENT_VERSION" <<'NODE'
const [nextVersion, currentVersion] = process.argv.slice(2);
function parseSemver(version) {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
if (!match) return null;
return {
major: Number.parseInt(match[1], 10),
minor: Number.parseInt(match[2], 10),
patch: Number.parseInt(match[3], 10),
prerelease: match[4] ? match[4].split('.') : [],
};
}
function comparePrereleaseIdentifier(left, right) {
const leftNumeric = /^\d+$/.test(left);
const rightNumeric = /^\d+$/.test(right);
if (leftNumeric && rightNumeric) {
return Number.parseInt(left, 10) - Number.parseInt(right, 10);
}
if (leftNumeric && !rightNumeric) return -1;
if (!leftNumeric && rightNumeric) return 1;
if (left < right) return -1;
if (left > right) return 1;
return 0;
}
function compareSemver(left, right) {
if (left.major !== right.major) return left.major - right.major;
if (left.minor !== right.minor) return left.minor - right.minor;
if (left.patch !== right.patch) return left.patch - right.patch;
const leftPrerelease = left.prerelease;
const rightPrerelease = right.prerelease;
if (leftPrerelease.length === 0 && rightPrerelease.length === 0) return 0;
if (leftPrerelease.length === 0) return 1;
if (rightPrerelease.length === 0) return -1;
const maxLength = Math.max(leftPrerelease.length, rightPrerelease.length);
for (let index = 0; index < maxLength; index += 1) {
if (index >= leftPrerelease.length) return -1;
if (index >= rightPrerelease.length) return 1;
const comparison = comparePrereleaseIdentifier(
leftPrerelease[index],
rightPrerelease[index],
);
if (comparison !== 0) return comparison;
}
return 0;
}
const parsedNext = parseSemver(nextVersion);
const parsedCurrent = parseSemver(currentVersion);
if (!parsedNext || !parsedCurrent) {
process.exit(2);
}
process.stdout.write(compareSemver(parsedNext, parsedCurrent) > 0 ? 'true' : 'false');
NODE
)"
if [ "$IS_GREATER" != "true" ]; then
echo "Version must be greater than current package.json version ($CURRENT_VERSION)."
exit 1
fi
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag already exists locally: $TAG"
exit 1
fi
if git ls-remote --tags --exit-code origin "refs/tags/$TAG" >/dev/null; then
echo "Tag already exists on origin: $TAG"
exit 1
fi
echo "version=$NORMALIZED" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Bump package version without creating tag
run: npm version "${{ steps.version.outputs.version }}" --no-git-tag-version
- name: Typecheck and linting
run: npm run lint && npm run typecheck
- name: Build zipped artifacts (Chromium + Firefox)
run: |
npm run zip
npm run zip:firefox
- name: Validate zip artifacts
id: artifacts
shell: bash
run: |
mapfile -t CHROME_ZIPS < <(find .output -maxdepth 1 -type f -name "*-chrome.zip" | sort)
mapfile -t FIREFOX_ZIPS < <(find .output -maxdepth 1 -type f -name "*-firefox.zip" | sort)
if [ "${#CHROME_ZIPS[@]}" -ne 1 ] || [ "${#FIREFOX_ZIPS[@]}" -ne 1 ]; then
echo "Expected exactly 1 Chrome zip and 1 Firefox zip in .output/."
echo "Chrome zips found: ${#CHROME_ZIPS[@]}"
printf '%s\n' "${CHROME_ZIPS[@]}"
echo "Firefox zips found: ${#FIREFOX_ZIPS[@]}"
printf '%s\n' "${FIREFOX_ZIPS[@]}"
echo "All zip files in .output/:"
find .output -maxdepth 1 -type f -name "*.zip" | sort
exit 1
fi
echo "zip_1=${CHROME_ZIPS[0]}" >> "$GITHUB_OUTPUT"
echo "zip_2=${FIREFOX_ZIPS[0]}" >> "$GITHUB_OUTPUT"
- name: Commit version changes
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json
if git diff --cached --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(release): bump version to ${{ steps.version.outputs.version }}"
fi
- name: Push commit
shell: bash
run: git push origin "HEAD:${GITHUB_REF_NAME}"
- name: Create and push tag
shell: bash
run: |
git tag "${{ steps.version.outputs.tag }}"
git push origin "${{ steps.version.outputs.tag }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
generate_release_notes: ${{ github.event.inputs.release_notes == '' }}
body: ${{ github.event.inputs.release_notes }}
files: |
${{ steps.artifacts.outputs.zip_1 }}
${{ steps.artifacts.outputs.zip_2 }}