microsoft/TypeAgent
Publicmirrored from https://github.com/microsoft/TypeAgentAvailable
.github/workflows/docs-generate.yml
309lines · modecode
| 1 | # Copyright (c) Microsoft Corporation. |
| 2 | # Licensed under the MIT License. |
| 3 | |
| 4 | # Daily regeneration of package-level READMEs under ts/packages/**. |
| 5 | # |
| 6 | # Detects packages whose source has changed since the last successful |
| 7 | # scheduled run (tracked by the `docs-bot/last-run` lightweight tag), |
| 8 | # rebuilds the AUTOGEN region in each affected README, runs link |
| 9 | # validation + Trademarks-block guard before writing, and opens a |
| 10 | # single batched PR. Prior open bot PRs are closed with --delete-branch |
| 11 | # so only one is ever live at a time. |
| 12 | # |
| 13 | # Authoring of the Overview section uses Azure OpenAI via |
| 14 | # packages/aiclient. Everything else is deterministic. |
| 15 | # |
| 16 | # Required repository configuration: |
| 17 | # variables: |
| 18 | # DOCS_BOT_APP_ID GitHub App that opens the PR |
| 19 | # secrets: |
| 20 | # DOCS_BOT_APP_PRIVATE_KEY Private key for the GitHub App |
| 21 | # AZURE_OPENAI_ENDPOINT Azure OpenAI endpoint URL |
| 22 | # AZURE_OPENAI_API_KEY Azure OpenAI key (or wire OIDC instead) |
| 23 | # |
| 24 | # The workflow is read-only and emits no PR until those are provisioned; |
| 25 | # it simply prints what it would have done. |
| 26 | |
| 27 | name: docs-generate |
| 28 | |
| 29 | on: |
| 30 | schedule: |
| 31 | # Daily at 08:00 UTC = 01:00 PST. Low-traffic window so churn from |
| 32 | # this PR does not collide with other automated PRs. |
| 33 | - cron: "0 8 * * *" |
| 34 | workflow_dispatch: |
| 35 | inputs: |
| 36 | dry-run: |
| 37 | description: "Dry run — analyse and render only, don't write or open PR" |
| 38 | type: boolean |
| 39 | default: false |
| 40 | packages: |
| 41 | description: "Comma-separated package names to regenerate (overrides change detection)" |
| 42 | type: string |
| 43 | default: "" |
| 44 | since: |
| 45 | description: "Override the watermark — git ref to diff against (e.g. main, HEAD~10)" |
| 46 | type: string |
| 47 | default: "" |
| 48 | llm: |
| 49 | description: "Use Azure OpenAI to author the Overview section (skeleton-only when off)" |
| 50 | type: boolean |
| 51 | default: true |
| 52 | max-packages: |
| 53 | description: "Per-run cap on packages touched (defaults to 25)" |
| 54 | type: string |
| 55 | default: "25" |
| 56 | |
| 57 | # Never cancel an in-flight docs-autogen run; let it finish and let the |
| 58 | # next scheduled run supersede the resulting PR if necessary. |
| 59 | concurrency: |
| 60 | group: ${{ github.workflow }} |
| 61 | cancel-in-progress: false |
| 62 | |
| 63 | permissions: |
| 64 | contents: write |
| 65 | pull-requests: write |
| 66 | |
| 67 | jobs: |
| 68 | regenerate: |
| 69 | runs-on: ubuntu-latest |
| 70 | |
| 71 | steps: |
| 72 | - uses: actions/checkout@v4 |
| 73 | with: |
| 74 | fetch-depth: 0 |
| 75 | |
| 76 | - uses: pnpm/action-setup@v4 |
| 77 | name: Install pnpm |
| 78 | with: |
| 79 | package_json_file: ts/package.json |
| 80 | |
| 81 | - uses: actions/setup-node@v4 |
| 82 | with: |
| 83 | node-version: 22 |
| 84 | cache: "pnpm" |
| 85 | cache-dependency-path: ts/pnpm-lock.yaml |
| 86 | |
| 87 | - name: Generate GitHub App token |
| 88 | id: app-token |
| 89 | uses: actions/create-github-app-token@v1 |
| 90 | with: |
| 91 | app-id: ${{ vars.DOCS_BOT_APP_ID }} |
| 92 | private-key: ${{ secrets.DOCS_BOT_APP_PRIVATE_KEY }} |
| 93 | |
| 94 | - name: Install ts dependencies |
| 95 | working-directory: ts |
| 96 | run: | |
| 97 | corepack enable |
| 98 | pnpm install --frozen-lockfile |
| 99 | |
| 100 | - name: Build docs-autogen tool |
| 101 | working-directory: ts |
| 102 | run: | |
| 103 | pnpm --filter aiclient build |
| 104 | pnpm --filter @typeagent/docs-autogen build |
| 105 | |
| 106 | # Compose the CLI flags from dispatch inputs once so each |
| 107 | # subsequent step uses the same set. `--write` is omitted on |
| 108 | # dry-run; otherwise the CLI writes to disk in-place and we |
| 109 | # detect the diff afterwards. |
| 110 | - name: Compose CLI flags |
| 111 | id: flags |
| 112 | env: |
| 113 | INPUT_PACKAGES: ${{ inputs.packages }} |
| 114 | INPUT_SINCE: ${{ inputs.since }} |
| 115 | INPUT_LLM: ${{ inputs.llm }} |
| 116 | INPUT_MAX_PACKAGES: ${{ inputs.max-packages }} |
| 117 | INPUT_DRY_RUN: ${{ inputs.dry-run }} |
| 118 | run: | |
| 119 | FLAGS="--render" |
| 120 | if [ "$INPUT_DRY_RUN" != "true" ]; then |
| 121 | FLAGS="$FLAGS --write" |
| 122 | else |
| 123 | FLAGS="$FLAGS --dry-run" |
| 124 | fi |
| 125 | if [ -n "$INPUT_PACKAGES" ]; then |
| 126 | # Convert comma-separated list into repeated --package flags. |
| 127 | IFS=',' read -ra PKGS <<< "$INPUT_PACKAGES" |
| 128 | for PKG in "${PKGS[@]}"; do |
| 129 | PKG_TRIMMED=$(echo "$PKG" | xargs) |
| 130 | if [ -n "$PKG_TRIMMED" ]; then |
| 131 | FLAGS="$FLAGS --package $PKG_TRIMMED" |
| 132 | fi |
| 133 | done |
| 134 | fi |
| 135 | if [ -n "$INPUT_SINCE" ]; then |
| 136 | FLAGS="$FLAGS --since $INPUT_SINCE" |
| 137 | fi |
| 138 | if [ "$INPUT_LLM" = "true" ]; then |
| 139 | FLAGS="$FLAGS --llm" |
| 140 | fi |
| 141 | if [ -n "$INPUT_MAX_PACKAGES" ]; then |
| 142 | FLAGS="$FLAGS --max-packages $INPUT_MAX_PACKAGES" |
| 143 | fi |
| 144 | echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" |
| 145 | echo "Composed CLI flags: $FLAGS" |
| 146 | |
| 147 | - name: Regenerate package READMEs |
| 148 | id: regen |
| 149 | working-directory: ts |
| 150 | env: |
| 151 | AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} |
| 152 | AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} |
| 153 | DEBUG: "docs-autogen:*" |
| 154 | run: | |
| 155 | # Run with stdout captured for the job summary; stderr passes |
| 156 | # through so diagnostics are visible in the live log. |
| 157 | set -o pipefail |
| 158 | node tools/docsAutogen/dist/cli.js ${{ steps.flags.outputs.flags }} \ |
| 159 | | tee /tmp/docs-autogen.log |
| 160 | echo "exit_code=$?" >> "$GITHUB_OUTPUT" |
| 161 | |
| 162 | # Detect whether anything actually changed under ts/packages/**. |
| 163 | # When the CLI ran with --write, all README edits are already on |
| 164 | # disk. We rely on git status alone — if it's clean there's no PR |
| 165 | # to open, even if the CLI processed packages (footer-only or |
| 166 | # unchanged verdicts skip writes). |
| 167 | - name: Detect changes |
| 168 | id: detect |
| 169 | working-directory: ts |
| 170 | run: | |
| 171 | if git diff --quiet --exit-code -- 'packages/**/README.md'; then |
| 172 | echo "changes=false" >> "$GITHUB_OUTPUT" |
| 173 | echo "No README changes after regeneration." |
| 174 | else |
| 175 | echo "changes=true" >> "$GITHUB_OUTPUT" |
| 176 | CHANGED=$(git diff --name-only -- 'packages/**/README.md' | wc -l) |
| 177 | echo "changed_files=$CHANGED" >> "$GITHUB_OUTPUT" |
| 178 | echo "$CHANGED README(s) modified." |
| 179 | fi |
| 180 | |
| 181 | # ── Create PR (and supersede prior bot PRs) ───────────────────── |
| 182 | - name: Create pull request |
| 183 | if: ${{ steps.detect.outputs.changes == 'true' && inputs.dry-run != true }} |
| 184 | env: |
| 185 | GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 186 | run: | |
| 187 | BRANCH="automated/docs-readmes-$(date +%Y%m%d)-${{ github.run_number }}" |
| 188 | |
| 189 | git config user.name "github-actions[bot]" |
| 190 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
| 191 | |
| 192 | git checkout -b "$BRANCH" |
| 193 | # Scope strictly to ts/packages/**/README.md to avoid sweeping |
| 194 | # in any incidental working-tree noise from CI. |
| 195 | git add 'ts/packages/**/README.md' |
| 196 | CHANGED_LIST=$(git diff --cached --name-only) |
| 197 | |
| 198 | git commit -m "docs: regenerate package READMEs ($(date +%Y-%m-%d)) |
| 199 | |
| 200 | Automated by docs-generate workflow. |
| 201 | |
| 202 | ${{ steps.detect.outputs.changed_files }} file(s) updated. |
| 203 | |
| 204 | Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
| 205 | Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" |
| 206 | |
| 207 | git push origin "$BRANCH" |
| 208 | |
| 209 | # Close any previously-open bot PRs before opening the new |
| 210 | # one. Each daily run produces a unique branch, so without |
| 211 | # dedup the repo accumulates stacking duplicate PRs whenever |
| 212 | # yesterday's hasn't been merged. Always keep the freshest. |
| 213 | PREV_PRS=$(gh pr list \ |
| 214 | --state open \ |
| 215 | --search 'head:automated/docs-readmes- in:branch' \ |
| 216 | --json number,headRefName \ |
| 217 | --jq '.[] | select(.headRefName != "'"$BRANCH"'") | .number') |
| 218 | if [ -n "$PREV_PRS" ]; then |
| 219 | echo "Closing superseded docs-autogen PRs: $PREV_PRS" |
| 220 | for PR in $PREV_PRS; do |
| 221 | gh pr close "$PR" \ |
| 222 | --delete-branch \ |
| 223 | --comment "Superseded by a newer automated docs PR." \ |
| 224 | || echo "::warning::Failed to close PR #$PR" |
| 225 | done |
| 226 | fi |
| 227 | |
| 228 | BODY=$(cat <<PREOF |
| 229 | ## Automated package README regeneration |
| 230 | |
| 231 | This PR was automatically generated by the \`docs-generate\` workflow. |
| 232 | |
| 233 | ### Summary |
| 234 | - **Files updated:** ${{ steps.detect.outputs.changed_files }} |
| 235 | - **LLM mode:** ${{ inputs.llm == false && 'skeleton-only' || 'aiclient (Overview only)' }} |
| 236 | - **Per-run cap:** ${{ inputs.max-packages || '25' }} package(s) |
| 237 | - **Trigger:** ${{ github.event_name == 'schedule' && 'scheduled (daily)' || format('manual ({0})', github.actor) }} |
| 238 | |
| 239 | ### Changed READMEs |
| 240 | \`\`\` |
| 241 | $CHANGED_LIST |
| 242 | \`\`\` |
| 243 | |
| 244 | ### How this works |
| 245 | 1. Diffs \`ts/packages/**\` source files against the last successful run |
| 246 | (tracked by the \`docs-bot/last-run\` git tag). |
| 247 | 2. For each affected package, regenerates the AUTOGEN region of its |
| 248 | README — Reference section deterministically, Overview section |
| 249 | via Azure OpenAI when enabled. |
| 250 | 3. Validates every generated link resolves on disk and that the |
| 251 | canonical Trademarks block survives intact; refuses to write |
| 252 | otherwise. |
| 253 | 4. Skips packages whose new content differs only in the staleness |
| 254 | footer (so daily PRs don't churn unchanged docs). |
| 255 | |
| 256 | ### Review checklist |
| 257 | - [ ] Sample one or two READMEs and confirm the Reference section |
| 258 | accurately describes the package. |
| 259 | - [ ] Sample one Overview and confirm it reads as contributor-grade |
| 260 | documentation (not marketing prose, no hallucinated APIs). |
| 261 | - [ ] Confirm no \`## Trademarks\` block has been altered. |
| 262 | |
| 263 | See [\`ts/docs/architecture/doc-autogen.md\`](../blob/main/ts/docs/architecture/doc-autogen.md) for design details. |
| 264 | PREOF |
| 265 | ) |
| 266 | |
| 267 | gh pr create \ |
| 268 | --base main \ |
| 269 | --head "$BRANCH" \ |
| 270 | --title "docs: regenerate package READMEs ($(date +%Y-%m-%d))" \ |
| 271 | --body "$BODY" \ |
| 272 | --label "documentation" |
| 273 | |
| 274 | # Advance the watermark only on a successful scheduled run with a |
| 275 | # PR. Manual dispatches and dry-runs intentionally do not move |
| 276 | # the tag, so they remain idempotent against the daily cron. |
| 277 | - name: Advance watermark tag |
| 278 | if: ${{ github.event_name == 'schedule' && steps.detect.outputs.changes == 'true' && inputs.dry-run != true }} |
| 279 | env: |
| 280 | GH_TOKEN: ${{ steps.app-token.outputs.token }} |
| 281 | run: | |
| 282 | # Tag the commit we generated against (the original SHA before |
| 283 | # the PR branch was created), not the branch tip. |
| 284 | git tag -f docs-bot/last-run ${{ github.sha }} |
| 285 | git push origin docs-bot/last-run --force |
| 286 | |
| 287 | # ── Job summary ───────────────────────────────────────────────── |
| 288 | - name: Job summary |
| 289 | if: always() |
| 290 | run: | |
| 291 | { |
| 292 | echo "## Package README regeneration" |
| 293 | echo "" |
| 294 | echo "| Metric | Value |" |
| 295 | echo "|--------|-------|" |
| 296 | echo "| Trigger | ${{ github.event_name }} |" |
| 297 | echo "| Dry run | ${{ inputs.dry-run || 'false' }} |" |
| 298 | echo "| LLM enabled | ${{ inputs.llm || 'true' }} |" |
| 299 | echo "| Per-run cap | ${{ inputs.max-packages || '25' }} |" |
| 300 | echo "| Changes detected | ${{ steps.detect.outputs.changes || 'false' }} |" |
| 301 | echo "| Files modified | ${{ steps.detect.outputs.changed_files || '0' }} |" |
| 302 | echo "" |
| 303 | if [ -f /tmp/docs-autogen.log ]; then |
| 304 | echo "### CLI output (truncated)" |
| 305 | echo '```' |
| 306 | tail -n 200 /tmp/docs-autogen.log || true |
| 307 | echo '```' |
| 308 | fi |
| 309 | } >> "$GITHUB_STEP_SUMMARY" |
| 310 | |