Skip to content

Commit aa96a0a

Browse files
committed
feat: add auto-docs workflow and screenshot functionality for documentation updates
1 parent 88a8b89 commit aa96a0a

7 files changed

Lines changed: 2875 additions & 1888 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
You are updating the Glific documentation site (glific/docs) in response to a GitHub issue.
2+
3+
Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}
4+
5+
${ISSUE_BODY}
6+
7+
Two related product repositories have already been cloned, read-only, as subdirectories
8+
of this working directory:
9+
- repos/glific (backend, Elixir)
10+
- repos/glific-frontend (frontend, React)
11+
12+
Your task:
13+
14+
1. Investigate the issue above and, using the two repos, figure out what product
15+
behavior, screen, or feature it refers to. Look at recent commits, relevant source
16+
files, and README/CHANGELOG content in those repos for context.
17+
18+
2. Decide which existing doc page(s) under docs/ need updating, or whether a new page
19+
is needed. Follow the structure, numbering, and page conventions documented in this
20+
repo's CLAUDE.md.
21+
22+
3. Edit or create the minimal set of doc files needed, matching the conventions in
23+
CLAUDE.md and the neighboring pages in the same folder.
24+
25+
4. Wherever the doc should show a screenshot of the actual running app, insert a single
26+
placeholder line of this exact form (a later automated step replaces it with a real
27+
image — do not invent or guess an image path yourself):
28+
29+
![](SCREENSHOT:<short-slug>:<app-route-path>)
30+
31+
- <short-slug> is a short kebab-case identifier, unique within this change (e.g.
32+
"flow-editor-new-node").
33+
- <app-route-path> is the in-app route to screenshot, starting with "/" (e.g.
34+
"/flow/configure/123").
35+
- Only add these where a screenshot genuinely helps the reader; no more than 3.
36+
37+
5. If, after investigating, this issue does not actually require a documentation change,
38+
make NO file changes and instead write exactly the word "SKIP" as the first line of
39+
pr-body.md at the repo root, followed by a one-sentence explanation on the next line.
40+
41+
6. Otherwise, write a concise PR description (2-4 sentences: what changed and why,
42+
referencing the issue) to pr-body.md at the repo root. Body text only, no title.
43+
44+
Constraints:
45+
- Only create or edit files under docs/, plus the single file pr-body.md at the repo
46+
root. Do not touch workflow files, package.json, static/img/generated/, or anything
47+
under repos/.
48+
- Do not invent product behavior you can't find evidence for in the two repos; if
49+
uncertain, note the uncertainty in the doc text rather than guessing confidently.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Scans the docs/ files Claude just edited for `SCREENSHOT:<slug>:<route>` placeholders,
2+
// captures each route from the staging Glific instance, and rewrites the placeholder
3+
// with a real image path. No AI involved here — purely mechanical, run after the
4+
// Claude authoring step in .github/workflows/auto-docs.yml.
5+
//
6+
// Logs in once and reuses that single browser session for every screenshot in the run
7+
// (Glific auth is phone number + password — see src/containers/Auth/Login/Login.tsx
8+
// in glific-frontend: the field names are "phoneNumber" and "password", and the submit
9+
// button is data-testid="SubmitButton"; login finishes with a hard page redirect away
10+
// from /login rather than client-side routing).
11+
12+
import { execSync } from "node:child_process";
13+
import { chromium } from "playwright";
14+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
15+
import { dirname, relative } from "node:path";
16+
17+
const STAGING_URL = requireEnv("GLIFIC_STAGING_URL").replace(/\/+$/, "");
18+
const PHONE = requireEnv("GLIFIC_STAGING_PHONE");
19+
const PASSWORD = requireEnv("GLIFIC_STAGING_PASSWORD");
20+
const ISSUE_NUMBER = requireEnv("ISSUE_NUMBER");
21+
22+
const PLACEHOLDER_RE = /!\[\]\(SCREENSHOT:([a-z0-9-]+):([^)]+)\)/g;
23+
24+
function requireEnv(name) {
25+
const value = process.env[name];
26+
if (!value) {
27+
console.error(`Missing required env var ${name}`);
28+
process.exit(1);
29+
}
30+
return value;
31+
}
32+
33+
function changedDocFiles() {
34+
const output = execSync("git status --porcelain -- docs", {
35+
encoding: "utf8",
36+
});
37+
return output
38+
.split("\n")
39+
.map((line) => line.slice(3).trim())
40+
.filter((path) => path.endsWith(".md") || path.endsWith(".mdx"));
41+
}
42+
43+
function findPlaceholders(files) {
44+
const found = [];
45+
for (const file of files) {
46+
const content = readFileSync(file, "utf8");
47+
for (const match of content.matchAll(PLACEHOLDER_RE)) {
48+
found.push({ file, full: match[0], slug: match[1], route: match[2] });
49+
}
50+
}
51+
return found;
52+
}
53+
54+
function relativeImagePath(docFile, imagePath) {
55+
const rel = relative(dirname(docFile), imagePath);
56+
return rel.startsWith(".") ? rel : `./${rel}`;
57+
}
58+
59+
async function login(page) {
60+
await page.goto(`${STAGING_URL}/login`, { waitUntil: "networkidle" });
61+
await page.fill('input[name="phoneNumber"]', PHONE);
62+
await page.fill('input[name="password"]', PASSWORD);
63+
try {
64+
await Promise.all([
65+
page.waitForURL((url) => !url.pathname.includes("/login"), {
66+
timeout: 20_000,
67+
}),
68+
page.click('[data-testid="SubmitButton"]'),
69+
]);
70+
} catch (err) {
71+
throw new Error(
72+
`Login to staging Glific instance (${STAGING_URL}) did not leave /login within 20s — check GLIFIC_STAGING_PHONE/GLIFIC_STAGING_PASSWORD. Underlying error: ${err.message}`
73+
);
74+
}
75+
await page.waitForLoadState("networkidle");
76+
}
77+
78+
async function main() {
79+
const files = changedDocFiles();
80+
const placeholders = findPlaceholders(files);
81+
82+
if (placeholders.length === 0) {
83+
console.log("No SCREENSHOT: placeholders found, nothing to capture.");
84+
return;
85+
}
86+
87+
const browser = await chromium.launch();
88+
const page = await browser.newPage({
89+
viewport: { width: 1440, height: 900 },
90+
});
91+
92+
try {
93+
await login(page); // one session, reused for every capture below
94+
95+
const replacements = new Map(); // file -> [{full, markdown}]
96+
for (const { file, full, slug, route } of placeholders) {
97+
const outDir = `static/img/generated/${ISSUE_NUMBER}`;
98+
const outPath = `${outDir}/${slug}.png`;
99+
mkdirSync(outDir, { recursive: true });
100+
101+
console.log(`Capturing ${route} -> ${outPath}`);
102+
await page.goto(`${STAGING_URL}${route}`, { waitUntil: "networkidle" });
103+
await page.screenshot({ path: outPath });
104+
105+
const markdown = `![${slug}](${relativeImagePath(file, outPath)})`;
106+
const list = replacements.get(file) ?? [];
107+
list.push({ full, markdown });
108+
replacements.set(file, list);
109+
}
110+
111+
for (const [file, list] of replacements) {
112+
let content = readFileSync(file, "utf8");
113+
for (const { full, markdown } of list) {
114+
content = content.split(full).join(markdown);
115+
}
116+
writeFileSync(file, content);
117+
}
118+
} finally {
119+
await browser.close();
120+
}
121+
}
122+
123+
main().catch((err) => {
124+
console.error(err);
125+
process.exit(1);
126+
});

.github/workflows/auto-docs.yml

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
name: Auto Docs (label-triggered)
2+
3+
# Fires when an issue is labeled "auto-docs". Investigates the ticket against
4+
# the glific and glific-frontend repos, updates the relevant doc page(s) with
5+
# Claude, captures a fresh screenshot of the real app for any page that needs
6+
# one, and opens a PR for human review. Never auto-merges.
7+
8+
on:
9+
issues:
10+
types: [labeled]
11+
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
issues: write
16+
17+
jobs:
18+
auto-docs:
19+
if: github.event.label.name == 'auto-docs' && github.event.issue.pull_request == null
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout docs
23+
uses: actions/checkout@v4
24+
25+
- name: Checkout glific (backend, read-only context)
26+
uses: actions/checkout@v4
27+
with:
28+
repository: glific/glific
29+
path: repos/glific
30+
fetch-depth: 1
31+
32+
- name: Checkout glific-frontend (read-only context)
33+
uses: actions/checkout@v4
34+
with:
35+
repository: glific/glific-frontend
36+
path: repos/glific-frontend
37+
fetch-depth: 1
38+
39+
- name: Build prompt
40+
id: build_prompt
41+
env:
42+
ISSUE_TITLE: ${{ github.event.issue.title }}
43+
ISSUE_BODY: ${{ github.event.issue.body }}
44+
ISSUE_NUMBER: ${{ github.event.issue.number }}
45+
run: |
46+
envsubst < .github/scripts/docs-agent-prompt.md > /tmp/prompt.md
47+
{
48+
echo 'prompt<<EOF_PROMPT_9f3a1c'
49+
cat /tmp/prompt.md
50+
echo 'EOF_PROMPT_9f3a1c'
51+
} >> "$GITHUB_OUTPUT"
52+
53+
- name: Run Claude doc-authoring step
54+
uses: anthropics/claude-code-action@v1
55+
with:
56+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
57+
prompt: ${{ steps.build_prompt.outputs.prompt }}
58+
claude_args: |
59+
--max-turns 15
60+
--allowedTools "Read,Glob,Grep,Edit,Write"
61+
62+
- name: Remove cloned context repos (never committed, not screenshotted)
63+
run: rm -rf repos
64+
65+
- name: Guardrail - confirm Claude only touched docs/ and pr-body.md
66+
id: guardrail
67+
run: |
68+
# cut -c4- (not awk) because doc folder names contain spaces, e.g.
69+
# "docs/3. Product Features/..." — awk would truncate at the first space.
70+
changed=$(git status --porcelain | cut -c4-)
71+
bad=0
72+
while IFS= read -r f; do
73+
[ -z "$f" ] && continue
74+
case "$f" in
75+
*' -> '*) f="${f#*-> }" ;; # renames: check the destination path
76+
esac
77+
case "$f" in
78+
docs/*|pr-body.md) ;;
79+
*)
80+
echo "::error::Unexpected file changed outside docs/: $f"
81+
bad=1
82+
;;
83+
esac
84+
done <<< "$changed"
85+
if [ "$bad" = "1" ]; then
86+
echo "Claude's changes touched files outside the allowed scope. Aborting without committing." >&2
87+
exit 1
88+
fi
89+
90+
if [ -f pr-body.md ] && [ "$(head -n1 pr-body.md)" = "SKIP" ]; then
91+
echo "skip=true" >> "$GITHUB_OUTPUT"
92+
else
93+
echo "skip=false" >> "$GITHUB_OUTPUT"
94+
fi
95+
96+
- name: Comment when skipped
97+
if: steps.guardrail.outputs.skip == 'true'
98+
run: |
99+
reason=$(tail -n +2 pr-body.md)
100+
gh issue comment "${{ github.event.issue.number }}" --body "Auto-docs pipeline ran but made no changes: ${reason:-no docs update was needed for this ticket.}"
101+
rm -f pr-body.md
102+
env:
103+
GITHUB_TOKEN: ${{ github.token }}
104+
105+
- name: Set up Node
106+
if: steps.guardrail.outputs.skip == 'false'
107+
uses: actions/setup-node@v3
108+
with:
109+
node-version: 18
110+
cache: yarn
111+
112+
- name: Install dependencies
113+
if: steps.guardrail.outputs.skip == 'false'
114+
run: yarn install --frozen-lockfile
115+
116+
- name: Install Playwright browser
117+
if: steps.guardrail.outputs.skip == 'false'
118+
run: npx playwright install --with-deps chromium
119+
120+
- name: Take screenshots and rewrite placeholders
121+
if: steps.guardrail.outputs.skip == 'false'
122+
env:
123+
GLIFIC_STAGING_URL: ${{ secrets.GLIFIC_STAGING_URL }}
124+
GLIFIC_STAGING_PHONE: ${{ secrets.GLIFIC_STAGING_PHONE }}
125+
GLIFIC_STAGING_PASSWORD: ${{ secrets.GLIFIC_STAGING_PASSWORD }}
126+
ISSUE_NUMBER: ${{ github.event.issue.number }}
127+
run: node .github/scripts/take-screenshots.mjs
128+
129+
- name: Open PR
130+
if: steps.guardrail.outputs.skip == 'false'
131+
env:
132+
GITHUB_TOKEN: ${{ github.token }}
133+
ISSUE_NUMBER: ${{ github.event.issue.number }}
134+
run: |
135+
branch="docs/auto-${ISSUE_NUMBER}"
136+
git config user.name "github-actions[bot]"
137+
git config user.email "github-actions[bot]@users.noreply.github.com"
138+
git checkout -b "$branch"
139+
git add docs
140+
[ -d static/img/generated ] && git add static/img/generated
141+
git commit -m "docs: auto-update from issue #${ISSUE_NUMBER}"
142+
git push -u origin "$branch"
143+
144+
{
145+
cat pr-body.md
146+
echo ""
147+
echo "Closes #${ISSUE_NUMBER}"
148+
echo ""
149+
echo "_Opened automatically by the auto-docs pipeline. Please review before merging._"
150+
} > /tmp/pr-body-final.md
151+
rm -f pr-body.md
152+
153+
gh pr create \
154+
--title "docs: update from issue #${ISSUE_NUMBER}" \
155+
--body-file /tmp/pr-body-final.md \
156+
--label auto-docs \
157+
--base main \
158+
--head "$branch"
159+
160+
gh issue comment "$ISSUE_NUMBER" --body "Opened a docs PR for this: $(gh pr view "$branch" --json url -q .url)"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Dependencies
22
/node_modules
33

4+
# Sibling product repos cloned by the auto-docs workflow for context only
5+
/repos
6+
47
.env
58

69
# Production

0 commit comments

Comments
 (0)