From 635c3727bbed092e7bb049ddd1506cbdbfe4fbe2 Mon Sep 17 00:00:00 2001 From: Bart Louwers Date: Wed, 16 Jul 2025 16:07:54 +0200 Subject: [PATCH] Write release notification comments to PRs (#3622) --- .github/scripts/notify-release-on-prs.ts | 155 ++++++++++++++++++++++ .github/workflows/android-device-test.yml | 2 + .github/workflows/android-release.yml | 13 ++ .github/workflows/ios-ci.yml | 17 ++- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/notify-release-on-prs.ts diff --git a/.github/scripts/notify-release-on-prs.ts b/.github/scripts/notify-release-on-prs.ts new file mode 100644 index 000000000..991cd0fcc --- /dev/null +++ b/.github/scripts/notify-release-on-prs.ts @@ -0,0 +1,155 @@ +import { Octokit } from '@octokit/rest'; +import { parseArgs } from 'node:util'; +import * as core from "@actions/core"; + +const { values } = parseArgs({ + options: { + tag: { type: 'string', short: 't' } + }, + allowPositionals: false, + strict: true +}); + +function errMessage(err: unknown) { + return err instanceof Error ? err.message : `${err}`; +} + +const owner = 'maplibre'; +const repo = 'maplibre-native'; + +function extractPrNumbers(params: { + releaseBody: string; + owner: string; + repo: string; +}): Set { + const { releaseBody, owner, repo } = params; + const prs = new Set(); + + const esc = (s: string) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); + const urlRegex = new RegExp( + `https?:\\/\\/github\\.com\\/${esc(owner)}\\/${esc(repo)}\\/pull\\/(\\d+)`, + 'g' + ); + + for (const match of releaseBody.matchAll(urlRegex)) { + prs.add(Number(match[1])); + } + + return prs; +} + +async function createOrUpdateReleaseNotification({ + octokit, + prNumber, + tag, + owner, + repo +}: { + owner: string, + repo: string, + octokit: Octokit, + prNumber: number, + tag: string +}) { + + const m = tag.match(/^([A-Za-z0-9]+)-v(.+)$/i); + const platform = m?.[1]?.toLowerCase(); + const version = m?.[2] ?? ''; + + if (!platform || !version) { + throw new Error(`Failed to parse platform or version from ${tag}`); + } + + const MARKER = ``; + + function linkText() { + if (platform === 'ios') { + return `MapLibre iOS ${version}`; + } else if (platform === 'android') { + return `MapLibre Android ${version}`; + } + return tag; + } + + const { data: comments } = await octokit.issues.listComments({ + owner, + repo, + issue_number: prNumber + }); + + const existing = comments.find((({ body }) => body?.includes(MARKER))); + + const releaseUrl = `https://github.com/${owner}/${repo}/releases/tag/${tag}`; + releaseUrl + const body = ` +${MARKER} +🚀 This PR has been included in the [${linkText()}](${releaseUrl}) release. +`.trim(); + + if (existing) { + await octokit.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body + }); + console.log(`Updated comment on PR #${prNumber}`); + } else { + await octokit.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body + }); + console.log(`Created comment on PR #${prNumber}`); + } +} + +try { + const { tag } = values; + + if (!tag) { + throw new Error("pass tag with --tag"); + } + + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN environment variable is required'); + } + + const octokit = new Octokit({ auth: token }); + + const release = await octokit.repos.getReleaseByTag({ owner, repo, tag }); + const releaseBody = release.data.body || ''; + + if (!releaseBody) { + throw new Error(`No release notes found for tag '${tag}'.`); + } + + const prs = extractPrNumbers({ + owner, + releaseBody, + repo + }); + + if (prs.size === 0) { + console.log(`No PR references found in release notes for tag '${tag}'.`); + process.exit(0); + } + + for (const prNumber of prs) { + try { + await createOrUpdateReleaseNotification({ + octokit, + owner, + prNumber, + repo, + tag + }); + } catch (err) { + console.error(`Failed to comment on PR #${prNumber}: ${errMessage(err)}`); + } + } +} catch (err) { + core.setFailed(errMessage(err)); +} diff --git a/.github/workflows/android-device-test.yml b/.github/workflows/android-device-test.yml index 545776177..342156858 100644 --- a/.github/workflows/android-device-test.yml +++ b/.github/workflows/android-device-test.yml @@ -84,6 +84,8 @@ jobs: with: node-version-file: '.nvmrc' + - run: npm install + - name: Check if android-build was skipped id: parent_workflow run: node .github/scripts/check-job-skipped.ts diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 9ed48c732..617ca0c63 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -83,6 +83,10 @@ jobs: submodules: recursive fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + - uses: actions/setup-java@v4 with: distribution: "temurin" @@ -151,3 +155,12 @@ jobs: SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + + - run: npm install + working-directory: . + + - name: Write release notifications + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/scripts/notify-release-on-prs.ts --tag ${{ needs.android-create-release.outputs.version_tag }} + working-directory: . diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index f30867325..9fb1d863b 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -29,6 +29,7 @@ on: permissions: id-token: write # needed for AWS contents: write # allow making a release + actions: write # for triggering ios-release-cocoapods.yml jobs: pre_job: @@ -244,6 +245,11 @@ jobs: (github.event_name == 'push' && steps.version-file-ios-changed.outputs.any_changed == 'true') run: echo make_release=true >> "$GITHUB_ENV" + - uses: actions/setup-node@v4 + if: env.make_release + with: + node-version-file: ".nvmrc" + - name: Build XCFramework if: env.make_release run: | @@ -348,12 +354,21 @@ jobs: "version":"${{ env.version }}", "download_url":"${{ fromJSON(steps.github_release.outputs.assets)[0].browser_download_url }}"}}' + - run: npm install + working-directory: . + - name: Release (CocoaPods) shell: bash -leo pipefail {0} # so pod is found if: env.make_release run: gh workflow run ios-release-cocoapods.yml --field version=${{ env.version }} env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Write release notifications + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/scripts/notify-release-on-prs.ts --tag ios-v${{ env.version }} + working-directory: . ios-build-cmake: needs: pre_job