Skip to content

Commit c05aed0

Browse files
Copilotmattleibow
andauthored
Replace monolithic docs workflow with staged deployment system (#362)
--------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
1 parent eaa1148 commit c05aed0

File tree

10 files changed

+535
-64
lines changed

10 files changed

+535
-64
lines changed

.github/workflows/builds-docs.yml

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: "Docs - PR Staging - Cleanup"
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
types: [ closed ]
7+
8+
# Default to no permissions — each job declares only what it needs.
9+
permissions: {}
10+
11+
# Ensure documentation workflows run sequentially
12+
concurrency:
13+
group: "docs-deployment"
14+
cancel-in-progress: false
15+
16+
jobs:
17+
cleanup:
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: write # push to gh-pages
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Remove staging directory from gh-pages
28+
env:
29+
PR_NUMBER: ${{ github.event.number }}
30+
run: |
31+
# Validate PR_NUMBER is a positive integer before using in paths
32+
if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
33+
echo "Invalid PR number: $PR_NUMBER"
34+
exit 1
35+
fi
36+
37+
# Switch to gh-pages branch; exit cleanly if it doesn't exist yet
38+
if ! git fetch origin gh-pages:gh-pages 2>/dev/null; then
39+
echo "gh-pages branch does not exist, nothing to clean up"
40+
exit 0
41+
fi
42+
if ! git checkout gh-pages 2>/dev/null; then
43+
echo "Failed to checkout gh-pages, nothing to clean up"
44+
exit 0
45+
fi
46+
47+
if [ -d "staging/$PR_NUMBER" ]; then
48+
rm -rf staging/$PR_NUMBER
49+
git config --local user.email "action@github.com"
50+
git config --local user.name "GitHub Action"
51+
git add -A
52+
if ! git diff --cached --quiet; then
53+
git commit -m "Remove staging docs for PR #$PR_NUMBER"
54+
git push origin gh-pages
55+
echo "Removed staging directory for PR #$PR_NUMBER"
56+
else
57+
echo "No changes to commit for PR #$PR_NUMBER"
58+
fi
59+
else
60+
echo "No staging directory found for PR #$PR_NUMBER"
61+
fi
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
name: "Docs - Fork PR - Deploy"
2+
3+
# This workflow runs in a TRUSTED context (base repo token with write access).
4+
# It is triggered AFTER "Docs - Deploy" completes for a fork PR.
5+
# The "Docs - Deploy" build job uploads the site artifact; this workflow
6+
# downloads it and deploys to gh-pages/staging/{pr}/, then comments on the PR.
7+
#
8+
# Security: no untrusted code is executed here — we only deploy a pre-built
9+
# artifact produced by our own workflow. The workflow_run trigger always runs
10+
# with the base repository's token, regardless of where the PR originated.
11+
12+
on:
13+
workflow_run:
14+
workflows: [ "Docs - Deploy" ]
15+
types: [ completed ]
16+
17+
# Default to no permissions — each job declares only what it needs.
18+
permissions: {}
19+
20+
# Ensure documentation workflows run sequentially
21+
concurrency:
22+
group: "docs-deployment"
23+
cancel-in-progress: false
24+
25+
jobs:
26+
deploy-fork-pr-staging:
27+
# Only run for successful fork PR builds
28+
if: |
29+
github.event.workflow_run.conclusion == 'success' &&
30+
github.event.workflow_run.event == 'pull_request' &&
31+
github.event.workflow_run.head_repository.full_name != github.repository
32+
runs-on: ubuntu-latest
33+
permissions:
34+
contents: write # push to gh-pages
35+
pull-requests: write # comment on PR
36+
steps:
37+
- name: Find staging artifact for this PR
38+
id: artifact
39+
uses: actions/github-script@v7
40+
env:
41+
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
42+
with:
43+
script: |
44+
const runId = parseInt(process.env.WORKFLOW_RUN_ID, 10);
45+
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
46+
owner: context.repo.owner,
47+
repo: context.repo.repo,
48+
run_id: runId
49+
});
50+
const staging = artifacts.data.artifacts.find(a => a.name.startsWith('staging-site-'));
51+
if (!staging) {
52+
core.setFailed('No staging artifact found for this workflow run');
53+
return;
54+
}
55+
// Extract and validate PR number (must be a positive integer)
56+
const prNumberStr = staging.name.replace('staging-site-', '');
57+
const prNumber = parseInt(prNumberStr, 10);
58+
if (!prNumber || prNumber <= 0 || String(prNumber) !== prNumberStr) {
59+
core.setFailed(`Invalid PR number extracted from artifact name: ${staging.name}`);
60+
return;
61+
}
62+
core.setOutput('artifact_name', staging.name);
63+
core.setOutput('pr_number', String(prNumber));
64+
65+
- name: Download staging artifact
66+
uses: actions/download-artifact@v4
67+
with:
68+
run-id: ${{ github.event.workflow_run.id }}
69+
github-token: ${{ secrets.GITHUB_TOKEN }}
70+
name: ${{ steps.artifact.outputs.artifact_name }}
71+
path: /tmp/site-temp
72+
73+
- name: Checkout gh-pages branch
74+
uses: actions/checkout@v4
75+
with:
76+
ref: gh-pages
77+
fetch-depth: 0
78+
79+
- name: Deploy staging content
80+
env:
81+
PR_NUMBER: ${{ steps.artifact.outputs.pr_number }}
82+
run: |
83+
# Validate PR_NUMBER is a positive integer before using in paths
84+
if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
85+
echo "Invalid PR number: $PR_NUMBER"
86+
exit 1
87+
fi
88+
mkdir -p staging/$PR_NUMBER
89+
rm -rf staging/$PR_NUMBER/*
90+
cp -r /tmp/site-temp/. staging/$PR_NUMBER/
91+
rm -rf /tmp/site-temp
92+
git config --local user.email "action@github.com"
93+
git config --local user.name "GitHub Action"
94+
git add staging/$PR_NUMBER
95+
if ! git diff --cached --quiet; then
96+
git commit -m "Deploy staging docs for PR #$PR_NUMBER"
97+
git push origin gh-pages
98+
else
99+
echo "No changes to commit"
100+
fi
101+
102+
- name: Comment on PR
103+
uses: actions/github-script@v7
104+
env:
105+
PR_NUMBER: ${{ steps.artifact.outputs.pr_number }}
106+
with:
107+
script: |
108+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
109+
if (!prNumber || prNumber <= 0) {
110+
core.setFailed('Invalid PR number');
111+
return;
112+
}
113+
const stagingUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/`;
114+
const sampleUrl = `https://mono.github.io/SkiaSharp.Extended/staging/${prNumber}/sample/`;
115+
116+
// Only comment once; check for an existing bot comment
117+
const comments = await github.rest.issues.listComments({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
issue_number: prNumber
121+
});
122+
const botComment = comments.data.find(c =>
123+
c.user.type === 'Bot' &&
124+
c.body.includes('Documentation Preview')
125+
);
126+
if (botComment) {
127+
console.log('Bot comment already exists, skipping.');
128+
return;
129+
}
130+
131+
const commentBody = [
132+
'📖 **Documentation Preview**',
133+
'',
134+
'The documentation for this PR has been deployed and is available at:',
135+
'',
136+
`🔗 **[View Staging Documentation](${stagingUrl})**`,
137+
'',
138+
`🔗 **[View Staging Blazor Sample](${sampleUrl})**`,
139+
'',
140+
'This preview will be updated automatically when you push new commits to this PR.',
141+
'',
142+
'---',
143+
'*This comment is automatically updated by the documentation staging workflow.*'
144+
].join('\n');
145+
146+
await github.rest.issues.createComment({
147+
owner: context.repo.owner,
148+
repo: context.repo.repo,
149+
issue_number: prNumber,
150+
body: commentBody
151+
});
152+

0 commit comments

Comments
 (0)