Repository navigation
fix(ci): keep Surge preview comment current #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jp-ayyappan
wants to merge
3
commits into
main
Choose a base branch
from
fix/surge-preview-status-comment
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| const marker = "<!-- opentdf-surge-preview-status"; | ||
| const versionedMarker = /<!-- opentdf-surge-preview-status:(\d+):(\d+) -->/; | ||
|
|
||
| /** Identify comments owned by this workflow, including its older comment formats. */ | ||
| function isPreviewComment(comment) { | ||
| if (comment.user?.login !== "github-actions[bot]") { | ||
| return false; | ||
| } | ||
|
|
||
| const body = comment.body || ""; | ||
| return body.includes(marker) || | ||
| body.startsWith("❌ Surge preview build failed") || | ||
| body.startsWith("📄 Preview deployed to https://opentdf-docs-pr-"); | ||
| } | ||
|
|
||
| /** Format the current result for the PR timeline. */ | ||
| function statusBody(status, previewUrl, runUrl) { | ||
| switch (status) { | ||
| case "deployed": | ||
| return `📄 Preview deployed to ${previewUrl}`; | ||
| case "build-failed": | ||
| return `❌ Surge preview build failed. The preview was not updated. [Build logs](${runUrl})`; | ||
| case "deploy-failed": | ||
| return `❌ Surge preview deployment failed. [Workflow logs](${runUrl})`; | ||
| case "removed": | ||
| return "🧹 Surge preview removed."; | ||
| case "teardown-failed": | ||
| return `❌ Surge preview teardown failed. [Workflow logs](${runUrl})`; | ||
| default: | ||
| throw new Error(`Unknown preview status: ${status}`); | ||
| } | ||
| } | ||
|
|
||
| /** Order runs by the build start time, then by run ID when starts share a second. */ | ||
| function sourceOrder(context, sourceTime, sourceRunId) { | ||
| const time = Date.parse(sourceTime); | ||
| const runId = Number(sourceRunId ?? context.runId); | ||
| if (!Number.isSafeInteger(time) || !Number.isSafeInteger(runId) || runId < 1) { | ||
| throw new Error("Invalid preview run timestamp or ID"); | ||
| } | ||
| return { time, runId }; | ||
| } | ||
|
|
||
| /** Read the run order recorded in a managed comment; older comments have none. */ | ||
| function commentOrder(comment) { | ||
| const match = comment.body?.match(versionedMarker); | ||
| return match ? { time: Number(match[1]), runId: Number(match[2]) } : undefined; | ||
| } | ||
|
|
||
| /** Compare two run orders. */ | ||
| function compareOrder(left, right) { | ||
| return left.time - right.time || left.runId - right.runId; | ||
| } | ||
|
|
||
| /** Find the newest managed comment, falling back to the last legacy comment. */ | ||
| function currentComment(comments) { | ||
| const versioned = comments.filter((comment) => commentOrder(comment)); | ||
| return versioned.reduce((current, comment) => | ||
| compareOrder(commentOrder(comment), commentOrder(current)) > 0 ? comment : current, | ||
| versioned[0], | ||
| ) || comments[comments.length - 1]; | ||
| } | ||
|
|
||
| /** List preview status comments on a PR. */ | ||
| async function previewComments(github, context, prNumber) { | ||
| if (!Number.isSafeInteger(prNumber) || prNumber < 1) { | ||
| throw new Error(`Invalid PR number: ${prNumber}`); | ||
| } | ||
| const comments = await github.paginate(github.rest.issues.listComments, { | ||
| ...context.repo, | ||
| issue_number: prNumber, | ||
| per_page: 100, | ||
| }); | ||
| return comments.filter(isPreviewComment); | ||
| } | ||
|
|
||
| /** Skip a build that started before the status already displayed on this PR. */ | ||
| async function isStalePreviewRun({ github, context, prNumber, sourceTime, sourceRunId }) { | ||
| const order = sourceOrder(context, sourceTime, sourceRunId); | ||
| const current = currentComment(await previewComments(github, context, prNumber)); | ||
| const existingOrder = current && commentOrder(current); | ||
| return Boolean(existingOrder && compareOrder(order, existingOrder) < 0); | ||
| } | ||
|
|
||
| /** Create or update one preview status comment and remove older duplicates. */ | ||
| async function updatePreviewComment({ github, context, prNumber, status, runUrl, sourceTime, sourceRunId }) { | ||
| const order = sourceOrder(context, sourceTime, sourceRunId); | ||
| const { owner, repo } = context.repo; | ||
| const issue_number = prNumber; | ||
| const previewUrl = `https://opentdf-docs-pr-${prNumber}.surge.sh`; | ||
| const workflowUrl = runUrl || `${process.env.GITHUB_SERVER_URL || "https://github.com"}/${owner}/${repo}/actions/runs/${context.runId}`; | ||
| const body = `${marker}:${order.time}:${order.runId} -->\n${statusBody(status, previewUrl, workflowUrl)}`; | ||
| const comments = await previewComments(github, context, prNumber); | ||
| const current = currentComment(comments); | ||
| const existingOrder = current && commentOrder(current); | ||
|
|
||
| if (existingOrder && compareOrder(order, existingOrder) < 0) { | ||
| return; | ||
| } | ||
|
|
||
| if (current) { | ||
| if (current.body !== body) { | ||
| await github.rest.issues.updateComment({ owner, repo, comment_id: current.id, body }); | ||
| } | ||
| } else { | ||
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | ||
| } | ||
|
|
||
| for (const comment of comments) { | ||
| if (comment.id !== current?.id) { | ||
| await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| module.exports = { isStalePreviewRun, updatePreviewComment }; | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| const assert = require("node:assert/strict"); | ||
| const test = require("node:test"); | ||
| const { isStalePreviewRun, updatePreviewComment } = require("./preview-status.cjs"); | ||
| const sourceTime = "2026-10-06T15:00:00Z"; | ||
|
|
||
| function fixture(comments = []) { | ||
| const calls = []; | ||
| const context = { repo: { owner: "opentdf", repo: "docs" }, runId: 123 }; | ||
| const issues = { | ||
| listComments() {}, | ||
| async createComment({ body }) { | ||
| calls.push(["create", body]); | ||
| comments.push({ id: 1000, body, user: { login: "github-actions[bot]" } }); | ||
| }, | ||
| async updateComment({ comment_id, body }) { | ||
| calls.push(["update", comment_id, body]); | ||
| comments.find((comment) => comment.id === comment_id).body = body; | ||
| }, | ||
| async deleteComment({ comment_id }) { | ||
| calls.push(["delete", comment_id]); | ||
| comments.splice(comments.findIndex((comment) => comment.id === comment_id), 1); | ||
| }, | ||
| }; | ||
| const github = { | ||
| rest: { issues }, | ||
| async paginate(method, options) { | ||
| assert.equal(method, issues.listComments); | ||
| assert.equal(options.issue_number, 398); | ||
| return comments; | ||
| }, | ||
| }; | ||
|
|
||
| return { calls, comments, context, github }; | ||
| } | ||
|
|
||
| test("a successful build replaces a legacy failure comment", async () => { | ||
| const state = fixture([{ | ||
| id: 42, | ||
| body: "❌ Surge preview build failed — no preview was deployed.", | ||
| user: { login: "github-actions[bot]" }, | ||
| }]); | ||
|
|
||
| await updatePreviewComment({ | ||
| github: state.github, | ||
| context: state.context, | ||
| prNumber: 398, | ||
| status: "deployed", | ||
| sourceTime, | ||
| }); | ||
|
|
||
| assert.deepEqual(state.calls.map((call) => call.slice(0, 2)), [["update", 42]]); | ||
| assert.match(state.comments[0].body, /Preview deployed to https:\/\/opentdf-docs-pr-398\.surge\.sh/); | ||
| }); | ||
|
|
||
| test("duplicate preview comments are removed without touching other comments", async () => { | ||
| const state = fixture([ | ||
| { id: 1, body: "📄 Preview deployed to https://opentdf-docs-pr-398.surge.sh", user: { login: "github-actions[bot]" } }, | ||
| { id: 2, body: "❌ Surge preview build failed", user: { login: "github-actions[bot]" } }, | ||
| { id: 3, body: "<!-- opentdf-surge-preview-status -->\n❌ Surge preview build failed", user: { login: "github-actions[bot]" } }, | ||
| { id: 4, body: "❌ Surge preview build failed", user: { login: "reviewer" } }, | ||
| ]); | ||
|
|
||
| await updatePreviewComment({ | ||
| github: state.github, | ||
| context: state.context, | ||
| prNumber: 398, | ||
| status: "deployed", | ||
| sourceTime, | ||
| }); | ||
|
|
||
| assert.deepEqual(state.calls.map((call) => call.slice(0, 2)), [ | ||
| ["update", 3], ["delete", 1], ["delete", 2], | ||
| ]); | ||
| assert.deepEqual(state.comments.map((comment) => comment.id), [3, 4]); | ||
| }); | ||
|
|
||
| test("new status comments are reused for later runs", async () => { | ||
| const state = fixture(); | ||
| const args = { github: state.github, context: state.context, prNumber: 398, sourceTime }; | ||
|
|
||
| await updatePreviewComment({ ...args, status: "build-failed", runUrl: "https://github.com/opentdf/docs/actions/runs/7" }); | ||
| await updatePreviewComment({ ...args, status: "build-failed", runUrl: "https://github.com/opentdf/docs/actions/runs/7" }); | ||
| await updatePreviewComment({ ...args, status: "deployed" }); | ||
|
|
||
| assert.deepEqual(state.calls.map((call) => call[0]), ["create", "update"]); | ||
| assert.match(state.calls[0][1], /actions\/runs\/7/); | ||
| assert.equal(state.comments.length, 1); | ||
| }); | ||
|
|
||
| test("an older run cannot replace a newer status", async () => { | ||
| const state = fixture(); | ||
| const args = { github: state.github, context: state.context, prNumber: 398 }; | ||
|
|
||
| await updatePreviewComment({ ...args, status: "deployed", sourceTime: "2026-10-06T16:00:00Z", sourceRunId: 200 }); | ||
| const stale = await isStalePreviewRun({ ...args, sourceTime: "2026-10-06T15:00:00Z", sourceRunId: 100 }); | ||
| const sameSecondOlderRun = await isStalePreviewRun({ ...args, sourceTime: "2026-10-06T16:00:00Z", sourceRunId: 199 }); | ||
| await updatePreviewComment({ ...args, status: "build-failed", sourceTime: "2026-10-06T15:00:00Z", sourceRunId: 100 }); | ||
|
|
||
| assert.equal(stale, true); | ||
| assert.equal(sameSecondOlderRun, true); | ||
| assert.deepEqual(state.calls.map((call) => call[0]), ["create"]); | ||
| assert.match(state.comments[0].body, /Preview deployed/); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.