Merge pull request #87 from ferueda/release-force/1.4.1 #58
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: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: >- | |
| auto — default release-please behavior (publish when a release PR was merged). | |
| release-pr — open or refresh the release PR only; merge it yourself to publish. | |
| force — open a Release-As marker PR for dependency-only changes. | |
| required: true | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - release-pr | |
| - force | |
| bump: | |
| description: Version bump when mode is force | |
| required: false | |
| default: patch | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| push: | |
| branches: | |
| - main | |
| concurrency: ${{ github.workflow }}-${{ github.ref }} | |
| jobs: | |
| release: | |
| name: Release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # to create release tags/commits | |
| pull-requests: write # to create the "Version Packages" PR | |
| id-token: write # required for NPM provenance | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - run: corepack enable | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Resolve release mode | |
| id: mode | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "value=${{ inputs.mode }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "value=auto" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Resolve forced release version | |
| if: ${{ steps.mode.outputs.value == 'force' }} | |
| id: force | |
| env: | |
| BUMP: ${{ inputs.bump || 'patch' }} | |
| run: | | |
| NEXT=$(node --input-type=module <<'NODE' | |
| import { readFileSync } from "node:fs"; | |
| const bump = process.env.BUMP; | |
| const manifest = JSON.parse(readFileSync(".release-please-manifest.json", "utf8")); | |
| const versions = Object.values(manifest).map((version) => { | |
| const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); | |
| if (!match) throw new Error(`Unsupported manifest version: ${version}`); | |
| return match.slice(1).map(Number); | |
| }); | |
| const [major, minor, patch] = versions.reduce((max, version) => { | |
| for (let i = 0; i < 3; i += 1) { | |
| if (version[i] > max[i]) return version; | |
| if (version[i] < max[i]) return max; | |
| } | |
| return max; | |
| }); | |
| if (bump === "major") console.log(`${major + 1}.0.0`); | |
| else if (bump === "minor") console.log(`${major}.${minor + 1}.0`); | |
| else if (bump === "patch") console.log(`${major}.${minor}.${patch + 1}`); | |
| else throw new Error(`Unsupported bump: ${bump}`); | |
| NODE | |
| ) | |
| echo "version=$NEXT" >> "$GITHUB_OUTPUT" | |
| echo "Resolved forced release version $NEXT" | |
| - name: Open forced release marker PR | |
| if: ${{ steps.mode.outputs.value == 'force' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NEXT: ${{ steps.force.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| BRANCH="release-force/${NEXT}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then | |
| git fetch origin "$BRANCH" | |
| git checkout -B "$BRANCH" "origin/$BRANCH" | |
| else | |
| git checkout -B "$BRANCH" | |
| git commit --allow-empty -m "chore: force release ${NEXT}" -m "Release-As: ${NEXT}" | |
| git push --set-upstream origin "$BRANCH" | |
| fi | |
| BODY=$(cat <<EOF | |
| Opens a protected-branch compliant Release-As marker. | |
| After this PR is merged, the normal push workflow will open the release-please PR for ${NEXT}. Merge that release PR to publish to npm. | |
| EOF | |
| ) | |
| PR=$(gh pr list --head "$BRANCH" --base main --json number --jq '.[0].number') | |
| if [ -z "$PR" ] || [ "$PR" = "null" ]; then | |
| gh pr create --base main --head "$BRANCH" --title "chore: force release ${NEXT}" --body "$BODY" | |
| else | |
| gh pr edit "$PR" --title "chore: force release ${NEXT}" --body "$BODY" | |
| echo "Updated existing force release PR #$PR" | |
| fi | |
| - name: Open or refresh release PR | |
| if: ${{ steps.mode.outputs.value == 'release-pr' }} | |
| uses: googleapis/release-please-action@v5 | |
| id: release_pr | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| skip-github-release: true | |
| - name: Release Please | |
| if: ${{ steps.mode.outputs.value == 'auto' }} | |
| uses: googleapis/release-please-action@v5 | |
| id: release | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| - name: Install Dependencies | |
| if: ${{ steps.release.outputs.releases_created == 'true' }} | |
| run: pnpm install --frozen-lockfile | |
| - name: Verify Builds and Tests | |
| if: ${{ steps.release.outputs.releases_created == 'true' }} | |
| run: pnpm check | |
| - name: Build Workspace | |
| if: ${{ steps.release.outputs.releases_created == 'true' }} | |
| run: pnpm build | |
| - name: Publish to NPM | |
| if: ${{ steps.release.outputs.releases_created == 'true' }} | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| PATHS_RELEASED: ${{ steps.release.outputs.paths_released }} | |
| run: | | |
| set -euo pipefail | |
| node --input-type=module <<'NODE' > /tmp/released-packages | |
| const paths = JSON.parse(process.env.PATHS_RELEASED || "[]"); | |
| for (const path of paths) console.log(path); | |
| NODE | |
| if [ ! -s /tmp/released-packages ]; then | |
| echo "No released package paths reported by release-please" >&2 | |
| exit 1 | |
| fi | |
| while IFS= read -r path; do | |
| echo "Publishing ${path}" | |
| pnpm --filter "./${path}" publish --access public --no-git-checks | |
| done < /tmp/released-packages | |
| - name: Verify NPM dist-tags | |
| if: ${{ steps.release.outputs.releases_created == 'true' }} | |
| env: | |
| PATHS_RELEASED: ${{ steps.release.outputs.paths_released }} | |
| run: | | |
| node --input-type=module <<'NODE' | |
| import { execFileSync } from "node:child_process"; | |
| import { readFileSync } from "node:fs"; | |
| import { join } from "node:path"; | |
| const paths = JSON.parse(process.env.PATHS_RELEASED || "[]"); | |
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| for (const path of paths) { | |
| const pkg = JSON.parse(readFileSync(join(path, "package.json"), "utf8")); | |
| let latest = ""; | |
| for (let attempt = 1; attempt <= 6; attempt += 1) { | |
| latest = execFileSync("npm", ["view", `${pkg.name}@latest`, "version"], { | |
| encoding: "utf8", | |
| }).trim(); | |
| if (latest === pkg.version) break; | |
| await sleep(10000); | |
| } | |
| if (latest !== pkg.version) { | |
| throw new Error(`${pkg.name}@latest is ${latest}, expected ${pkg.version}`); | |
| } | |
| console.log(`${pkg.name}@latest -> ${latest}`); | |
| } | |
| NODE |