From a8d232343871da6589a1d359e73027bc01119cc6 Mon Sep 17 00:00:00 2001 From: Adam Gohs Date: Wed, 9 Sep 2026 10:56:46 -0400 Subject: [PATCH 1/2] ci: overhaul document review and publication workflows --- .claude/skills/git-conventions.md | 2 +- .claude/skills/new-doc/SKILL.md | 14 +- .claude/skills/new-revision/SKILL.md | 14 +- .claude/skills/pr/SKILL.md | 6 +- .claude/skills/technical-edit/SKILL.md | 6 +- .github/pull_request_template.md | 35 +- .github/workflows/ci-build.yml | 105 +-- .github/workflows/deploy.yml | 16 +- .github/workflows/pr-preview-cleanup.yml | 35 +- .github/workflows/pr-preview.yml | 144 ++-- .github/workflows/review-signal.yml | 10 + .github/workflows/stage-progression.yml | 609 +------------ CLAUDE.md | 46 +- README.md | 60 +- .../documentation-guide/00-introduction.mdx | 22 +- .../01-getting-started.mdx | 2 +- .../02-versioning-system.mdx | 6 +- .../04-creating-new-document-walkthrough.mdx | 40 +- .../documentation-guide/05-docx-converter.mdx | 2 +- .../09-review-and-approval-overview.mdx | 18 +- .../documentation-guide/10-review-lanes.mdx | 8 +- .../11-author-workflow.mdx | 24 +- .../12-reviewer-workflow.mdx | 39 +- .../documentation-guide/13-technical-edit.mdx | 10 +- .../14-director-workflow.mdx | 52 +- .../15-site-admin-workflow.mdx | 220 +---- .../17-appendix-b-build-process-overview.mdx | 81 +- docs/dev/github-workflows/00-introduction.mdx | 6 +- docs/dev/github-workflows/01-overview.mdx | 6 +- .../02-repository-structure-permissions.mdx | 23 +- .../github-workflows/08-merging-to-main.mdx | 17 +- .../11-release-management.mdx | 4 + package-lock.json | 3 +- package.json | 6 +- .../2026-09-08-review-workflow-decisions.md | 803 ++++++++++++++++++ ...6-09-08-review-workflow-overhaul-design.md | 291 +++++++ planning/2026-09-09-activation-checklist.md | 126 +++ planning/2026-09-09-implementation.md | 31 + planning/2026-09-09-verification.md | 22 + scripts/review/builds.js | 59 ++ scripts/review/controller.js | 539 ++++++++++++ scripts/review/documents.js | 138 +++ scripts/review/github.js | 141 +++ scripts/review/policy.js | 242 ++++++ scripts/review/preview.js | 140 +++ scripts/review/sessions.js | 234 +++++ tests/review-controller.test.js | 23 + tests/review-documents.test.js | 71 ++ tests/review-integration.test.js | 527 ++++++++++++ tests/review-policy.test.js | 208 +++++ tests/review-preview.test.js | 145 ++++ 51 files changed, 4165 insertions(+), 1266 deletions(-) create mode 100644 .github/workflows/review-signal.yml create mode 100644 planning/2026-09-08-review-workflow-decisions.md create mode 100644 planning/2026-09-08-review-workflow-overhaul-design.md create mode 100644 planning/2026-09-09-activation-checklist.md create mode 100644 planning/2026-09-09-implementation.md create mode 100644 planning/2026-09-09-verification.md create mode 100644 scripts/review/builds.js create mode 100644 scripts/review/controller.js create mode 100644 scripts/review/documents.js create mode 100644 scripts/review/github.js create mode 100644 scripts/review/policy.js create mode 100644 scripts/review/preview.js create mode 100644 scripts/review/sessions.js create mode 100644 tests/review-controller.test.js create mode 100644 tests/review-documents.test.js create mode 100644 tests/review-integration.test.js create mode 100644 tests/review-policy.test.js create mode 100644 tests/review-preview.test.js diff --git a/.claude/skills/git-conventions.md b/.claude/skills/git-conventions.md index 18559ac49..a9fe3c517 100644 --- a/.claude/skills/git-conventions.md +++ b/.claude/skills/git-conventions.md @@ -30,7 +30,7 @@ under `docs/` has no lane and no review stages. The prefix routes the PR to a review lane. Use the full two-segment prefix — a bare `docs/{name}` does **not** match a lane and strands the PR in `stage:needs-lane`. -- `docs/new/{slug}` — new document (Peer → Lead Civil → Technical edit → Director) +- `docs/new/{slug}` — new document (Peer → Lead Civil → Technical edit in the Content PR; separate Director review after draft merge) - `docs/major/{slug}-v{X.0}` — major revision (Peer → Lead Civil → Technical edit) - `docs/minor/{slug}-v{X.Y}` — minor revision (Peer → Technical edit) - `docs/fix/{slug}` — editorial fix (no review; admin self-merge) diff --git a/.claude/skills/new-doc/SKILL.md b/.claude/skills/new-doc/SKILL.md index bfe79e89d..0d5ad6cf7 100644 --- a/.claude/skills/new-doc/SKILL.md +++ b/.claude/skills/new-doc/SKILL.md @@ -58,9 +58,7 @@ If the category IS `dev/`: ### 1f — Active / Draft Status -Ask: "Should this document be active (visible and clickable) or inactive (shows 'Coming Soon' badge)? (default: active)" - -Record both `active` (true/false) and `draft` (inverse of active) values. +Ask whether the draft should be active after its Content PR merges. Record `active` independently and always start a new document with `draft: true`. The later Publication PR removes draft status after Director approval or waiver. ### 1g — File Structure (dev only) @@ -393,3 +391,13 @@ Next steps: - Add chapter files (02-*.mdx, 03-*.mdx, etc.) - Run `npm start` to preview locally ``` + +## Step 9: Prepare review + +Use a descriptive `docs/new/` branch. The prefix suggests intent; it does not assign review state. Keep the author PR to one document and directly related source, assets, registrations, and site changes. After the Content PR opens, an administrator records: + +```text +/review classify new +``` + +The Content PR requires peer, Lead Civil, and manually initiated technical editing. It merges and deploys from `main` as an active draft before an administrator starts the separate Director Review PR. Do not remove `draft: true` in the Content PR and do not deploy the author branch. diff --git a/.claude/skills/new-revision/SKILL.md b/.claude/skills/new-revision/SKILL.md index de846e47b..e0f8b0d49 100644 --- a/.claude/skills/new-revision/SKILL.md +++ b/.claude/skills/new-revision/SKILL.md @@ -25,8 +25,8 @@ The corresponding value in the JSON is the `currentVersion` (e.g., `v1.0`). Ask: "Is this a major revision or a minor revision?" -- **Major revision** — substantial changes warranting a new major version. Branch prefix: `docs/major/`. Goes through Lane 2 (Peer → Lead Civil → Technical edit). No Director review. -- **Minor revision** — smaller updates warranting a minor version bump. Branch prefix: `docs/minor/`. Goes through Lane 3 (Peer → Technical edit). No Director review. +- **Major revision** — substantial changes warranting a new major version. Descriptive branch prefix: `docs/major/`. Requires Peer → Lead Civil → Technical edit. No Director review. +- **Minor revision** — smaller updates warranting a minor version bump. Descriptive branch prefix: `docs/minor/`. Requires Peer → Technical edit. No Director review. ### 1c — New version number @@ -161,5 +161,13 @@ Next steps: 1. Edit the new version's MDX files to make your changes 2. Update 00-version-history.mdx with a real description and your name 3. Run `npm start` to preview locally - 4. When ready, commit and open a PR (the workflow will detect the branch prefix and start {Lane 2: Peer → Lead Civil | Lane 3: Peer review} automatically) + 4. When ready, commit and open one PR for this document ``` + +After the PR opens, an administrator records the authoritative classification: + +```text +/review classify +``` + +The branch prefix is descriptive and does not assign review state. The administrator assigns one named reviewer per human stage. Technical editing is manually initiated and reviews the changed content with enough surrounding context to assess it. Production builds only from administrator-merged `main`; do not deploy the revision branch. diff --git a/.claude/skills/pr/SKILL.md b/.claude/skills/pr/SKILL.md index 4509f578f..09312dab3 100644 --- a/.claude/skills/pr/SKILL.md +++ b/.claude/skills/pr/SKILL.md @@ -68,9 +68,9 @@ Read `.claude/skills/git-conventions.md` for PR conventions. - Derived from the **overall purpose** of all commits on the branch, not just the latest commit - Example: "Redesign homepage with product tile grid layout" -### Body — choose the format based on the branch prefix +### Body — choose the format based on the change -**If the current branch starts with `docs/`** → use the **Template Style** (Step 5a). The repo's PR template at `.github/pull_request_template.md` contains a checklist for document authors, including the Lane 1 "Technical edit comments addressed" checkbox that the stage-progression workflow watches for. Passing `--body` to `gh pr create` overrides the template entirely, so the skill must reproduce the template structure with the auto-generated content filled in. +**If the change affects a document** → use the **Template Style** (Step 5a). Reproduce the repository template when passing `--body`, including the one-document scope, `doc_location`, related issues, validation, and review notes. Branch prefixes describe intent; an administrator's controller classification is authoritative. **Otherwise (infrastructure, tooling, dependency, or any non-doc branch)** → use the **Summary Style** (Step 5b). These PRs are silently ignored by the review workflow and don't need the doc-author checklist. @@ -89,7 +89,7 @@ In both styles, the summary content should be based on **ALL** commits on the br - Under `## Description`, replace the `` comment with the summary bullets. - Under `## Affected documents`, replace the bare `- ` line with the list of affected MDX files. Format each as a markdown link relative to the repo root: `- [filename.mdx](docs/full/path/filename.mdx)`. If there are no changed MDX files, write `- _No MDX files changed in this PR._` - Leave the `## Related issue(s)` section's comment placeholder unchanged so the author can fill it in. - - Leave **all checklist items unchecked**, including the Lane 1 Technical edit checkbox. The author checks them as they complete each item; the workflow specifically depends on the Technical edit checkbox being present and unchecked at PR open time. + - Mark validation items only when evidence supports them. Do not add a technical-edit advancement checkbox or manually request stage reviewers. - Leave the `## Notes for reviewers` comment placeholder unchanged. The result should be a complete copy of the template with the Description and Affected documents sections populated. diff --git a/.claude/skills/technical-edit/SKILL.md b/.claude/skills/technical-edit/SKILL.md index bd2d49531..a0d9cfa7f 100644 --- a/.claude/skills/technical-edit/SKILL.md +++ b/.claude/skills/technical-edit/SKILL.md @@ -52,6 +52,8 @@ Tell the user: - How many files were reviewed - Which review prompt version was used - Remind the author to check the "Technical edit comments addressed" checkbox in the PR description when they're done addressing comments -- Remind the author to reply to threads from the **Files changed** tab using **Start a review**, then submit once. Replying from the Conversation tab files each reply as its own standalone review and emails every PR subscriber separately. +- Suggest batching related replies from **Files changed** with **Start a review** to reduce separate submissions and notification noise. Do not promise an exact email count. -Note that step 4 posts a single bundled review on purpose: one `POST /pulls/:pr/reviews` call with every comment in the `comments` array produces one notification. Never loop over `POST /pulls/:pr/comments` — that creates one standalone review per comment and floods every subscriber. +Step 4 posts one bundled review so findings arrive coherently and avoid unnecessary separate notifications. Native GitHub delivery still depends on each user's settings. + +For a new document, review the full document. For a major or minor revision, review changed content with enough surrounding context to assess it. Completion is explicit: after the edit finishes, an administrator records `/review complete editor`. Author replies and thread resolution are not stage gates. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4d4f8a46f..8aa1b20fa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,35 +1,24 @@ ## Description - + -## Affected documents +## Document scope - + -- +- Document: +- Document location (`doc_location`, or `-`): ## Related issue(s) - + -## Pre-submission checklist +## Validation -- [ ] I have previewed these changes locally or via the PR preview URL -- [ ] My branch name uses an expected prefix — documentation: `docs/new/`, `docs/major/`, `docs/minor/`, `docs/fix/`, `docs/dev/`; everything else: `feature/`, `fix/`, `chore/`, `ci/` -- [ ] I have updated `00-version-history.mdx` if this change warrants a version entry -- [ ] I have assigned a specific peer reviewer via the Reviewers sidebar (if known) +- [ ] I previewed the change locally or through the PR preview +- [ ] I ran the checks appropriate to this change +- [ ] I updated version history when the document revision warrants it -## Technical edit (Lanes 1, 2, and 3) +## Review notes - - -- [ ] Technical edit comments addressed - -## Notes for reviewers - - + diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index ddc47b496..85e9a0a32 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,65 +1,70 @@ name: CI Build - +run-name: 'Validate PR #${{ github.event.pull_request.number || inputs.pr }} at ${{ github.event.pull_request.head.sha || inputs.sha }}' on: pull_request: types: [opened, synchronize, reopened] - + workflow_dispatch: + inputs: + pr: + description: 'PR number to validate (used by review automation)' + required: true + type: string + sha: + description: 'Exact PR head revision to validate' + required: true + type: string permissions: contents: read - pull-requests: write - statuses: write - + pull-requests: read +concurrency: + group: ci-${{ github.event.pull_request.number || inputs.pr }} + cancel-in-progress: true jobs: build: - name: CI Build + name: Build preview artifact runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read steps: + - name: Resolve PR + id: pr + uses: actions/github-script@v7 + with: + script: | + const n = context.payload.pull_request?.number || Number(context.payload.inputs?.pr); + if (!Number.isSafeInteger(n) || n < 1) throw new Error('Invalid PR number'); + if (context.eventName === 'workflow_dispatch' && context.ref !== 'refs/heads/main') throw new Error('Dispatch from main only'); + const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number:n}); + if (pr.state !== 'open') throw new Error('PR is closed'); + const expected = context.payload.pull_request?.head.sha || context.payload.inputs?.sha; + if (expected !== pr.head.sha) throw new Error('PR changed before this build started; build the latest revision'); + core.setOutput('number', String(n)); + core.setOutput('sha', pr.head.sha); + core.setOutput('repository', pr.head.repo.full_name); - uses: actions/checkout@v4 + with: + repository: ${{ steps.pr.outputs.repository }} + ref: ${{ steps.pr.outputs.sha }} + persist-credentials: false - uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: '22' + cache: npm - run: npm ci - - run: npm run build - - name: Signal admin-may-merge for non-docs PRs - if: success() - uses: actions/github-script@v7 + - run: npm run test:review --if-present + - name: Build preview + run: npm run build + env: + DOCUSAURUS_URL: https://usace-rmc.github.io + DOCUSAURUS_BASE_URL: /RMC-Software-Documentation-Previews/pr-${{ steps.pr.outputs.number }}/ + DOCUSAURUS_IS_PREVIEW: 'true' + - uses: actions/upload-artifact@v4 with: - script: | - const prNumber = context.payload.pull_request.number; - const files = await github.paginate(github.rest.pulls.listFiles, { - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - per_page: 100, - }); - // Branch naming for this path is conventional, not enforced: - // feature/, fix/, chore/, ci/. What actually decides the lane is - // content -- a PR touching nothing under docs/ has no review lane - // and needs only this build to pass. - const touchesDocs = files.some(f => f.filename.startsWith('docs/')); - if (touchesDocs) return; // docs PRs are handled by stage-progression.yml - - // Flip the review-workflow commit status to success so branch - // protection allows merge. This is the non-docs counterpart to - // stage-progression.yml's status-setting for docs PRs. - const headSha = context.payload.pull_request.head.sha; - await github.rest.repos.createCommitStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: headSha, - state: 'success', - context: 'review-workflow', - description: 'No doc changes — admin may merge', - }); - - const marker = ''; - const sha = headSha.substring(0, 7); - const body = `${marker}\n\n✅ **CI build passed** for commit \`${sha}\`\n\nThis PR changes no files under \`docs/\`, so it carries no review lane. **CI Build is the only gate** - there are no review stages to clear.\n\n@usace-rmc/docs-admin may merge.`; - const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.user.type === 'Bot' && c.body.includes(marker)); - 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 }); - } + name: preview-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.sha }} + path: build/ + if-no-files-found: error + retention-days: 7 + outputs: + pr: ${{ steps.pr.outputs.number }} + sha: ${{ steps.pr.outputs.sha }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cae3baf8c..2e1ead94e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,12 +12,8 @@ on: - 'package.json' - 'package-lock.json' - 'scripts/**' + - '.github/workflows/deploy.yml' workflow_dispatch: - inputs: - ref: - description: 'Branch or ref to deploy (leave blank for main)' - required: false - type: string permissions: contents: read @@ -32,13 +28,19 @@ jobs: build: name: Build site runs-on: ubuntu-latest + permissions: + contents: read steps: + - name: Require main + if: github.ref != 'refs/heads/main' + run: echo 'Production builds must run from main.' >&2; exit 1 - uses: actions/checkout@v4 with: - ref: ${{ inputs.ref || github.ref }} + ref: ${{ github.sha }} + persist-credentials: false - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' - run: npm ci - run: npm run build diff --git a/.github/workflows/pr-preview-cleanup.yml b/.github/workflows/pr-preview-cleanup.yml index 971084574..572afe0a9 100644 --- a/.github/workflows/pr-preview-cleanup.yml +++ b/.github/workflows/pr-preview-cleanup.yml @@ -1,31 +1,20 @@ name: PR Preview Cleanup - on: - pull_request: + pull_request_target: types: [closed] - + workflow_run: + workflows: ['Deploy to GitHub Pages'] + types: [completed] + workflow_dispatch: permissions: - contents: read - + actions: write jobs: - cleanup: - name: Delete preview directory + request-cleanup: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/github-script@v7 with: - repository: usace-rmc/RMC-Software-Documentation-Previews - ref: gh-pages - ssh-key: ${{ secrets.PREVIEW_DEPLOY_KEY }} - - name: Remove PR preview directory - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - if [ -d "pr-${PR_NUMBER}" ]; then - rm -rf "pr-${PR_NUMBER}" - git add -A - git commit -m "Clean up preview for PR #${PR_NUMBER}" - git push - fi + script: | + // Publication and deletion share the publisher's serialized transaction. + await github.rest.actions.createWorkflowDispatch({...context.repo, + workflow_id:'pr-preview.yml', ref:'main'}); diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index ec65ede11..7fe903a57 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -1,87 +1,93 @@ -name: PR Preview Build - +name: Publish PR Preview on: - pull_request: - types: [opened, synchronize, reopened] - paths: - - 'docs/**' - - 'src/**' - - 'static/**' - - 'docusaurus.config.js' - - 'tailwind.config.js' - - 'package.json' - - 'package-lock.json' - - 'scripts/**' - + workflow_dispatch: + workflow_run: + workflows: ['Review Control'] + types: [completed] permissions: contents: read - pull-requests: write - + actions: read + pull-requests: read concurrency: - group: pr-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - + group: review-control + cancel-in-progress: false jobs: - build-and-deploy: - name: Build and deploy preview + publish: runs-on: ubuntu-latest + environment: review-control steps: - uses: actions/checkout@v4 + with: + ref: main + persist-credentials: false - uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' - - run: npm ci - - name: Build site with PR-specific baseUrl + node-version: '22' + - run: npm ci --ignore-scripts + - uses: actions/create-github-app-token@v3 + id: app + with: + client-id: ${{ vars.REVIEW_APP_CLIENT_ID }} + private-key: ${{ secrets.REVIEW_APP_PRIVATE_KEY }} + - name: Recover pending review actions + run: node scripts/review/controller.js reconcile + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEW_TOKEN: ${{ steps.app.outputs.token }} + REVIEW_BOT_LOGIN: ${{ steps.app.outputs.app-slug }}[bot] + - name: Select verified preview artifact + id: preview + run: node scripts/review/preview.js select env: - DOCUSAURUS_URL: https://usace-rmc.github.io - DOCUSAURUS_BASE_URL: /RMC-Software-Documentation-Previews/pr-${{ github.event.pull_request.number }}/ - DOCUSAURUS_IS_PREVIEW: 'true' - run: npm run build + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEW_TOKEN: ${{ steps.app.outputs.token }} + REVIEW_BOT_LOGIN: ${{ steps.app.outputs.app-slug }}[bot] + - uses: actions/download-artifact@v4 + if: steps.preview.outputs.number != '' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ steps.preview.outputs.run }} + artifact-ids: ${{ steps.preview.outputs.artifact }} + path: preview-artifact + merge-multiple: true + - name: Validate static artifact + if: steps.preview.outputs.number != '' + run: node scripts/review/preview.js validate preview-artifact - uses: peaceiris/actions-gh-pages@v4 + if: steps.preview.outputs.number != '' with: deploy_key: ${{ secrets.PREVIEW_DEPLOY_KEY }} external_repository: usace-rmc/RMC-Software-Documentation-Previews publish_branch: gh-pages - publish_dir: ./build - destination_dir: pr-${{ github.event.pull_request.number }} + publish_dir: preview-artifact + destination_dir: pr-${{ steps.preview.outputs.number }} keep_files: true - user_name: 'github-actions[bot]' - user_email: 'github-actions[bot]@users.noreply.github.com' - commit_message: 'Deploy preview for PR #${{ github.event.pull_request.number }}' - - name: Post or update preview URL comment - uses: actions/github-script@v7 - with: - script: | - const prNumber = context.issue.number; - const url = `https://usace-rmc.github.io/RMC-Software-Documentation-Previews/pr-${prNumber}/`; - const sha = context.payload.pull_request.head.sha.substring(0, 7); - const marker = ''; - const body = `${marker}\n\n📄 **Preview deployed** for commit \`${sha}\`\n\n${url}\n\n_This preview updates automatically when new commits are pushed. Deleted when the PR closes._`; - const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.user.type === 'Bot' && c.body.includes(marker)); - 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 }); - } - - name: Mark preview as stale on failure - if: failure() - uses: actions/github-script@v7 + - name: Update persistent review summary + if: always() + run: node scripts/review/preview.js record + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEW_TOKEN: ${{ steps.app.outputs.token }} + REVIEW_BOT_LOGIN: ${{ steps.app.outputs.app-slug }}[bot] + PREVIEW_NUMBER: ${{ steps.preview.outputs.number }} + PREVIEW_SHA: ${{ steps.preview.outputs.sha }} + PREVIEW_RESULT: ${{ job.status }} + PREVIEW_RUN: ${{ steps.preview.outputs.run }} + PREVIEW_ARTIFACT: ${{ steps.preview.outputs.artifact }} + - name: Select completed previews for cleanup + run: node scripts/review/preview.js cleanup-list + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 with: - script: | - const prNumber = context.issue.number; - const sha = context.payload.pull_request.head.sha.substring(0, 7); - const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const marker = ''; - const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); - const existing = comments.data.find(c => c.user.type === 'Bot' && c.body.includes(marker)); - if (existing) { - // Strip any prior stale warning before appending a fresh one - const cleanBody = existing.body.split('\n\n⚠️')[0]; - const newBody = `${cleanBody}\n\n⚠️ **Preview build failed on commit \`${sha}\`** — the preview URL above is **stale** and reflects an earlier successful build. Do not rely on it as an accurate representation of this PR's current state. [View workflow logs](${runUrl}).`; - await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: newBody }); - } else { - const body = `${marker}\n\n❌ **Preview build failed** on commit \`${sha}\`\n\nThe site failed to build for this PR, so there is no preview to view. [View workflow logs](${runUrl}).`; - await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body }); - } + repository: usace-rmc/RMC-Software-Documentation-Previews + ref: gh-pages + ssh-key: ${{ secrets.PREVIEW_DEPLOY_KEY }} + path: preview-site + - name: Remove completed previews + run: node scripts/review/preview.js cleanup preview-site + - name: Record removed previews + run: node scripts/review/preview.js cleanup-record + env: + REVIEW_TOKEN: ${{ steps.app.outputs.token }} + REVIEW_BOT_LOGIN: ${{ steps.app.outputs.app-slug }}[bot] diff --git a/.github/workflows/review-signal.yml b/.github/workflows/review-signal.yml new file mode 100644 index 000000000..f095f3219 --- /dev/null +++ b/.github/workflows/review-signal.yml @@ -0,0 +1,10 @@ +name: Review Signal +on: + pull_request_review: + types: [submitted, dismissed] +permissions: {} +jobs: + signal: + runs-on: ubuntu-latest + steps: + - run: echo 'Review activity will be reconciled by the trusted default-branch controller.' diff --git a/.github/workflows/stage-progression.yml b/.github/workflows/stage-progression.yml index a9ff1a708..b63207a4e 100644 --- a/.github/workflows/stage-progression.yml +++ b/.github/workflows/stage-progression.yml @@ -1,588 +1,39 @@ -name: Stage Progression - +name: Review Control on: - pull_request: - types: [opened, reopened, synchronize, labeled, edited, review_requested, review_request_removed] - pull_request_review: - types: [submitted, dismissed] - + pull_request_target: + types: [opened, reopened, synchronize, edited, labeled, unlabeled, review_requested, review_request_removed, closed] + issue_comment: + types: [created] + workflow_run: + workflows: ['CI Build', 'Review Signal', 'Deploy to GitHub Pages'] + types: [completed] + workflow_dispatch: permissions: - pull-requests: write - issues: write contents: read - statuses: write - concurrency: - group: stage-progression-${{ github.event.pull_request.number }} + group: review-control cancel-in-progress: false - jobs: - progress: - name: Manage review stage + control: runs-on: ubuntu-latest + environment: review-control steps: - - name: Run stage progression logic - uses: actions/github-script@v7 + - uses: actions/checkout@v4 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 = ''; - const STATE_DATA_RE = //; - - function emptyState() { - return { - 'stage:peer-review': [], - 'stage:lead-civil-review': [], - 'stage:ai-editor-review': [], - 'stage:director-review': [], - }; - } - - function renderStateComment(state) { - const dataLine = ``; - 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 { - // Rendered WITHOUT an @ prefix on purpose. An @mention here - // permanently subscribes that user to the PR, so every later - // bot comment -- including stages they already finished -- - // emails them. This board is a status readout, not a call to - // action; GitHub's Reviewers sidebar does the notifying. - 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._'); - lines.push(''); - lines.push('_Names above are deliberately unlinked. An @mention would subscribe each person to every later comment on this PR; GitHub notifies assigned reviewers through the Reviewers sidebar instead._'); - 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}\` doesn't match a known sub-prefix (\`docs/new/\`, \`docs/major/\`, \`docs/minor/\`, \`docs/fix/\`, \`docs/dev/\`), so the lane can't be auto-detected.` - : `Branch \`${branch}\` doesn't follow the \`docs/{new,major,minor,fix,dev}/\` naming convention, and this PR modifies files under \`docs/\`. Branches for non-documentation work (\`feature/\`, \`fix/\`, \`chore/\`, \`ci/\`) skip the review lanes entirely, but only when the PR changes nothing under \`docs/\`.`; - await postComment(`📋 **Lane assignment needed**\n\n${reason}\n\n@usace-rmc/docs-admin please apply a \`lane:*\` label to route this PR.`); - 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}\` doesn't match a known sub-prefix.` - : `Branch \`${branch}\` doesn't follow \`docs/{new,major,minor,fix,dev}/\` naming. This PR now modifies files under \`docs/\`, so it can no longer skip the review lanes.`; - await postComment(`📋 **Lane assignment needed**\n\n${reason}\n\n@usace-rmc/docs-admin please apply a \`lane:*\` label to route this PR.`); - 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. - if (existingStage === 'stage:ready-to-merge') { - // Post-approval commits invalidate the merge gate. Flip the - // status back to pending until an admin re-acknowledges via - // the `admin:approve-merge-after-push` label. This prevents - // silent merge of changes that bypassed the review chain. - await setReviewStatus('pending', 'New commits pushed after ready-to-merge — admin re-approval required'); - await postComment(`⚠️ **New commits pushed after PR reached \`stage:ready-to-merge\`**\n\nThe \`review-workflow\` merge gate has been flipped back to **pending** until an admin re-acknowledges the new commits.\n\n@usace-rmc/docs-admin please review the new changes. When ready to merge, apply the label \`admin:approve-merge-after-push\` — the bot will re-set the status to success and remove the label automatically.`); - return; - } - if (existingStage && STAGE_STATUS_DESC[existingStage]) { - await setReviewStatus('pending', STAGE_STATUS_DESC[existingStage]); - } - const syncState = await readState(); - const stageAssignees = (syncState && existingStage && syncState[existingStage]) || []; - if (stageAssignees.length > 0) { - // Only ping assignees who have actually submitted a review on - // this PR before. If a reviewer has never reviewed yet, there - // is nothing to "backcheck" — they'll see the new commits - // when they open the PR for the first time. This avoids - // pinging a newly-assigned reviewer every time the author - // pushes routine fixes before the review has started. - const reviewsForBackcheck = await github.paginate(github.rest.pulls.listReviews, { - owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, per_page: 100, - }); - const hasReviewed = new Set(reviewsForBackcheck.map(r => r.user && r.user.login).filter(Boolean)); - const toNotify = stageAssignees.filter(u => hasReviewed.has(u)); - if (toNotify.length > 0) { - const mentions = toNotify.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 a reviewer via the sidebar. Two paths: - // * Individual requests get recorded in the state comment - // under either the current stage or an explicitly-pinned - // stage (via an `assign:` label). - // * Team requests get a one-time guidance comment — the bot - // only gates on individual approvals, so admins still need - // to assign individuals. - if (eventName === 'pull_request' && action === 'review_requested') { - if (!existingLane || !existingStage) return; - if (!ACTIVE_REVIEW_STAGES.includes(existingStage)) return; - - // Team request: post a one-time clarification so admins don't - // assume the team request is sufficient. Dedup'd via marker. - const requestedTeam = context.payload.requested_team && context.payload.requested_team.slug; - if (requestedTeam) { - const TEAM_REQUEST_MARKER = ''; - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, repo: context.repo.repo, - issue_number: prNumber, per_page: 100, - }); - const alreadyPosted = comments.some(c => c.body.includes(TEAM_REQUEST_MARKER)); - if (!alreadyPosted) { - await postComment(`${TEAM_REQUEST_MARKER}\nℹ️ **Team review requests don't advance the stage.**\n\nA review request for **@${context.repo.owner}/${requestedTeam}** is recorded by GitHub but the bot only gates stage advancement on **individual** reviewer approvals.\n\n@usace-rmc/docs-admin please assign one or more individuals via the Reviewers sidebar so their approvals can advance this PR.`); - } - return; - } - - const requested = context.payload.requested_reviewer && context.payload.requested_reviewer.login; - if (!requested) return; - - // Allow admins to pre-assign a reviewer for a later stage by - // first applying an `assign:` label (e.g. - // `assign:lead-civil-review`). Without that label, the - // assignment is recorded under the current stage. - const ASSIGN_PREFIX = 'assign:'; - const assignLabel = labels.find(l => l.startsWith(ASSIGN_PREFIX)); - let targetStage = existingStage; - if (assignLabel) { - const candidate = `stage:${assignLabel.slice(ASSIGN_PREFIX.length)}`; - if (ACTIVE_REVIEW_STAGES.includes(candidate)) { - targetStage = candidate; - } - } - - const state = await ensureState(); - if (!state[targetStage]) state[targetStage] = []; - if (!state[targetStage].includes(requested)) { - state[targetStage].push(requested); - await writeState(state); - } - return; - } - - // ── pull_request review_request_removed ────────────────── - // Mirror the assignment logic: remove the reviewer from - // whichever stage's bucket they're in (they may have been - // pre-pinned to a stage other than the current one via the - // `assign:` label mechanism). - if (eventName === 'pull_request' && action === 'review_request_removed') { - if (!existingLane || !existingStage) return; - const removed = context.payload.requested_reviewer && context.payload.requested_reviewer.login; - if (!removed) return; - const state = await readState(); - if (!state) return; - let mutated = false; - for (const stage of ACTIVE_REVIEW_STAGES) { - if (!state[stage]) continue; - const idx = state[stage].indexOf(removed); - if (idx !== -1) { - state[stage].splice(idx, 1); - mutated = true; - } - } - if (mutated) 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 - // - // `admin:technical-edit-complete` is the accurate name, because - // only Lane 1 continues to a Director. The older - // `admin:advance-to-director` is still accepted as a deprecated - // alias so PRs already carrying it keep working. - if (added === 'admin:technical-edit-complete' || 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(added); - await postComment(`⚠️ **${added}** 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(added); - 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. Advancing to **Director review**.\n\n@usace-rmc/docs-admin please trigger the checkpoint deploy of \`${branch}\` and assign a director reviewer. See the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`); - } 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. This PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin please flip the \`draft\` flag, update \`00-version-history.mdx\`, merge to \`main\`, and approve the production deploy. See the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`); - } - return; - } - - // Admin override: re-approve a `stage:ready-to-merge` PR after - // post-approval commits were pushed. The bot flipped the merge - // gate to pending when those commits landed; this label flips - // it back to success and is then removed. - if (added === 'admin:approve-merge-after-push') { - if (existingStage !== 'stage:ready-to-merge') { - await removeLabel('admin:approve-merge-after-push'); - await postComment(`⚠️ **admin:approve-merge-after-push** can only be applied to a PR currently at \`stage:ready-to-merge\`. Label removed, no action taken.`); - return; - } - await removeLabel('admin:approve-merge-after-push'); - await setReviewStatus('success', STAGE_STATUS_DESC['stage:ready-to-merge']); - await postComment(`✅ **Post-push merge re-approved** by \`${context.payload.sender.login}\`. The \`review-workflow\` status is back to success and the PR is mergeable.`); - 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. Advancing to **Director review**.\n\n@usace-rmc/docs-admin please trigger the checkpoint deploy of \`${branch}\` and assign a director reviewer. See the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`); - } 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. This PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin please flip the \`draft\` flag, update \`00-version-history.mdx\`, merge to \`main\`, and approve the production deploy. See the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`); - } - } - } - return; - } - - // ── pull_request_review submitted / dismissed ──────────── - // Three review outcomes are handled: - // * approved — advance the stage if the approver is in the - // assigned list for the current stage - // * changes_requested — acknowledge with a comment; no stage - // change (the author needs to push fixes first) - // * dismissed — warn admin that a prior approval was dropped; - // do not auto-revert the stage (too risky) - // The `commented` and `edited` cases are intentionally ignored. - if (eventName === 'pull_request_review') { - if (!existingLane || !existingStage) return; - if (!ACTIVE_REVIEW_STAGES.includes(existingStage)) return; - const reviewAction = context.payload.action; - const reviewState = context.payload.review.state; - const reviewer = context.payload.review.user.login; - - if (reviewAction === 'submitted' && reviewState === 'changes_requested') { - const stageDisplay = STAGE_DISPLAY[existingStage] || existingStage; - await postComment(`📝 **Changes requested** by \`${reviewer}\` at **${stageDisplay}**.\n\n@${prAuthor} please address the comments and push fixes. The stage will advance once an assigned reviewer approves.`); - return; - } - - if (reviewAction === 'dismissed') { - const dismissedBy = (context.payload.sender && context.payload.sender.login) || 'unknown'; - await postComment(`⚠️ **Review by \`${reviewer}\` was dismissed** by \`${dismissedBy}\`.\n\nThe stage label has not been changed automatically. @usace-rmc/docs-admin - if this dismissal means a prior stage's approval is no longer valid, please manually revert the stage labels and request a re-review.`); - return; - } - - // Only approval submissions reach here. - if (reviewAction !== 'submitted' || reviewState !== 'approved') return; - - 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\n@usace-rmc/docs-admin - if \`${reviewer}\` should be advancing the stage, 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}\`. Advancing 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}\`. Advancing 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). No live deploy is needed at this stage — see the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`; - } else if (existingStage === 'stage:director-review') { - nextStage = 'stage:ready-to-merge'; - comment = `✅ **Director review approved** by \`${reviewer}\`. This PR is **ready for final merge and publication**.\n\n@usace-rmc/docs-admin please flip the \`draft\` flag, update \`00-version-history.mdx\`, merge to \`main\`, and approve the production deploy. See the Site Admin Workflow chapter of the Documentation Guide for the full sequence.`; - } - } else if (existingLane === 'lane:major-revision') { - if (existingStage === 'stage:peer-review') { - nextStage = 'stage:lead-civil-review'; - comment = `✅ **Peer review approved** by \`${reviewer}\`. Advancing 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}\`. Advancing 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).`; - } - } else if (existingLane === 'lane:minor-revision') { - if (existingStage === 'stage:peer-review') { - nextStage = 'stage:ai-editor-review'; - comment = `✅ **Peer review approved** by \`${reviewer}\`. Advancing 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).`; - } - } - - 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); - } - } + ref: main + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm ci --ignore-scripts + - uses: actions/create-github-app-token@v3 + id: app + with: + client-id: ${{ vars.REVIEW_APP_CLIENT_ID }} + private-key: ${{ secrets.REVIEW_APP_PRIVATE_KEY }} + - name: Reconcile review policy + run: node scripts/review/controller.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEW_TOKEN: ${{ steps.app.outputs.token }} + REVIEW_BOT_LOGIN: ${{ steps.app.outputs.app-slug }}[bot] diff --git a/CLAUDE.md b/CLAUDE.md index de85725e1..d915bc216 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,46 +104,24 @@ Content with
## Branching and Review Workflow -The `main` branch is protected. All changes go through a pull request. +The `main` branch is protected and all changes use pull requests. Branch prefixes are descriptive starting points; an administrator records the authoritative classification with the trusted review controller. -**Whether a PR has a review lane at all is decided by content, not by branch name:** a PR that changes at least one file under `docs/` is a documentation PR and gets a lane; a PR that changes nothing under `docs/` has no lane and no review stages. - -### Documentation branches (files under `docs/`) - -Branch prefixes auto-assign the PR to one of five review lanes via the `stage-progression.yml` GitHub workflow: - -| Prefix | Lane | Reviews required | +| Prefix | Classification | Stages | |---|---|---| -| `docs/new/` | New document | Peer → Lead Civil → Technical edit → Director | -| `docs/major/` | Major revision (new major version) | Peer → Lead Civil → Technical edit | -| `docs/minor/` | Minor revision (new minor version) | Peer → Technical edit | -| `docs/fix/` | Editorial fix | None (admin self-merge) | -| `docs/dev/` | Dev docs (anything under `docs/dev/`) | None (admin self-merge) | - -Only Lane 1 ends in a Director review. Lanes 2 and 3 go straight from the technical edit to `stage:ready-to-merge`. - -Dev-docs detection is also content-based: any PR whose changed files under `docs/` are all under `docs/dev/` is auto-routed to `lane:dev` regardless of branch name. - -### Non-documentation branches (components, plumbing, styling, scripts, CI) - -| Prefix | Use for | -|---|---| -| `feature/` | New components, new site capabilities, enhancements | -| `fix/` | Bug fixes in components, styles, scripts, or build tooling | -| `chore/` | Dependency bumps, refactors, config and cleanup | -| `ci/` | Changes to GitHub workflows and repository automation | - -These carry **no review lane and no review stages**. The only gate is `CI Build`; once it passes, `ci-build.yml` flips `review-workflow` to success and a `@usace-rmc/docs-admin` member may merge. CODEOWNERS still requires an admin on protected paths. +| `docs/new/` | New document | Peer → Lead Civil → technical edit, followed after draft publication by separate Director review | +| `docs/major/` | Major revision | Peer → Lead Civil → technical edit | +| `docs/minor/` | Minor revision | Peer → technical edit | +| `docs/fix/` | Editorial correction | No formal document stages | +| `docs/dev/` | Developer documentation | No formal document stages | +| `feature/`, `fix/`, `chore/`, `ci/` | Site code/infrastructure | No formal document stages | -A PR that mixes site code *and* content under `docs/` is a documentation PR — put it on the matching `docs/…` prefix, or an admin will have to assign the lane by hand. +Keep each author PR to one document. Directly related source, assets, registry entries, and site changes may accompany it. Administrators classify every PR, may correct a lane, and assign exactly one named human to each required stage. A non-admin author cannot review the same work, and non-admin reviewers must differ between stages. Administrators may act in any role. -### Merge gate +Human stages complete through the named reviewer's GitHub approval. Technical editing is manually initiated: new documents receive a full-document edit; revisions focus on changed context. An administrator explicitly completes the AI stage. Completed or waived stages persist across commits until an administrator uses `/review restart `. Replies and resolved threads are collaboration aids, not merge gates; `/review ready` optionally requests renewed attention. -Branch protection on `main` requires two status checks: -- `CI Build` — runs `npm run build` on every PR -- `review-workflow` — set by `stage-progression.yml` for documentation PRs (flips to success at `stage:ready-to-merge`, or immediately for `lane:editorial-fix` and `lane:dev`), and by `ci-build.yml` for non-documentation PRs (flips to success as soon as the build passes) +Only administrators merge to `main` and approve production deployments. Builds and deployments are automatic from `main`; never run a feature-branch production deploy. New documents first merge as drafts, then use separate Director Review and Publication PRs. Attribution in document metadata remains a manual edit. -Full details: [docs/dev/documentation-guide/](docs/dev/documentation-guide/) chapters 09–15. +Full command and role guidance: [docs/dev/documentation-guide/](docs/dev/documentation-guide/) chapters 09–15. ## Code Style diff --git a/README.md b/README.md index 819af9dcf..562edc867 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,37 @@ [![License: 0BSD](https://img.shields.io/badge/License-0BSD-blue.svg)](LICENSE) -This repository contains the official documentation site for the RMC suite of tools, developed and maintained by the U.S. Army Corps of Engineers Risk Management Center (RMC). The site is built using [Docusaurus](https://docusaurus.io/) and is styled with [Tailwind CSS](https://tailwindcss.com/), customized to align with USACE branding. +This repository contains the official documentation site for the U.S. Army Corps of Engineers Risk Management Center software suite. It is built with Docusaurus and Tailwind CSS and includes user guides, technical manuals, developer documentation, versioned content, figures, equations, citations, search, and accessible navigation. -The project includes: +See the published [Documentation Guide](https://usace-rmc.github.io/RMC-Software-Documentation/docs/documentation-guide/introduction/) for authoring and component guidance. -- User guides and technical manuals for RMC software tools -- Detailed component-level documentation (React-based) -- Tables, figures, citations, equations, and glossary support -- Versioned documentation management -- Built-in navigation, search, and dark mode +## Pull requests and review -The project is organized to support easy contribution, internal consistency, and extensibility across future RMC documentation needs. +Every change uses a pull request to the protected `main` branch. Branch prefixes describe intent and provide an initial classification; the trusted review controller records the authoritative classification and review state. -For development standards, file structure, custom components, and styling conventions, please refer to the internal [Documentation Guide](https://usace-rmc.github.io/RMC-Software-Documentation/docs/documentation-guide/introduction/). +| Prefix | Classification | Required document review | +| --- | --- | --- | +| `docs/new/` | New document | Peer → Lead Civil → technical edit; separate Director review after draft publication | +| `docs/major/` | Major revision | Peer → Lead Civil → technical edit | +| `docs/minor/` | Minor revision | Peer → technical edit | +| `docs/fix/` | Editorial correction | Administrator handling; no formal document stages | +| `docs/dev/` | Developer documentation | Administrator handling; no formal document stages | +| `feature/`, `fix/`, `chore/`, `ci/` | Site code or infrastructure | Administrator handling and required CI | -The guide includes: +An author PR may cover one document. It may also include directly related source, asset, registry, and site changes. An administrator classifies every PR with `/review classify`, assigns one named reviewer to each required human stage, and may correct the classification at any time. For non-administrators, authors cannot review their own work and the named reviewers must differ between stages. Administrators may act in any role. -- How to write and structure MDX content -- How to use and customize React components -- Tailwind utility conventions and shared styles -- Table formatting and counter file integration -- Versioning strategy and linking guidelines -- The full review and approval workflow for all documentation changes +Completed stages remain complete after new commits until an administrator explicitly restarts a stage. GitHub replies and resolved conversations are useful collaboration tools but are not automated gates. Authors may use `/review ready` to request a new look after revisions. -## Review and Approval Workflow +New documents use three clearly labeled PRs: -All changes to documentation go through a pull request against the protected `main` branch. A PR that changes at least one file under `docs/` is a documentation PR, and its branch prefix determines which of five review lanes it is routed to: +1. The Content PR completes peer, Lead Civil, and technical editing, then merges to `main` and publishes as a draft. +2. An administrator starts a separate Director Review PR against a stable full-document baseline and assigns one Director. +3. Director approval or an administrator waiver creates a Publication PR that applies the reviewed document changes and removes draft status. An administrator verifies and merges it. -| Branch prefix | Lane | Reviews required | -|---|---|---| -| `docs/new/` | New document | Peer → Lead Civil → Technical edit → Director | -| `docs/major/` | Major revision (new major version) | Peer → Lead Civil → Technical edit | -| `docs/minor/` | Minor revision (new minor version) | Peer → Technical edit | -| `docs/fix/` | Editorial fix | None (admin self-merge) | -| `docs/dev/` | Dev docs (anything under `docs/dev/`) | None (admin self-merge) | +Only administrators merge to `main` and approve production deployment. Successful merges build automatically; contributors do not deploy from feature branches. -### Non-documentation changes - -Work on components, styling, build scripts, configuration, and CI changes nothing under `docs/`, so it carries **no review lane and no review stages**. Use a `feature/`, `fix/`, `chore/`, or `ci/` branch. The only gate is the `CI Build` check; once it passes, a site administrator may merge. - -### Preview builds and the merge gate - -Each PR receives an automatic preview build at an unadvertised URL where reviewers read the rendered document. Branch protection on `main` requires both the `CI Build` and `review-workflow` status checks to pass before merge, so the merge button reflects the workflow's judgment automatically. - -For details on each lane, who reviews what, and step-by-step instructions for authors, reviewers, the Director, and site administrators, see chapters 9 through 15 of the [Documentation Guide](https://usace-rmc.github.io/RMC-Software-Documentation/docs/documentation-guide/introduction/). +See chapters 9–15 of the Documentation Guide for commands and role-specific instructions. ## Contact -For questions or support, contact the RMC documentation team: - -Adam Gohs -502-315-6484 -Adam.C.Gohs@usace.army.mil +For questions or support, contact Adam Gohs at Adam.C.Gohs@usace.army.mil or 502-315-6484. diff --git a/docs/dev/documentation-guide/00-introduction.mdx b/docs/dev/documentation-guide/00-introduction.mdx index 4f3937ea8..7cf4d1d70 100644 --- a/docs/dev/documentation-guide/00-introduction.mdx +++ b/docs/dev/documentation-guide/00-introduction.mdx @@ -139,13 +139,13 @@ The documentation guide is structured to take you from setup through advanced us ### Review and Approval Process -9. **[Review and Approval Process Overview](./09-review-and-approval-overview.mdx)** - Roles, lanes, the merge gate, and the draft watermark -10. **[Review Lanes](./10-review-lanes.mdx)** - Branch prefixes and which review stages apply to each lane +9. **[Review and Approval Process Overview](./09-review-and-approval-overview.mdx)** - Roles, controller state, merge, and publication +10. **[Review Classifications and Stages](./10-review-lanes.mdx)** - Classifications, descriptive branch prefixes, and required stages 11. **[Author Workflow](./11-author-workflow.mdx)** - From branch creation to publication 12. **[Reviewer Workflow](./12-reviewer-workflow.mdx)** - For peer reviewers and RMC Lead Civils 13. **[Technical Edit](./13-technical-edit.mdx)** - AI-assisted editorial review (includes the full prompt) -14. **[Director Workflow](./14-director-workflow.mdx)** - Final approval for new documents -15. **[Site Admin Workflow](./15-site-admin-workflow.mdx)** - Reviewer assignment, checkpoint deploys, merge prep +14. **[Director Review and Publication](./14-director-workflow.mdx)** - Separate full-document review and publication for new documents +15. **[Site Admin Workflow](./15-site-admin-workflow.mdx)** - Classification, assignments, exceptions, merge, and deployment ### Appendices (Advanced Topics) @@ -268,7 +268,7 @@ This is regular text with **bold** and _italic_ formatting. ## Common Contribution Workflows -The `main` branch is protected. Every change goes through a feature branch and a pull request. For changes under `docs/`, the branch prefix routes the PR to one of five review lanes — see [Review Lanes](./10-review-lanes.mdx) for the full mapping. Work that touches nothing under `docs/` — components, styling, scripts, configuration, CI — uses a `feature/`, `fix/`, `chore/`, or `ci/` branch and has no review lane at all; a passing `CI Build` is the only requirement. +The `main` branch is protected. Every change goes through a descriptive branch and pull request. Branch prefixes suggest intent; an administrator records the authoritative classification for every PR. Documentation classifications use the stages in [Review Classifications and Stages](./10-review-lanes.mdx). Site-code and infrastructure changes have no formal document stages but still require current CI and administrator merge. ### Workflow 1: Convert Existing Word Document @@ -283,7 +283,7 @@ The `main` branch is protected. Every change goes through a feature branch and a 5. Add generated files to the appropriate `docs/` folder. 6. Register the document in `src/docConfig.js` with `active: false, draft: true`. 7. Test locally with `npm start`. -8. Commit, push the branch, and open a pull request. The review workflow auto-assigns Lane 1 (peer → Lead Civil → technical edit → Director). +8. Commit, push the branch, and open one Content PR for the document. An administrator classifies it as `new` and assigns peer, Lead Civil, and technical-edit stages. It merges as a draft before separate Director review. **Typical time:** 1-3 hours depending on document complexity. @@ -310,7 +310,7 @@ The walkthrough includes folder setup, asset organization, testing procedures, a 5. Create MDX files (using the DOCX converter or manually). 6. Register the document in `src/docConfig.js` with `active: false, draft: true`. 7. Test locally with `npm start`. -8. Commit, push, and open a PR. The review workflow auto-assigns Lane 1 and guides you through peer → Lead Civil → technical edit → Director review (see [Author Workflow](./11-author-workflow.mdx)). +8. Commit, push, and open one Content PR. An administrator classifies it and assigns peer, Lead Civil, and technical editing. It merges as a draft before separate Director review (see [Author Workflow](./11-author-workflow.mdx)). **For detailed instructions, templates, real-world examples, and troubleshooting, use the [comprehensive walkthrough](./04-creating-new-document-walkthrough.mdx).** @@ -327,18 +327,18 @@ The walkthrough includes folder setup, asset organization, testing procedures, a 3. Test locally. 4. Commit, push, and open a PR. The site admin will review and merge — no formal peer review required. -**For minor revisions / new minor version (Lane 3):** +**For minor revisions / a new minor version:** 1. Create a `docs/minor/-v` branch off `main`. 2. Create the new version folder (e.g., `v1.1/`) by copying the previous version. 3. Mirror the version structure for figures, bibliography, and source documents. 4. Make your updates and flip `draft: true` on the document's entry in `src/docConfig.js`. 5. Test locally. -6. Commit, push, and open a PR. The workflow assigns Lane 3 (peer review → technical edit). +6. Commit, push, and open one PR for the document. An administrator classifies it as `minor`; it requires peer review and technical editing. -**For major revisions / new major version (Lane 2):** +**For major revisions / a new major version:** -Same as Lane 3 but on a `docs/major/-v` branch. The workflow adds an RMC Lead Civil review stage between peer review and the technical edit. +Use the same process on a `docs/major/-v` branch. An administrator classifies it as `major`; it requires peer review, RMC Lead Civil review, and technical editing. **Typical time:** 30 minutes to a few hours depending on scope. Review turnaround adds days; plan accordingly. diff --git a/docs/dev/documentation-guide/01-getting-started.mdx b/docs/dev/documentation-guide/01-getting-started.mdx index 4e9e4dd70..e5e543032 100644 --- a/docs/dev/documentation-guide/01-getting-started.mdx +++ b/docs/dev/documentation-guide/01-getting-started.mdx @@ -278,7 +278,7 @@ This will start the development server at `http://localhost:3000`. Any changes y - Press `Ctrl+C` in the terminal :::tip Deployment is Admin-Only -Contributors should focus on creating and editing documentation content using `npm start` for local testing. Site administrators will handle all building and deployment to production. See [Appendix B: Build Process Overview](./17-appendix-b-build-process-overview.mdx) for deployment details. +Contributors use `npm start` for local editing and run `npm run build` when their changes can affect the site. Production builds run automatically from merged `main`; administrators merge and approve the protected production environment. See [Appendix B: Build Process Overview](./17-appendix-b-build-process-overview.mdx). ::: :::tip Take the Guided Site Tour diff --git a/docs/dev/documentation-guide/02-versioning-system.mdx b/docs/dev/documentation-guide/02-versioning-system.mdx index 91a79cce3..8b757c21f 100644 --- a/docs/dev/documentation-guide/02-versioning-system.mdx +++ b/docs/dev/documentation-guide/02-versioning-system.mdx @@ -273,7 +273,7 @@ All necessary scripts run automatically during `npm start` or `npm run build`. Y ### Step 7: Commit Changes and Open a Pull Request -The `main` branch is protected — changes cannot be pushed directly. All revisions are committed to a feature branch and merged via a pull request. The branch prefix routes the PR to the appropriate review lane (see [Review Lanes](./10-review-lanes.mdx)): +The `main` branch is protected, so revisions use a branch and pull request. The prefix describes the intended classification; an administrator records the authoritative classification with the review controller (see [Review Classifications and Stages](./10-review-lanes.mdx)): - **Major revision** (e.g., `v1.0` → `v2.0`): use prefix `docs/major/` - **Minor revision** (e.g., `v1.0` → `v1.1`): use prefix `docs/minor/` @@ -305,7 +305,7 @@ The `main` branch is protected — changes cannot be pushed directly. All revisi
  1. Click Publish branch (first push) or Push origin (subsequent pushes)
  2. Click Create Pull Request — this opens the GitHub website to the PR creation page
  3. -
  4. Fill in the PR description, assign a peer reviewer if known, and click Create pull request
  5. +
  6. Fill in the PR description, identify the document location, and click Create pull request. The administrator assigns the stage reviewer.
), @@ -335,7 +335,7 @@ git push -u origin docs/minor/lifesim-users-guide-v1.1`} defaultValue="github-desktop" /> -After the PR is opened, a preview build is published to an unadvertised URL and the review workflow auto-assigns a lane label. See [Author Workflow](./11-author-workflow.mdx) for what happens next. +After the PR opens, CI and the preview build report their results. An administrator classifies the PR as `major` or `minor` and assigns one named reviewer for the first stage. See [Author Workflow](./11-author-workflow.mdx). --- diff --git a/docs/dev/documentation-guide/04-creating-new-document-walkthrough.mdx b/docs/dev/documentation-guide/04-creating-new-document-walkthrough.mdx index 4763caa39..e44d9d2f4 100644 --- a/docs/dev/documentation-guide/04-creating-new-document-walkthrough.mdx +++ b/docs/dev/documentation-guide/04-creating-new-document-walkthrough.mdx @@ -130,12 +130,12 @@ Even when using the DOCX converter (Chapter 05), you'll still need to follow mos **When to use this guide:** -- Creating documentation for new software (Lane 1 — `docs/new/` branch) -- Adding a new document type for existing software (Lane 1 — `docs/new/` branch) +- Creating documentation for new software on a `docs/new/` branch +- Adding a new document type for existing software on a `docs/new/` branch -For creating a **new version** of an existing document (v1.1, v2.0, etc.), see [Versioning System](./02-versioning-system.mdx) instead — that's Lane 2 (major revision) or Lane 3 (minor revision), not Lane 1. +For creating a **new version** of an existing document (v1.1, v2.0, etc.), see [Versioning System](./02-versioning-system.mdx) instead. -**The big picture.** Every new document goes through Lane 1 of the review workflow: you author it on a `docs/new/` branch, open a pull request, and the document is reviewed by a peer, the RMC Lead Civil, and an AI-assisted technical edit before the RMC Director gives final approval. The site administrator handles all deploys. See [Review Lanes](./10-review-lanes.mdx) and [Author Workflow](./11-author-workflow.mdx) for the full picture. +**The big picture.** Author one new document on a `docs/new/` branch. Its Content PR receives peer, Lead Civil, and AI-assisted technical editing, then merges and deploys as a draft. An administrator later starts a separate full-document Director Review PR; approval or waiver produces a Publication PR. See [Review Classifications and Stages](./10-review-lanes.mdx) and [Author Workflow](./11-author-workflow.mdx). --- @@ -176,10 +176,10 @@ Before you begin, gather and prepare the following: ## Step 1: Create a Branch -The `main` branch is protected — you cannot push directly to it. All new documents are authored on a feature branch that gets merged via a pull request. For a new document, the branch prefix is **`docs/new/`** — this routes the PR to Lane 1 (the full review workflow with peer, Lead Civil, technical edit, and Director stages). +The `main` branch is protected. Author new documents on a feature branch and merge them through a pull request. Use **`docs/new/`** to describe the intended classification. An administrator's controller command is authoritative. :::tip Not creating a new document? -Five branch prefixes are available, each routing to a different review lane (new document, major revision, minor revision, editorial fix, dev docs). See [Review Lanes](./10-review-lanes.mdx) for the full list and how each lane works. This chapter covers Lane 1 only — for revisions to an existing document, see [Versioning System](./02-versioning-system.mdx) instead. +Documentation prefixes describe new documents, major revisions, minor revisions, editorial corrections, and developer documentation. See [Review Classifications and Stages](./10-review-lanes.mdx). This chapter covers a new document; for revisions, see [Versioning System](./02-versioning-system.mdx). ::: **Branch naming:** `docs/new/` where the slug describes the document. Use lowercase with hyphens. @@ -623,7 +623,7 @@ Verify the following before proceeding: Every published document is registered in [`src/docConfig.js`](https://github.com/USACE-RMC/RMC-Software-Documentation/blob/main/src/docConfig.js). This file is the single source of truth for which documents appear on the site and which carry the DRAFT watermark. Without an entry, your document won't appear on the appropriate landing page, won't be indexed by search, and won't be included in the production build. -Add a new entry to the `docs` array, grouped by category and software. For a brand-new document, start with `active: false, draft: true` — this hides the document from the production site until it's ready, but keeps it building in dev mode so you can preview it locally. The site administrator will flip these flags as part of the final merge after Director approval. +Add a new entry to the `docs` array, grouped by category and software. Set `draft: true`. Choose `active` based on whether the draft should be available in production after the Content PR merges. Publication removes draft status after separate Director review. **Example entry for a new desktop application document:** @@ -738,7 +738,7 @@ This will: ## Step 8: Open a Pull Request -When local testing passes, commit your work and open a pull request. The `docs/new/` branch prefix routes the PR to **Lane 1** of the review workflow: peer review → RMC Lead Civil review → AI technical edit → Director approval. +When local testing passes, commit your work and open a Content pull request. The `docs/new/` prefix describes the intended classification. An administrator records `/review classify new `; the Content PR then requires peer, Lead Civil, and AI technical-edit stages. ### Commit and push @@ -762,24 +762,19 @@ After pushing, GitHub returns a URL like `https://github.com/USACE-RMC/RMC-Softw The repository ships a PR template that prefills a description structure. Fill in the sections: - **Description** — what document you're adding and why -- **Affected documents** — the path you created +- **Document scope** — the one document and its `doc_location` - **Related issues** — any tracked issues this addresses -- **Pre-submission checklist** — confirm branch prefix, local preview, and version-history entry -- **Technical edit checkbox** — leave unchecked until later in the review +- **Validation** — record the preview, checks, and version-history update Click **Create pull request**. ### What happens automatically -Within a minute or two of opening the PR: - -1. A **preview build** is published to an unadvertised URL. Look for the bot comment that says "Preview deployed." -2. The stage progression workflow assigns `lane:new-doc` and `stage:peer-review` labels, and posts a comment identifying the lane and what's needed next. -3. The `review-workflow` merge gate is set to `pending` — `main` cannot be merged into until the workflow flips it to success. +After opening the PR, CI builds the site and the preview workflow reports its result. The administrator classifies the PR and assigns one named peer reviewer. The controller keeps one persistent summary with the classification, stage, reviewer, completed or waived stages, preview, and next action. Labels make purpose and status easy to scan; they do not control state. ### What you do next -If you have a peer reviewer in mind, assign them via the **Reviewers** sidebar. If you don't, the site administrator will assign one. From here on, follow the [Author Workflow](./11-author-workflow.mdx) chapter — it covers responding to reviewer comments, addressing the technical edit, and what to expect through each review stage. +Follow the [Author Workflow](./11-author-workflow.mdx). The administrator assigns every stage reviewer. Apply feedback and use `/review ready` if substantial revisions need renewed attention. Replies and resolved threads help the discussion but are not automated gates. ### When something special is needed @@ -790,7 +785,7 @@ The PR comment thread is the place to coordinate anything unusual. Tag `@usace-r - You need a new React component that doesn't exist yet - The document has a specific deployment-timing dependency (e.g., must go live alongside a software release) -You no longer need to email the administrator with a deployment request — the PR itself is the coordination point, and the workflow handles the deploy automatically once the review is complete. +The PR is the coordination point. After the Content PR merges, production builds automatically from `main` and publishes the document as a draft. An administrator later starts the separate Director review. --- @@ -827,7 +822,7 @@ You no longer need to email the administrator with a deployment request — the **7. Test locally** with `npm start`. Verify all figures display, videos play, citations render, and there are no console errors. -**8. Open the pull request:** push the branch, open the PR, fill in the template, and let the workflow take over. Because this is a brand-new software product, add a comment tagging `@usace-rmc/docs-admin` to request the homepage/navigation links be added — that's an admin task that runs alongside the review. +**8. Open the Content pull request:** push the branch, fill in the template, and identify its `doc_location`. Because this is a brand-new software product, note the required homepage or navigation links in the PR. ### Example 2: New Toolbox Technical Manual @@ -864,7 +859,7 @@ You no longer need to email the administrator with a deployment request — the **7. Test locally** — verify the new tool appears under the existing seismic-hazard-suite in the sidebar. -**8. Open the pull request.** Because the suite already exists, no admin coordination is needed for navigation — the workflow handles everything once the review is complete. +**8. Open the Content pull request.** Identify the document location and validation so an administrator can classify it and assign review. --- @@ -931,9 +926,10 @@ Use this scannable checklist to confirm every step is complete: ✓ Pushed branch to remote ✓ Opened PR via GitHub web interface or IDE ✓ Filled in PR template description -✓ Verified `lane:new-doc` and `stage:peer-review` labels were applied automatically +✓ Identified the one document and its `doc_location` +✓ Verified the controller summary shows administrator classification and assignment ✓ Verified preview URL builds and displays the new document -✓ Tagged `@usace-rmc/docs-admin` with any special requests (new-software navigation links, custom sidebar exception, new component request, deployment-timing dependency) +✓ Recorded special navigation, component, or timing needs in the PR --- diff --git a/docs/dev/documentation-guide/05-docx-converter.mdx b/docs/dev/documentation-guide/05-docx-converter.mdx index 905cf4c6c..602739151 100644 --- a/docs/dev/documentation-guide/05-docx-converter.mdx +++ b/docs/dev/documentation-guide/05-docx-converter.mdx @@ -717,7 +717,7 @@ Press `Ctrl+C` to stop the development server when testing is complete. :::info Production Build Not Required -**Contributors do NOT need to run `npm run build`** before committing. The development server (`npm start`) is sufficient for testing. Site administrators will handle building and deploying the site to production. +Use `npm start` while editing, then run `npm run build` before opening or updating the PR when the conversion can affect the site. Production builds run automatically from merged `main` after administrator approval. If you encounter any issues while testing locally, contact the repository administrator rather than attempting to troubleshoot build processes. diff --git a/docs/dev/documentation-guide/09-review-and-approval-overview.mdx b/docs/dev/documentation-guide/09-review-and-approval-overview.mdx index 0c355e9af..bd9a01d0a 100644 --- a/docs/dev/documentation-guide/09-review-and-approval-overview.mdx +++ b/docs/dev/documentation-guide/09-review-and-approval-overview.mdx @@ -32,8 +32,8 @@ Six roles participate in the workflow. Only the author needs local development t 'Subject-matter expert assigned ad-hoc to review technical accuracy on the preview URL. Works entirely in the GitHub web interface.', 'Provides technical oversight and quality assurance. Assigned ad-hoc per document. Reviews on the preview URL.', 'AI-powered editorial review covering grammar, clarity, tense, terminology, and Section 508 accessibility. A team member with the necessary tooling triggers the review; the AI posts inline comments on the PR. A human technical editor can be substituted at the site admin\'s discretion.', - 'The RMC Director provides final approval for new documents only. Reviews the document on the live site (with a DRAFT watermark) and clicks Approve — no file editing required.', - 'Manages reviewer assignments, checkpoint deploys, merge preparation, and final deploys. The only role with access to protected parts of the repository.', + 'The RMC Director provides final approval for new documents only, in a separate full-document Director Review PR after draft publication.', + 'Classifies PRs, assigns one named reviewer per stage, records exceptions, merges to main, and approves production.', ], ]} /> @@ -61,7 +61,7 @@ Every documentation change falls into one of five lanes based on its scope. 'Any change to documents under docs/dev/ (developer guides, internal references)', ], [ - 'Peer → Lead Civil → Technical edit → Director', + 'Content PR: Peer → Lead Civil → Technical edit; then separate Director Review and Publication PRs', 'Peer → Lead Civil → Technical edit', 'Peer → Technical edit', 'None (site admin self-merges)', @@ -70,9 +70,9 @@ Every documentation change falls into one of five lanes based on its scope. ]} /> -For Lane 1, peer review, Lead Civil review, and the technical edit all happen against the preview URL or the source MDX files. Only after the technical edit completes does the site administrator deploy the document to the live site (watermarked) for Director review at the document's final URL. For Lanes 2 and 3, the entire review happens on the preview URL — the live site continues to show the previously-published version, unwatermarked. +For a new document, peer, Lead Civil, and technical editing happen in the Content PR. That PR then merges to `main` and deploys as a draft. An administrator starts a separate Director Review PR with a stable full-document preview; approval or waiver produces a Publication PR. -Only Lane 1 ends in a Director review. Lanes 2 and 3 finish at the technical edit and go straight to `stage:ready-to-merge`; Lanes 4 and 5 have no review stages at all. +Only a new document requires Director review. Major and minor revisions finish after technical editing; editorial, developer-documentation, and code changes have no formal document stages. See [Review Lanes](./10-review-lanes.mdx) for branch-prefix conventions and detailed examples. @@ -80,9 +80,9 @@ See [Review Lanes](./10-review-lanes.mdx) for branch-prefix conventions and deta The five lanes cover documentation. They do not cover the rest of the repository — React components, CSS, build scripts, Docusaurus configuration, and GitHub workflows. -**A pull request that changes nothing under `docs/` has no review lane and no review stages.** It is not assigned a `lane:*` or `stage:*` label, no reviewers are requested by the workflow, and no approval is required to merge it. The only gate is the `CI Build` check; once `npm run build` passes, the `review-workflow` status flips to success and a site administrator merges. +Every PR receives an administrator classification. A code-only PR uses `/review classify code -` and has no formal document stages; current required CI and administrator merge still apply. -Branch prefixes for that work are `feature/`, `fix/`, `chore/`, and `ci/`. They are a readability convention rather than a routing signal — what actually decides whether a PR is reviewed is whether it touches `docs/`. +Branch prefixes are descriptive conventions. The administrator's recorded classification is authoritative. A pull request that changes site code **and** documentation is a documentation PR and needs a `docs/` branch prefix. See [Changes with no review lane](./10-review-lanes.mdx#changes-with-no-review-lane) for the full treatment. @@ -96,7 +96,7 @@ Every PR carries a GitHub commit status called `review-workflow` that functions columns={[ [ 'Any active review stage (peer, Lead Civil, AI editor, Director, or needs-lane)', - 'stage:ready-to-merge (all reviews complete)', + 'Controller reports all required stages complete or waived', 'lane:editorial-fix or lane:dev assigned', 'Non-docs PR, CI Build passed', ], @@ -113,7 +113,7 @@ The status is re-evaluated on every push to a PR. The goal is that the merge but ## The draft watermark -Documents flagged as drafts display a large diagonal "DRAFT" watermark. For Lane 1, the watermark appears on the live site during the Director review phase only — the document is not deployed to the live site until the technical edit is complete. The watermark signals to any reader who finds the live URL that the content is not yet authoritative, and it is removed when the site administrator flips the draft flag after Director approval. +Documents flagged as drafts display a large diagonal "DRAFT" watermark. A new-document Content PR deploys from `main` with this watermark before separate Director review. The Publication PR removes draft status after Director approval or waiver. For Lanes 2 and 3, the document under revision exists only on the preview site during review. The currently-published version on the live site is never watermarked. diff --git a/docs/dev/documentation-guide/10-review-lanes.mdx b/docs/dev/documentation-guide/10-review-lanes.mdx index 73365beba..96afd5975 100644 --- a/docs/dev/documentation-guide/10-review-lanes.mdx +++ b/docs/dev/documentation-guide/10-review-lanes.mdx @@ -14,7 +14,7 @@ Every change **to documentation** follows one of five review lanes. Changes that Whether a pull request gets a lane is decided by content, not by branch name: a PR that changes at least one file under `docs/` is a documentation PR. For those, the workflow assigns a lane automatically using two signals, in order: 1. **Content-based detection.** If every documentation file changed in the PR is under `docs/dev/`, the PR is assigned `lane:dev` regardless of branch name. -2. **Branch-name detection.** Otherwise, the branch prefix determines the lane. +2. **Administrator classification.** The branch prefix suggests intent, but an administrator records or corrects the authoritative classification with `/review classify`. `; content detection may inform the suggestion but does not replace that action. **No version change, no watermark.** diff --git a/docs/dev/documentation-guide/11-author-workflow.mdx b/docs/dev/documentation-guide/11-author-workflow.mdx index 0ae532d17..4004a9b53 100644 --- a/docs/dev/documentation-guide/11-author-workflow.mdx +++ b/docs/dev/documentation-guide/11-author-workflow.mdx @@ -14,7 +14,7 @@ What an author does from start to publication, across all five lanes. ## Starting work -For every lane, the author creates a feature branch off `main` with the appropriate prefix. The branch prefix is what routes the PR to the right review lane — see [Review Lanes](./10-review-lanes.mdx) for the mapping. +For every classification, the author creates a feature branch off `main` with the descriptive prefix. An administrator records the authoritative classification; the branch name does not control review state.