Fix hardcoded base URLs so PR previews load assets correctly #54
Workflow file for this run
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
| name: Stage Progression | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize, labeled, edited, review_requested, review_request_removed] | |
| pull_request_review: | |
| types: [submitted] | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| contents: read | |
| statuses: write | |
| concurrency: | |
| group: stage-progression-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| progress: | |
| name: Manage review stage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Run stage progression logic | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| if (!pr) return; | |
| const prNumber = pr.number; | |
| const eventName = context.eventName; | |
| const action = context.payload.action; | |
| // ── Constants ──────────────────────────────────────────── | |
| const LANE_LABELS = ['lane:new-doc', 'lane:major-revision', 'lane:minor-revision', 'lane:editorial-fix', 'lane:dev']; | |
| const STAGE_LABELS = ['stage:needs-lane', 'stage:peer-review', 'stage:lead-civil-review', 'stage:ai-editor-review', 'stage:director-review', 'stage:ready-to-merge']; | |
| const STAGE_DISPLAY = { | |
| 'stage:peer-review': 'Peer reviewer(s)', | |
| 'stage:lead-civil-review': 'Lead Civil reviewer(s)', | |
| 'stage:ai-editor-review': 'Technical editor(s)', | |
| 'stage:director-review': 'Director reviewer(s)', | |
| }; | |
| const ACTIVE_REVIEW_STAGES = Object.keys(STAGE_DISPLAY); | |
| const STAGE_STATUS_DESC = { | |
| 'stage:needs-lane': 'Awaiting lane assignment', | |
| 'stage:peer-review': 'Awaiting peer review', | |
| 'stage:lead-civil-review': 'Awaiting Lead Civil review', | |
| 'stage:ai-editor-review': 'Awaiting technical edit', | |
| 'stage:director-review': 'Awaiting Director review', | |
| 'stage:ready-to-merge': 'All reviews complete — ready to merge', | |
| }; | |
| // ── Re-fetch PR to get fresh labels/SHA/body ───────────── | |
| // Webhook payloads can be stale when events queue through the | |
| // concurrency block and prior runs have added labels or the | |
| // author has pushed new commits. | |
| const currentPr = (await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| })).data; | |
| const labels = currentPr.labels.map(l => l.name); | |
| const headSha = currentPr.head.sha; | |
| const branch = currentPr.head.ref; | |
| const prBody = currentPr.body || ''; | |
| const prAuthor = currentPr.user.login; | |
| const existingLane = labels.find(l => LANE_LABELS.includes(l)); | |
| const existingStage = labels.find(l => STAGE_LABELS.includes(l)); | |
| // ── Helpers ────────────────────────────────────────────── | |
| function detectLane(b) { | |
| if (b.startsWith('docs/new/')) return 'lane:new-doc'; | |
| if (b.startsWith('docs/major/')) return 'lane:major-revision'; | |
| if (b.startsWith('docs/minor/')) return 'lane:minor-revision'; | |
| if (b.startsWith('docs/fix/')) return 'lane:editorial-fix'; | |
| if (b.startsWith('docs/dev/')) return 'lane:dev'; | |
| return null; | |
| } | |
| async function addLabels(ls) { | |
| if (ls.length) await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: prNumber, labels: ls, | |
| }); | |
| } | |
| async function removeLabel(l) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: prNumber, name: l, | |
| }); | |
| } catch (e) {} | |
| } | |
| async function postComment(body) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: prNumber, body, | |
| }); | |
| } | |
| // Fetches the PR's file list once and returns both whether it | |
| // touches any docs/ files and whether every docs/ file is under | |
| // docs/dev/. The dev-lane detection is content-based so that | |
| // dev docs automatically use the lightweight lane regardless of | |
| // branch naming — users don't have to remember a docs/dev/ | |
| // branch prefix to get the dev-lane treatment. | |
| async function getDocsFileState() { | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| pull_number: prNumber, per_page: 100, | |
| }); | |
| const docsFiles = files.filter(f => f.filename.startsWith('docs/')); | |
| return { | |
| touchesDocs: docsFiles.length > 0, | |
| allUnderDev: docsFiles.length > 0 && docsFiles.every(f => f.filename.startsWith('docs/dev/')), | |
| }; | |
| } | |
| // ── Commit status helper ───────────────────────────────── | |
| // Sets the `review-workflow` commit status on the PR's head SHA. | |
| // This status is the merge gate — branch protection on main requires | |
| // it to be `success` before a PR can merge. The bot flips it to | |
| // `success` only when the PR reaches stage:ready-to-merge (or for | |
| // lane:editorial-fix, immediately on lane assignment). | |
| async function setReviewStatus(state, description) { | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| sha: headSha, | |
| state, | |
| context: 'review-workflow', | |
| description, | |
| }); | |
| } | |
| // ── Review-state comment helpers ───────────────────────── | |
| // A hidden bot comment on each PR tracks which individuals have | |
| // been assigned as reviewers for each active review stage. The | |
| // assignment is captured on `review_requested` webhook events | |
| // (triggered when an admin assigns someone via the Reviewers | |
| // sidebar). Approvals only advance the stage if the approver is | |
| // in the assigned list for the current stage — this is the | |
| // per-individual gating mechanism. | |
| const STATE_MARKER = '<!-- review-state -->'; | |
| const STATE_DATA_RE = /<!-- state-data:(.*?) -->/; | |
| function emptyState() { | |
| return { | |
| 'stage:peer-review': [], | |
| 'stage:lead-civil-review': [], | |
| 'stage:ai-editor-review': [], | |
| 'stage:director-review': [], | |
| }; | |
| } | |
| function renderStateComment(state) { | |
| const dataLine = `<!-- state-data:${JSON.stringify(state)} -->`; | |
| const lines = ['📋 **Assigned reviewers for this PR**', '']; | |
| for (const stage of ACTIVE_REVIEW_STAGES) { | |
| const users = state[stage] || []; | |
| const display = STAGE_DISPLAY[stage]; | |
| if (users.length === 0) { | |
| lines.push(`- ${display}: _not assigned_`); | |
| } else { | |
| lines.push(`- ${display}: ${users.map(u => '@' + u).join(', ')}`); | |
| } | |
| } | |
| lines.push(''); | |
| lines.push('_The first approval from any assigned reviewer at the current stage advances the PR. Approvals from non-assigned reviewers are logged but do not advance the stage._'); | |
| return `${STATE_MARKER}\n${dataLine}\n\n${lines.join('\n')}`; | |
| } | |
| async function findStateComment() { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: prNumber, per_page: 100, | |
| }); | |
| return comments.find(c => c.user.type === 'Bot' && c.body.includes(STATE_MARKER)); | |
| } | |
| async function readState() { | |
| const comment = await findStateComment(); | |
| if (!comment) return null; | |
| const match = comment.body.match(STATE_DATA_RE); | |
| if (!match) return null; | |
| try { return JSON.parse(match[1]); } catch (e) { return null; } | |
| } | |
| async function writeState(state) { | |
| const body = renderStateComment(state); | |
| const existing = await findStateComment(); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| comment_id: existing.id, body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| issue_number: prNumber, body, | |
| }); | |
| } | |
| } | |
| async function ensureState() { | |
| const s = await readState(); | |
| if (s) return s; | |
| const blank = emptyState(); | |
| await writeState(blank); | |
| return blank; | |
| } | |
| // ── Shared: initialize a lane (used on open, sync, and manual label) ── | |
| async function initializeLane(lane) { | |
| await ensureState(); | |
| const toAdd = [lane]; | |
| let comment; | |
| if (lane === 'lane:editorial-fix' || lane === 'lane:dev') { | |
| toAdd.push('stage:ready-to-merge'); | |
| const statusDesc = lane === 'lane:editorial-fix' ? 'Editorial fix — admin may merge' : 'Dev doc — admin may merge'; | |
| const laneTitle = lane === 'lane:editorial-fix' ? 'Editorial Fix' : 'Dev Doc'; | |
| await setReviewStatus('success', statusDesc); | |
| comment = `📋 **Lane: ${laneTitle}**\n\nNo formal review required.\n\n@usace-rmc/docs-admin please review, merge, and approve the deploy.`; | |
| } else { | |
| toAdd.push('stage:peer-review'); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:peer-review']); | |
| const laneName = lane.replace('lane:', '').replace(/-/g, ' '); | |
| const scope = lane === 'lane:new-doc' ? 'Peer → Lead Civil → Technical Edit → Director' | |
| : lane === 'lane:major-revision' ? 'Peer → Lead Civil → Technical Edit' | |
| : 'Peer → Technical Edit'; | |
| comment = `📋 **Lane: ${laneName}**\n\nReview scope: ${scope}.\n\nCurrently in **peer review**. @usace-rmc/docs-admin please assign the peer reviewer(s) via the Reviewers sidebar.`; | |
| } | |
| await addLabels(toAdd); | |
| await postComment(comment); | |
| } | |
| // ═════════════════════════════════════════════════════════ | |
| // Event handlers | |
| // ═════════════════════════════════════════════════════════ | |
| // ── pull_request opened/reopened ───────────────────────── | |
| if (eventName === 'pull_request' && ['opened', 'reopened'].includes(action)) { | |
| const { touchesDocs, allUnderDev } = await getDocsFileState(); | |
| if (!touchesDocs) return; | |
| // Content-based dev detection runs before branch-name detection: | |
| // if every changed docs file lives under docs/dev/, it's a dev | |
| // doc PR regardless of what the branch is called. | |
| const lane = existingLane || (allUnderDev ? 'lane:dev' : detectLane(branch)); | |
| if (!lane) { | |
| await addLabels(['stage:needs-lane']); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:needs-lane']); | |
| const reason = branch.startsWith('docs/') | |
| ? `Branch \`${branch}\` starts with \`docs/\` but does not match an expected sub-prefix (\`docs/new/\`, \`docs/major/\`, \`docs/minor/\`, \`docs/fix/\`, \`docs/dev/\`).` | |
| : `Branch \`${branch}\` does not follow the \`docs/{new,major,minor,fix,dev}/\` naming convention, but this PR modifies files under \`docs/\` and therefore requires a review lane.`; | |
| await postComment(`⚠️ **Could not determine review lane**\n\n${reason}\n\n@usace-rmc/docs-admin please apply the correct \`lane:*\` label.`); | |
| return; | |
| } | |
| await initializeLane(lane); | |
| return; | |
| } | |
| // ── pull_request synchronize (new commits pushed) ──────── | |
| if (eventName === 'pull_request' && action === 'synchronize') { | |
| // Case A: no lane yet. Either a non-docs PR that just became | |
| // docs-touching (run lane detection), or a stage:needs-lane PR | |
| // that's still waiting for admin to assign a lane (no-op, just | |
| // refresh the status on the new head SHA). | |
| if (!existingLane) { | |
| if (existingStage === 'stage:needs-lane') { | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:needs-lane']); | |
| return; | |
| } | |
| const { touchesDocs, allUnderDev } = await getDocsFileState(); | |
| if (!touchesDocs) return; | |
| const lane = allUnderDev ? 'lane:dev' : detectLane(branch); | |
| if (!lane) { | |
| await addLabels(['stage:needs-lane']); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:needs-lane']); | |
| const reason = branch.startsWith('docs/') | |
| ? `Branch \`${branch}\` starts with \`docs/\` but does not match an expected sub-prefix.` | |
| : `Branch \`${branch}\` does not follow \`docs/{new,major,minor,fix,dev}/\` naming. This PR now modifies files under \`docs/\` and requires a review lane.`; | |
| await postComment(`⚠️ **Could not determine review lane**\n\n${reason}\n\n@usace-rmc/docs-admin please apply the correct \`lane:*\` label.`); | |
| return; | |
| } | |
| await initializeLane(lane); | |
| return; | |
| } | |
| // Case B: lane + stage already set. Do NOT reset the stage — | |
| // revisions during a review round are normal and expected. | |
| // Just re-set the commit status on the new head SHA (since | |
| // statuses are per-SHA) and ping the assigned reviewer(s) to | |
| // backcheck. | |
| if (existingStage === 'stage:ready-to-merge') { | |
| await setReviewStatus('success', STAGE_STATUS_DESC['stage:ready-to-merge']); | |
| await postComment(`⚠️ **New commits pushed after PR reached \`stage:ready-to-merge\`**\n\n@usace-rmc/docs-admin please eyeball the new changes before merging — the bot has re-set the \`review-workflow\` status to success, but this bypasses any incremental review of the latest push.`); | |
| return; | |
| } | |
| if (existingStage && STAGE_STATUS_DESC[existingStage]) { | |
| await setReviewStatus('pending', STAGE_STATUS_DESC[existingStage]); | |
| } | |
| const syncState = await readState(); | |
| if (syncState && existingStage && syncState[existingStage] && syncState[existingStage].length > 0) { | |
| const mentions = syncState[existingStage].map(u => '@' + u).join(', '); | |
| const stageDisplay = STAGE_DISPLAY[existingStage] || existingStage; | |
| await postComment(`🔄 **New commits pushed by @${prAuthor}.**\n\n${mentions} — please backcheck the revisions for **${stageDisplay}**.`); | |
| } | |
| return; | |
| } | |
| // ── pull_request review_requested ──────────────────────── | |
| // An admin assigned an individual as a reviewer via the sidebar. | |
| // Record them in the state comment under the current stage. | |
| // Ignored if the request is for a team (payload.requested_team) | |
| // instead of an individual — admins should request individuals. | |
| if (eventName === 'pull_request' && action === 'review_requested') { | |
| if (!existingLane || !existingStage) return; | |
| if (!ACTIVE_REVIEW_STAGES.includes(existingStage)) return; | |
| const requested = context.payload.requested_reviewer && context.payload.requested_reviewer.login; | |
| if (!requested) return; | |
| const state = await ensureState(); | |
| if (!state[existingStage]) state[existingStage] = []; | |
| if (!state[existingStage].includes(requested)) { | |
| state[existingStage].push(requested); | |
| await writeState(state); | |
| } | |
| return; | |
| } | |
| // ── pull_request review_request_removed ────────────────── | |
| if (eventName === 'pull_request' && action === 'review_request_removed') { | |
| if (!existingLane || !existingStage) return; | |
| if (!ACTIVE_REVIEW_STAGES.includes(existingStage)) return; | |
| const removed = context.payload.requested_reviewer && context.payload.requested_reviewer.login; | |
| if (!removed) return; | |
| const state = await readState(); | |
| if (!state || !state[existingStage]) return; | |
| const idx = state[existingStage].indexOf(removed); | |
| if (idx !== -1) { | |
| state[existingStage].splice(idx, 1); | |
| await writeState(state); | |
| } | |
| return; | |
| } | |
| // ── pull_request labeled ───────────────────────────────── | |
| if (eventName === 'pull_request' && action === 'labeled') { | |
| const added = context.payload.label.name; | |
| // Admin override: advance a PR past the technical edit stage | |
| // without requiring the author to check the PR description | |
| // checkbox. Used when the technical edit was done by a human, | |
| // or when the author isn't around. | |
| // - Lane 1 (new-doc): advances to Director review | |
| // - Lanes 2 & 3 (major/minor revision): advances to ready-to-merge | |
| if (added === 'admin:advance-to-director') { | |
| const lanesWithTechEdit = ['lane:new-doc', 'lane:major-revision', 'lane:minor-revision']; | |
| if (!lanesWithTechEdit.includes(existingLane) || existingStage !== 'stage:ai-editor-review') { | |
| await removeLabel('admin:advance-to-director'); | |
| await postComment(`⚠️ **admin:advance-to-director** can only be applied to a PR currently at \`stage:ai-editor-review\` in a lane that includes technical edit. Label removed, no action taken.`); | |
| return; | |
| } | |
| await removeLabel('admin:advance-to-director'); | |
| await removeLabel('stage:ai-editor-review'); | |
| if (existingLane === 'lane:new-doc') { | |
| await addLabels(['stage:director-review']); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:director-review']); | |
| await postComment(`✅ **Technical edit marked complete by site admin override.**\n\nAdvancing to **Director review**.\n\n@usace-rmc/docs-admin next steps:\n1. Trigger a checkpoint deploy of branch \`${branch}\` via Actions → Deploy to GitHub Pages → Run workflow (this is the first deploy of this PR to the live site, with the DRAFT watermark)\n2. Approve the deploy at the production environment gate\n3. Post the live URL in a comment on this PR\n4. Assign a member of @usace-rmc/docs-director via the Reviewers sidebar\n\nThe Director will review at the live URL. If the Director requests changes and the author pushes fixes, re-trigger the checkpoint deploy to refresh the live URL.`); | |
| } else { | |
| await addLabels(['stage:ready-to-merge']); | |
| await setReviewStatus('success', STAGE_STATUS_DESC['stage:ready-to-merge']); | |
| await postComment(`✅ **Technical edit marked complete by site admin override.**\n\nThis PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin next steps:\n1. Check out this branch\n2. Flip the document's \`draft\` flag to \`false\`\n3. Update \`00-version-history.mdx\`\n4. Commit and push\n5. Merge to \`main\`\n6. Approve the production deploy`); | |
| } | |
| return; | |
| } | |
| // Lane manually applied by an admin (overrides branch-name detection) | |
| if (LANE_LABELS.includes(added) && (!existingStage || existingStage === 'stage:needs-lane')) { | |
| await removeLabel('stage:needs-lane'); | |
| await ensureState(); | |
| if (added === 'lane:editorial-fix' || added === 'lane:dev') { | |
| await addLabels(['stage:ready-to-merge']); | |
| const statusDesc = added === 'lane:editorial-fix' ? 'Editorial fix — admin may merge' : 'Dev doc — admin may merge'; | |
| const laneLabel = added === 'lane:editorial-fix' ? 'editorial fix' : 'dev doc'; | |
| await setReviewStatus('success', statusDesc); | |
| await postComment(`📋 Lane set to **${laneLabel}**.\n\n@usace-rmc/docs-admin please review and merge.`); | |
| } else { | |
| await addLabels(['stage:peer-review']); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:peer-review']); | |
| await postComment(`📋 Lane set to **${added.replace('lane:', '').replace(/-/g, ' ')}**. Moving to peer review.\n\n@usace-rmc/docs-admin please assign the peer reviewer(s).`); | |
| } | |
| } | |
| return; | |
| } | |
| // ── pull_request edited (author checks technical-edit box) ── | |
| if (eventName === 'pull_request' && action === 'edited') { | |
| if (existingStage === 'stage:ai-editor-review') { | |
| const checkboxChecked = prBody.includes('[x] Technical edit comments addressed'); | |
| if (checkboxChecked) { | |
| await removeLabel('stage:ai-editor-review'); | |
| if (existingLane === 'lane:new-doc') { | |
| // Lane 1: advance to Director review with checkpoint deploy | |
| await addLabels(['stage:director-review']); | |
| await setReviewStatus('pending', STAGE_STATUS_DESC['stage:director-review']); | |
| await postComment(`✅ **Technical edit marked complete** by the author.\n\nAdvancing to **Director review**.\n\n@usace-rmc/docs-admin next steps:\n1. Trigger a checkpoint deploy of branch \`${branch}\` via Actions → Deploy to GitHub Pages → Run workflow (this is the first deploy of this PR to the live site, with the DRAFT watermark)\n2. Approve the deploy at the production environment gate\n3. Post the live URL in a comment on this PR\n4. Assign a member of @usace-rmc/docs-director via the Reviewers sidebar\n\nThe Director will review at the live URL. If the Director requests changes and the author pushes fixes, re-trigger the checkpoint deploy to refresh the live URL.`); | |
| } else { | |
| // Lanes 2 & 3: no Director review — ready to merge | |
| await addLabels(['stage:ready-to-merge']); | |
| await setReviewStatus('success', STAGE_STATUS_DESC['stage:ready-to-merge']); | |
| await postComment(`✅ **Technical edit marked complete** by the author.\n\nThis PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin next steps:\n1. Check out this branch\n2. Flip the document's \`draft\` flag to \`false\`\n3. Update \`00-version-history.mdx\`\n4. Commit and push\n5. Merge to \`main\`\n6. Approve the production deploy`); | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| // ── pull_request_review submitted (approved) ───────────── | |
| // Per-individual gating: an approval only advances the stage | |
| // if the approver is in the assigned list for the current | |
| // stage. Drive-by approvals from non-assigned reviewers are | |
| // acknowledged with an info comment but do not advance. | |
| if (eventName === 'pull_request_review' && context.payload.review.state === 'approved') { | |
| if (!existingLane || !existingStage) return; | |
| if (!ACTIVE_REVIEW_STAGES.includes(existingStage)) return; | |
| const reviewer = context.payload.review.user.login; | |
| const state = await readState(); | |
| const assigned = (state && state[existingStage]) || []; | |
| if (!assigned.includes(reviewer)) { | |
| const assignedList = assigned.length ? assigned.map(u => '@' + u).join(', ') : '_(none assigned yet)_'; | |
| await postComment(`ℹ️ **Approval from @${reviewer} logged, but stage not advanced.**\n\n@${reviewer} is not in the list of assigned reviewers for **${STAGE_DISPLAY[existingStage] || existingStage}**. The stage will advance only when an approval is received from one of the assigned reviewers: ${assignedList}.\n\nIf @${reviewer} should be advancing the stage, @usace-rmc/docs-admin please assign them via the Reviewers sidebar and ask them to re-approve.`); | |
| return; | |
| } | |
| let nextStage = null, comment = null; | |
| if (existingLane === 'lane:new-doc') { | |
| if (existingStage === 'stage:peer-review') { | |
| nextStage = 'stage:lead-civil-review'; | |
| comment = `✅ **Peer review approved** by @${reviewer}.\n\nAdvancing to **RMC Lead Civil review**.\n\n@usace-rmc/docs-admin please assign the Lead Civil reviewer(s) via the Reviewers sidebar. The Lead Civil reviews on the preview URL.`; | |
| } else if (existingStage === 'stage:lead-civil-review') { | |
| nextStage = 'stage:ai-editor-review'; | |
| comment = `✅ **Lead Civil review approved** by @${reviewer}.\n\nAdvancing to **technical edit**.\n\n@usace-rmc/docs-admin please run the \`/technical-edit\` Claude Code skill against this PR (or assign a human technical editor). The technical edit reviews the document source MDX directly and posts inline comments on the PR — **no live deploy is needed at this stage**.\n\nAfter the author addresses the technical edit comments and checks the completion checkbox in the PR description, the document will advance to Director review and the site admin will deploy it to the live site (watermarked) at that point.`; | |
| } else if (existingStage === 'stage:director-review') { | |
| nextStage = 'stage:ready-to-merge'; | |
| comment = `✅ **Director review approved** by @${reviewer}.\n\nThis PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin next steps:\n1. Check out this branch (locally or via github.dev)\n2. Flip the document's \`draft\` flag to \`false\`\n3. Update \`00-version-history.mdx\` with reviewer and approver names\n4. Commit and push\n5. Merge this PR to \`main\`\n6. Approve the final production deploy in the Actions tab`; | |
| } | |
| } else if (existingLane === 'lane:major-revision') { | |
| if (existingStage === 'stage:peer-review') { | |
| nextStage = 'stage:lead-civil-review'; | |
| comment = `✅ **Peer review approved** by @${reviewer}.\n\nAdvancing to **RMC Lead Civil review**.\n\n@usace-rmc/docs-admin please assign the Lead Civil reviewer(s) via the Reviewers sidebar.`; | |
| } else if (existingStage === 'stage:lead-civil-review') { | |
| nextStage = 'stage:ai-editor-review'; | |
| comment = `✅ **Lead Civil review approved** by @${reviewer}.\n\nAdvancing to **technical edit**.\n\n@usace-rmc/docs-admin please run the \`/technical-edit\` Claude Code skill against this PR (or assign a human technical editor). The technical edit reviews the document source MDX directly and posts inline comments on the PR.\n\nAfter the author addresses the technical edit comments and checks the completion checkbox in the PR description, the site admin will flip the draft flag, merge, and deploy.`; | |
| } | |
| } else if (existingLane === 'lane:minor-revision') { | |
| if (existingStage === 'stage:peer-review') { | |
| nextStage = 'stage:ai-editor-review'; | |
| comment = `✅ **Peer review approved** by @${reviewer}.\n\nAdvancing to **technical edit**.\n\n@usace-rmc/docs-admin please run the \`/technical-edit\` Claude Code skill against this PR (or assign a human technical editor). The technical edit reviews the document source MDX directly and posts inline comments on the PR.\n\nAfter the author addresses the technical edit comments and checks the completion checkbox in the PR description, the site admin will flip the draft flag, merge, and deploy.`; | |
| } | |
| } | |
| if (nextStage) { | |
| await removeLabel(existingStage); | |
| await addLabels([nextStage]); | |
| if (nextStage === 'stage:ready-to-merge') { | |
| await setReviewStatus('success', STAGE_STATUS_DESC['stage:ready-to-merge']); | |
| } else if (STAGE_STATUS_DESC[nextStage]) { | |
| await setReviewStatus('pending', STAGE_STATUS_DESC[nextStage]); | |
| } | |
| await postComment(comment); | |
| } | |
| } |