name: "Releasing PR" on: pull_request: types: [closed] paths-ignore: - 'docs/**/*' # Cancel in‑flight runs for the same PR when a new push arrives. concurrency: group: pr-${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: finalize: name: Finalize Release runs-on: [self-hosted] permissions: contents: write if: | github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') steps: # Extract tag from branch name (branch = release-X.Y.Z*) - name: Extract tag from branch name id: get_tag uses: actions/github-script@v7 with: script: | const branch = context.payload.pull_request.head.ref; const m = branch.match(/^release-(\d+\.\d+\.\d+(?:[-\w\.]+)?)$/); if (!m) { core.setFailed(`Branch '${branch}' does not match 'release-X.Y.Z[-suffix]'`); return; } const tag = `v${m[1]}`; core.setOutput('tag', tag); console.log(`βœ… Tag to publish: ${tag}`); # Checkout repo & create / push annotated tag - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Create tag on merge commit run: | git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }} git push -f origin ${{ steps.get_tag.outputs.tag }} # Ensure maintenance branch release-X.Y - name: Ensure maintenance branch release-X.Y uses: actions/github-script@v7 with: github-token: ${{ secrets.GH_PAT }} script: | const tag = '${{ steps.get_tag.outputs.tag }}'; // e.g. v0.1.3 or v0.1.3-rc3 const match = tag.match(/^v(\d+)\.(\d+)\.\d+(?:[-\w\.]+)?$/); if (!match) { core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-suffix'`); return; } const line = `${match[1]}.${match[2]}`; const branch = `release-${line}`; // Get main branch commit for the tag const ref = await github.rest.git.getRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `tags/${tag}` }); const commitSha = ref.data.object.sha; try { await github.rest.repos.getBranch({ owner: context.repo.owner, repo: context.repo.repo, branch }); await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `heads/${branch}`, sha: commitSha, force: true }); console.log(`πŸ” Force-updated '${branch}' to ${commitSha}`); } catch (err) { if (err.status === 404) { await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/heads/${branch}`, sha: commitSha }); console.log(`βœ… Created branch '${branch}' at ${commitSha}`); } else { console.error('Unexpected error --', err); core.setFailed(`Unexpected error creating/updating branch: ${err.message}`); throw err; } } # Get the latest published release - name: Get the latest published release id: latest_release uses: actions/github-script@v7 with: script: | try { const rel = await github.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo }); core.setOutput('tag', rel.data.tag_name); } catch (_) { core.setOutput('tag', ''); } # Compare current tag vs latest using semver-utils - name: Semver compare id: semver uses: madhead/semver-utils@v4.3.0 with: version: ${{ steps.get_tag.outputs.tag }} compare-to: ${{ steps.latest_release.outputs.tag }} # Derive flags: prerelease? make_latest? - name: Calculate publish flags id: flags uses: actions/github-script@v7 with: script: | const tag = '${{ steps.get_tag.outputs.tag }}'; // v0.31.5-rc.1 const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/); if (!m) { core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`); return; } const version = m[1] + (m[2] ?? ''); // 0.31.5-rc.1 const isRc = Boolean(m[2]); core.setOutput('is_rc', isRc); const outdated = '${{ steps.semver.outputs.comparison-result }}' === '<'; core.setOutput('make_latest', isRc || outdated ? 'false' : 'legacy'); # Publish draft release with correct flags - name: Publish draft release uses: actions/github-script@v7 with: script: | const tag = '${{ steps.get_tag.outputs.tag }}'; const releases = await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo }); const draft = releases.data.find(r => r.tag_name === tag && r.draft); if (!draft) throw new Error(`Draft release for ${tag} not found`); await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: draft.id, draft: false, prerelease: ${{ steps.flags.outputs.is_rc }}, make_latest: '${{ steps.flags.outputs.make_latest }}' }); console.log(`πŸš€ Published release for ${tag}`);