Repository navigation
319 lines (289 loc) · 14.6 KB
/
Copy pathdocs-refresh.yml
File metadata and controls
319 lines (289 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Twice-monthly (1st & 15th) regeneration of package-level
# README.AUTOGEN.md companion files under ts/packages/**, opened as a pull
# request by the TypeAgent-Bot GitHub App.
#
# Per-PR regeneration is now handled inline by docs-pr-amend.yml, which
# commits the companions for the packages a PR changed straight onto that
# PR. This scheduled workflow is the backstop for what no single PR covers:
# fork PRs (which cannot be pushed to inline), inline runs that failed or
# were skipped, and repo-wide regenerations when the generator or its
# templates change - that shifts every package's content hash, so the next
# sweep refreshes them all. Because docs-autogen skips any package whose
# embedded content hash still matches its inputs, a run only opens a PR when
# something genuinely drifted.
#
# Why this shape (no federated credentials, no stored secrets):
#
# 1. LLM authoring -> GitHub Models. The AI-authored documentation body
# is generated by @typeagent/aiclient pointed at GitHub Models
# (https://models.github.ai/inference) instead of Azure OpenAI.
# aiclient's generic OpenAI path (OPENAI_ENDPOINT / OPENAI_API_KEY /
# OPENAI_MODEL, Bearer auth) needs no Azure endpoint, no Key Vault,
# and no Workload Identity Federation. The model is reached with the
# job's built-in GITHUB_TOKEN under `permissions: models: read` — no
# secret is stored anywhere.
#
# 2. PR identity -> TypeAgent-Bot. The branch is pushed with the native
# GITHUB_TOKEN (`contents: write` — pushing is NOT blocked by the org
# "Actions can't create PRs" policy), and the PR itself is opened with
# the TypeAgent-Bot GitHub App token. A PR authored by the App is not
# subject to that policy, and — unlike a GITHUB_TOKEN-opened PR — it
# DOES trigger downstream CI checks. The App is the same one that
# `fix-dependabot-alerts.yml` already uses, so no new App, variable, or
# secret has to be provisioned: it reuses the existing
# `DEPENDABOT_APP_ID` variable and `DEPENDABOT_APP_PRIVATE_KEY` secret.
#
# 3. Change gating. Generation is scoped to packages whose source changed
# since the last README.AUTOGEN.md commit on the checked-out branch
# (the `--since` baseline computed below) — i.e. only packages modified
# since their docs were last generated are regenerated. There is NO
# per-run package cap: the microsoft-tenant GitHub Models limits are
# ample (tens of thousands of requests / 10s, 10M tokens / min), so a
# run may regenerate anywhere from zero to every package. Each run
# reproduces the FULL current doc drift, so a superseding PR can never
# silently drop an unmerged package. README.md is never modified — only
# the parallel README.AUTOGEN.md companions, and a hard pre-PR gate
# refuses to open a PR if anything other than those files changed. A PR
# is opened only when at least one README.AUTOGEN.md actually changed.
#
# 4. Lifecycle. One run on the 1st and 15th. A human reviewer merges the
# PR; if a new run produces a fresh PR first, the previous open bot PR
# is closed (--delete-branch) so only one is ever live.
name: docs-refresh
on:
schedule:
# Twice a month: the 1st and 15th at 08:00 UTC.
- cron: "0 8 1,15 * *"
workflow_dispatch:
inputs:
dry-run:
description: "Dry run — analyse and render only, don't write or open a PR"
type: boolean
default: false
since:
description: "Override the diff baseline — git ref/SHA to diff against (blank = last README.AUTOGEN.md commit)"
type: string
default: ""
model:
description: "GitHub Models model id used for authoring"
type: string
default: "openai/gpt-4o"
# Never cancel an in-flight run; let it finish so a half-written PR is not
# left behind. Only one docs PR is ever in flight at a time.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
# Push the regenerated branch with the native GITHUB_TOKEN. The PR is
# opened by the TypeAgent-Bot App token, so GITHUB_TOKEN does NOT need
# `pull-requests: write`.
contents: write
# Reach GitHub Models inference with the built-in GITHUB_TOKEN.
models: read
jobs:
regenerate:
name: Regenerate docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full history so the --since baseline (last README.AUTOGEN.md
# commit) resolves and the diff against it is complete.
fetch-depth: 0
# Validate the free-text dispatch inputs that reach a shell before
# they are used. Strict allowlists prevent shell-metachar injection
# from a compromised workflow_dispatch actor.
- name: Validate dispatch inputs
env:
INPUT_SINCE: ${{ inputs.since }}
INPUT_MODEL: ${{ inputs.model }}
run: |
set -e
if [ -n "$INPUT_SINCE" ] && ! printf '%s' "$INPUT_SINCE" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
echo "::error::Invalid characters in 'since' input." >&2
exit 1
fi
# model is template-expanded into later run blocks (summary / PR
# body); allowlist it to block shell-metachar injection.
if [ -n "$INPUT_MODEL" ] && ! printf '%s' "$INPUT_MODEL" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
echo "::error::Invalid characters in 'model' input." >&2
exit 1
fi
# Resolve the diff baseline. An explicit `since` dispatch input wins;
# otherwise use the most recent commit that touched any
# README.AUTOGEN.md on the checked-out branch. Because the bot's
# commits only land on the default branch when a docs PR is MERGED,
# an unmerged PR leaves this baseline put — so the next run re-selects
# the same drifted packages (plus any new ones) and the superseding PR
# always carries the full set. In git pathspec a bare `*` matches `/`,
# so '*README.AUTOGEN.md' matches the file anywhere in the tree.
- name: Resolve diff baseline
id: baseline
env:
INPUT_SINCE: ${{ inputs.since }}
run: |
set -euo pipefail
if [ -n "$INPUT_SINCE" ]; then
SINCE="$INPUT_SINCE"
else
SINCE="$(git log -1 --format=%H -- '*README.AUTOGEN.md' || true)"
fi
echo "since=$SINCE" >> "$GITHUB_OUTPUT"
if [ -n "$SINCE" ]; then
echo "Diff baseline: $SINCE"
else
echo "No README.AUTOGEN.md history found — docs-autogen will do a first-run full sweep (bounded by --max-packages)."
fi
# Regenerate the companions for packages changed since the baseline.
# The shared composite installs deps, builds the docs-autogen CLI, runs
# it against GitHub Models, and prettier-formats the written files.
- name: Regenerate README.AUTOGEN.md files
uses: ./.github/actions/docs-generate
with:
since: ${{ steps.baseline.outputs.since }}
github-token: ${{ github.token }}
model: ${{ inputs.model || 'openai/gpt-4o' }}
dry-run: ${{ inputs.dry-run }}
# Decide whether anything changed. README.md is never touched, so we
# scope strictly to README.AUTOGEN.md. A clean tree => no PR.
- name: Detect changes
id: detect
if: ${{ inputs.dry-run != true }}
run: |
if git diff --quiet --exit-code -- 'ts/packages/**/README.AUTOGEN.md'; then
echo "changes=false" >> "$GITHUB_OUTPUT"
echo "No README.AUTOGEN.md changes after regeneration."
else
echo "changes=true" >> "$GITHUB_OUTPUT"
CHANGED=$(git diff --name-only -- 'ts/packages/**/README.AUTOGEN.md' | wc -l)
echo "changed_files=$CHANGED" >> "$GITHUB_OUTPUT"
echo "$CHANGED README.AUTOGEN.md file(s) modified."
fi
# Hard safety gate: refuse to open a PR unless EVERY changed/added
# path is a generated ts/packages/**/README.AUTOGEN.md companion.
# Catches any case where regeneration, prettier, or a stray artifact
# touched a non-doc file — source, config, README.md, lockfiles, etc.
# must all be untouched.
- name: Enforce only README.AUTOGEN.md changes
if: ${{ inputs.dry-run != true }}
run: |
set -euo pipefail
# Tracked modifications/deletions vs HEAD, plus untracked new files
# (gitignored paths like dist/ and node_modules/ are excluded by
# git's ignore rules / --exclude-standard).
CHANGED="$(git diff --name-only HEAD; git ls-files --others --exclude-standard)"
UNEXPECTED="$(printf '%s\n' "$CHANGED" | sed '/^$/d' | sort -u \
| grep -vE '^ts/packages/.+/README\.AUTOGEN\.md$' || true)"
if [ -n "$UNEXPECTED" ]; then
echo "::error::Refusing to open a PR — regeneration touched files other than ts/packages/**/README.AUTOGEN.md:" >&2
printf '%s\n' "$UNEXPECTED" >&2
exit 1
fi
echo "Validation passed: only README.AUTOGEN.md markdown companions changed."
# Mint the TypeAgent-Bot App token only when we are about to open a
# PR. The token is used for `gh pr` operations (create / list / close);
# the branch push below uses the native GITHUB_TOKEN persisted by
# actions/checkout.
- name: Generate TypeAgent-Bot token
id: app-token
if: ${{ steps.detect.outputs.changes == 'true' }}
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ vars.DEPENDABOT_APP_ID }}
private-key: ${{ secrets.DEPENDABOT_APP_PRIVATE_KEY }}
- name: Open or supersede the docs PR
if: ${{ steps.detect.outputs.changes == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
BRANCH="automated/docs-readmes-$(date +%Y%m%d)-${{ github.run_number }}"
git config user.name "TypeAgent-Bot"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
# Stage only the generated companions; README.md is never modified.
git add 'ts/packages/**/README.AUTOGEN.md'
git commit -m "docs: regenerate package README.AUTOGEN.md files ($(date +%Y-%m-%d))
Automated by the docs-refresh workflow. AI body authored via
GitHub Models; deterministic Reference computed from source.
${{ steps.detect.outputs.changed_files }} file(s) updated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
# Push with the native GITHUB_TOKEN persisted by actions/checkout
# (contents: write). Pushing a branch is not gated by the org
# "Actions can't create PRs" policy.
git push origin "$BRANCH"
# Supersede: close any previously-open bot PRs before opening the
# new one so only the freshest (full-drift) PR is ever live.
PREV_PRS=$(gh pr list \
--state open \
--search 'head:automated/docs-readmes- in:branch' \
--json number,headRefName \
--jq '.[] | select(.headRefName != "'"$BRANCH"'") | .number')
if [ -n "$PREV_PRS" ]; then
echo "Closing superseded docs PRs: $PREV_PRS"
for PR in $PREV_PRS; do
gh pr close "$PR" \
--delete-branch \
--comment "Superseded by a newer automated docs PR." \
|| echo "::warning::Failed to close PR #$PR"
done
fi
BODY=$(cat <<PREOF
## Automated package README.AUTOGEN.md regeneration
Opened by **TypeAgent-Bot**. The AI-authored documentation body is
generated via **GitHub Models** (no Azure OpenAI, no federated
credential, no stored secret); the deterministic Reference appendix
is computed from \`package.json\` + \`src/\` + the workspace graph.
### Summary
- **Files updated:** ${{ steps.detect.outputs.changed_files }} \`README.AUTOGEN.md\` file(s)
- **Model:** \`${{ inputs.model || 'openai/gpt-4o' }}\` (GitHub Models)
- **Per-run cap:** none (every drifted package regenerated)
- **Trigger:** ${{ github.event_name }}
### How this works
1. Diffs \`ts/packages/**\` source against the last
\`README.AUTOGEN.md\` commit, so only drifted packages are
regenerated and every run reproduces the full current drift.
2. \`README.md\` is never modified — read only as authoritative
source material the model mirrors and extends.
3. Validates every generated link resolves on disk; broken links
are stripped to plain text before write.
4. A PR is opened only when a \`README.AUTOGEN.md\` actually changed.
### Review checklist
- [ ] Spot-check a Reference section against the package.
- [ ] Spot-check the AI-authored prose (no hallucinated APIs).
- [ ] Confirm no hand-written \`README.md\` was modified.
> A CI gate already enforced that only \`README.AUTOGEN.md\` files
> are in this diff — no source, config, or other files were touched.
PREOF
)
gh pr create \
--base main \
--head "$BRANCH" \
--title "docs: regenerate package README.AUTOGEN.md files ($(date +%Y-%m-%d))" \
--body "$BODY" \
--label "documentation"
- name: Job summary
if: always()
run: |
{
echo "## Package README.AUTOGEN.md regeneration"
echo ""
echo "| Metric | Value |"
echo "|--------|-------|"
echo "| Trigger | ${{ github.event_name }} |"
echo "| Dry run | ${{ inputs.dry-run || 'false' }} |"
echo "| Model | ${{ inputs.model || 'openai/gpt-4o' }} (GitHub Models) |"
echo "| Diff baseline | ${{ steps.baseline.outputs.since || '(full sweep)' }} |"
echo "| Per-run cap | none |"
echo "| Changes detected | ${{ steps.detect.outputs.changes || 'false' }} |"
echo "| Files modified | ${{ steps.detect.outputs.changed_files || '0' }} |"
echo ""
if [ -f /tmp/docs-autogen.log ]; then
echo "### CLI output (truncated)"
echo '```'
tail -n 200 /tmp/docs-autogen.log || true
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"