|
1 | 1 | name: CI Build |
2 | | - |
| 2 | +run-name: 'Validate PR #${{ github.event.pull_request.number || inputs.pr }} at ${{ github.event.pull_request.head.sha || inputs.sha }}' |
3 | 3 | on: |
4 | 4 | pull_request: |
5 | 5 | types: [opened, synchronize, reopened] |
6 | | - |
| 6 | + workflow_dispatch: |
| 7 | + inputs: |
| 8 | + pr: |
| 9 | + description: 'PR number to validate (used by review automation)' |
| 10 | + required: true |
| 11 | + type: string |
| 12 | + sha: |
| 13 | + description: 'Exact PR head revision to validate' |
| 14 | + required: true |
| 15 | + type: string |
7 | 16 | permissions: |
8 | 17 | contents: read |
9 | | - pull-requests: write |
10 | | - statuses: write |
11 | | - |
| 18 | + pull-requests: read |
| 19 | +concurrency: |
| 20 | + group: ci-${{ github.event.pull_request.number || inputs.pr }} |
| 21 | + cancel-in-progress: true |
12 | 22 | jobs: |
13 | 23 | build: |
14 | | - name: CI Build |
| 24 | + name: Build preview artifact |
15 | 25 | runs-on: ubuntu-latest |
| 26 | + permissions: |
| 27 | + contents: read |
| 28 | + pull-requests: read |
16 | 29 | steps: |
| 30 | + - name: Resolve PR |
| 31 | + id: pr |
| 32 | + uses: actions/github-script@v7 |
| 33 | + with: |
| 34 | + script: | |
| 35 | + const n = context.payload.pull_request?.number || Number(context.payload.inputs?.pr); |
| 36 | + if (!Number.isSafeInteger(n) || n < 1) throw new Error('Invalid PR number'); |
| 37 | + if (context.eventName === 'workflow_dispatch' && context.ref !== 'refs/heads/main') throw new Error('Dispatch from main only'); |
| 38 | + const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number:n}); |
| 39 | + if (pr.state !== 'open') throw new Error('PR is closed'); |
| 40 | + const expected = context.payload.pull_request?.head.sha || context.payload.inputs?.sha; |
| 41 | + if (expected !== pr.head.sha) throw new Error('PR changed before this build started; build the latest revision'); |
| 42 | + core.setOutput('number', String(n)); |
| 43 | + core.setOutput('sha', pr.head.sha); |
| 44 | + core.setOutput('repository', pr.head.repo.full_name); |
17 | 45 | - uses: actions/checkout@v4 |
| 46 | + with: |
| 47 | + repository: ${{ steps.pr.outputs.repository }} |
| 48 | + ref: ${{ steps.pr.outputs.sha }} |
| 49 | + persist-credentials: false |
18 | 50 | - uses: actions/setup-node@v4 |
19 | 51 | with: |
20 | | - node-version: '20' |
21 | | - cache: 'npm' |
| 52 | + node-version: '22' |
| 53 | + cache: npm |
22 | 54 | - run: npm ci |
23 | | - - run: npm run build |
24 | | - - name: Signal admin-may-merge for non-docs PRs |
25 | | - if: success() |
26 | | - uses: actions/github-script@v7 |
| 55 | + - run: npm run test:review --if-present |
| 56 | + - name: Build preview |
| 57 | + run: npm run build |
| 58 | + env: |
| 59 | + DOCUSAURUS_URL: https://usace-rmc.github.io |
| 60 | + DOCUSAURUS_BASE_URL: /RMC-Software-Documentation-Previews/pr-${{ steps.pr.outputs.number }}/ |
| 61 | + DOCUSAURUS_IS_PREVIEW: 'true' |
| 62 | + - uses: actions/upload-artifact@v4 |
27 | 63 | with: |
28 | | - script: | |
29 | | - const prNumber = context.payload.pull_request.number; |
30 | | - const files = await github.paginate(github.rest.pulls.listFiles, { |
31 | | - owner: context.repo.owner, |
32 | | - repo: context.repo.repo, |
33 | | - pull_number: prNumber, |
34 | | - per_page: 100, |
35 | | - }); |
36 | | - // Branch naming for this path is conventional, not enforced: |
37 | | - // feature/, fix/, chore/, ci/. What actually decides the lane is |
38 | | - // content -- a PR touching nothing under docs/ has no review lane |
39 | | - // and needs only this build to pass. |
40 | | - const touchesDocs = files.some(f => f.filename.startsWith('docs/')); |
41 | | - if (touchesDocs) return; // docs PRs are handled by stage-progression.yml |
42 | | -
|
43 | | - // Flip the review-workflow commit status to success so branch |
44 | | - // protection allows merge. This is the non-docs counterpart to |
45 | | - // stage-progression.yml's status-setting for docs PRs. |
46 | | - const headSha = context.payload.pull_request.head.sha; |
47 | | - await github.rest.repos.createCommitStatus({ |
48 | | - owner: context.repo.owner, |
49 | | - repo: context.repo.repo, |
50 | | - sha: headSha, |
51 | | - state: 'success', |
52 | | - context: 'review-workflow', |
53 | | - description: 'No doc changes — admin may merge', |
54 | | - }); |
55 | | -
|
56 | | - const marker = '<!-- ci-admin-merge-bot-comment -->'; |
57 | | - const sha = headSha.substring(0, 7); |
58 | | - 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.`; |
59 | | - const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }); |
60 | | - const existing = comments.data.find(c => c.user.type === 'Bot' && c.body.includes(marker)); |
61 | | - if (existing) { |
62 | | - await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body }); |
63 | | - } else { |
64 | | - await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body }); |
65 | | - } |
| 64 | + name: preview-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.sha }} |
| 65 | + path: build/ |
| 66 | + if-no-files-found: error |
| 67 | + retention-days: 7 |
| 68 | + outputs: |
| 69 | + pr: ${{ steps.pr.outputs.number }} |
| 70 | + sha: ${{ steps.pr.outputs.sha }} |
0 commit comments