Manual Release #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |