name: Releasing PR on: pull_request_target: types: [labeled, opened, synchronize, reopened, closed] concurrency: group: pull-requests-release-${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: verify: name: Test Release runs-on: [self-hosted] permissions: contents: read packages: write # Run only when the PR carries the "release" label and not closed. if: | contains(github.event.pull_request.labels.*.name, 'release') && github.event.action != 'closed' steps: - name: Checkout code (PR branch) uses: actions/checkout@v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 fetch-tags: true - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} registry: ghcr.io - name: Run tests run: make test 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 merged commit (default ref -> merge SHA) - 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: 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}`; try { await github.rest.repos.getBranch({ owner: context.repo.owner, repo: context.repo.repo, branch }); console.log(`Branch '${branch}' already exists`); } catch (_) { await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/heads/${branch}`, sha: context.sha }); console.log(`✅ Branch '${branch}' created at ${context.sha}`); } # 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-rc1 const m = tag.match(/^v(\d+\.\d+\.\d+)(-rc\d+)?$/); if (!m) { core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-rcN'`); return; } const version = m[1] + (m[2] ?? ''); // 0.31.5‑rc1 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}`);