Add GitHub Action to post web flasher link comments on successful PR workflows
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
name: Post Web Flasher Link Comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [CI]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
post-flasher-link:
|
||||||
|
if: >
|
||||||
|
github.event.workflow_run.event == 'pull_request' &&
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.repository == 'meshtastic/firmware'
|
||||||
|
continue-on-error: true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Post or update web flasher link comment
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const marker = '<!-- web-flasher-link -->';
|
||||||
|
const run = context.payload.workflow_run;
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
|
// Resolve the PR number (run.pull_requests is empty for fork PRs)
|
||||||
|
let prNumber = run.pull_requests?.[0]?.number;
|
||||||
|
if (!prNumber) {
|
||||||
|
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||||
|
owner, repo, commit_sha: run.head_sha,
|
||||||
|
});
|
||||||
|
prNumber = (prs.find((pr) => pr.head.sha === run.head_sha) ?? prs[0])?.number;
|
||||||
|
}
|
||||||
|
if (!prNumber) {
|
||||||
|
core.info('No pull request associated with this run; skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||||
|
|
||||||
|
// Only comment on PRs from contributors and members
|
||||||
|
const allowedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
||||||
|
if (!allowedAssociations.includes(pr.author_association)) {
|
||||||
|
core.info(`Author association ${pr.author_association} is not eligible; skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pr.state !== 'open') {
|
||||||
|
core.info('Pull request is not open; skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pr.head.sha !== run.head_sha) {
|
||||||
|
core.info('Run is for an outdated commit; skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require at least one per-arch firmware artifact from gather-artifacts
|
||||||
|
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||||
|
owner, repo, run_id: run.id, per_page: 100,
|
||||||
|
});
|
||||||
|
const archRe = /^firmware-(esp32|esp32s3|esp32c3|esp32c6|nrf52840|rp2040|rp2350|stm32)-(\d+\.\d+\.\d+\.[0-9a-f]+)$/;
|
||||||
|
const archArtifacts = artifacts.filter((a) => archRe.test(a.name) && !a.expired);
|
||||||
|
if (archArtifacts.length === 0) {
|
||||||
|
core.info('No per-arch firmware artifacts found; skipping.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = archRe.exec(archArtifacts[0].name)[2];
|
||||||
|
const expiresAt = archArtifacts[0].expires_at
|
||||||
|
? new Date(archArtifacts[0].expires_at).toISOString().slice(0, 10)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Per-board deep links from manifest-{platform}-{board}-{version} artifacts
|
||||||
|
const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const boardRe = new RegExp(`^manifest-([a-z0-9]+)-(.+)-${escapedVersion}$`);
|
||||||
|
const boards = artifacts
|
||||||
|
.map((a) => boardRe.exec(a.name))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((m) => ({ platform: m[1], board: m[2] }))
|
||||||
|
.sort((a, b) => a.board.localeCompare(b.board));
|
||||||
|
|
||||||
|
const flasherUrl = `https://flasher.meshtastic.org/?pr=${prNumber}`;
|
||||||
|
const boardLines = boards
|
||||||
|
.map((b) => `| [\`${b.board}\`](${flasherUrl}&device=${encodeURIComponent(b.board)}) | ${b.platform} |`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const body = [
|
||||||
|
marker,
|
||||||
|
'## 🔦 Try this PR in the Web Flasher',
|
||||||
|
'',
|
||||||
|
`[**Flash firmware \`${version}\`** (commit \`${run.head_sha.slice(0, 7)}\`)](${flasherUrl})`,
|
||||||
|
'',
|
||||||
|
'> [!WARNING]',
|
||||||
|
'> This is an automated, unreviewed CI test build. Back up your device configuration',
|
||||||
|
'> before flashing, and only flash devices you are able to recover.',
|
||||||
|
'',
|
||||||
|
`<details><summary>Boards built by this PR (${boards.length})</summary>`,
|
||||||
|
'',
|
||||||
|
'| Board | Platform |',
|
||||||
|
'| --- | --- |',
|
||||||
|
boardLines,
|
||||||
|
'',
|
||||||
|
'</details>',
|
||||||
|
'',
|
||||||
|
`*Build artifacts expire${expiresAt ? ` on ${expiresAt}` : ' after 30 days'}. Updated for ${run.head_sha}.*`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Sticky comment: update in place when the marker is found
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||||
|
owner, repo, issue_number: prNumber, per_page: 100,
|
||||||
|
});
|
||||||
|
const existing = comments.find((c) => c.body?.includes(marker));
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user