Repo sync #35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 'Sync Copilot SDK docs' | |
| on: | |
| # Event-driven sync — triggered by copilot-sdk when docs/ changes are pushed | |
| repository_dispatch: | |
| types: [sync-sdk-docs] | |
| # Manual trigger for on-demand syncs and testing | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Dry run — run normalization but skip push and PR creation' | |
| required: false | |
| default: 'false' | |
| type: boolean | |
| source_ref: | |
| description: 'copilot-sdk ref to sync from (branch, tag, or SHA). Defaults to main.' | |
| required: false | |
| default: 'main' | |
| type: string | |
| # PR validation — dry-run only, verifies scripts work on CI | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - '.github/workflows/sync-sdk-docs.yml' | |
| - 'content/copilot/get-started/sdk-quickstart.md' | |
| - 'src/workflows/sync-sdk-docs/**' | |
| concurrency: | |
| group: sync-sdk-docs-${{ github.event_name }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| sync-docs: | |
| name: 'Sync SDK docs' | |
| if: github.repository == 'github/docs-internal' | |
| runs-on: ubuntu-latest | |
| env: | |
| SDK_DOCS_TARGET: content/copilot/how-tos/copilot-sdk | |
| SYNC_BRANCH: sdk-docs/auto-sync | |
| ASSETS_TARGET: assets/images/help/copilot/copilot-sdk | |
| SOURCE_REF: ${{ inputs.source_ref || 'main' }} | |
| steps: | |
| - name: Checkout docs-internal | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # `preserve-redirects.ts` reads the pre-sync state from `git HEAD` to learn | |
| # which URLs are currently live. That is only a valid baseline when HEAD is | |
| # the published branch. `pull_request` runs are safe because they never | |
| # publish, but a `workflow_dispatch` from another branch would publish while | |
| # comparing against that branch's tree, writing stale redirects into a real | |
| # PR. Fail early rather than let the run reach the push step. | |
| - name: Verify publishing runs start from the default branch | |
| if: github.event_name != 'pull_request' && inputs.dry_run != 'true' | |
| run: | | |
| if [ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]; then | |
| echo "::error::This run would push and open a PR, but it started from" \ | |
| "'$GITHUB_REF_NAME' rather than the default branch '$DEFAULT_BRANCH'." \ | |
| "HEAD is the baseline for redirect preservation, so syncing from another" \ | |
| "branch can drop or invent redirects. Re-run from '$DEFAULT_BRANCH', or" \ | |
| "use the dry_run input to test from a branch." | |
| exit 1 | |
| fi | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| - name: Fetch SDK docs from copilot-sdk | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| run: | | |
| set -e | |
| tmp=$(mktemp -d) | |
| echo "Cloning copilot-sdk@$SOURCE_REF (sparse: docs/ only)..." | |
| git clone --depth 1 --branch "$SOURCE_REF" --filter=blob:none --sparse \ | |
| "https://x-access-token:${GH_TOKEN}@github.com/github/copilot-sdk.git" "$tmp" 2>&1 | tail -1 | |
| cd "$tmp" && git sparse-checkout set docs 2>/dev/null | |
| SDK_SHA=$(git -C "$tmp" rev-parse HEAD) | |
| echo "SDK_SHA=$SDK_SHA" >> "$GITHUB_ENV" | |
| echo "SDK_TMP=$tmp" >> "$GITHUB_ENV" | |
| echo "Fetched copilot-sdk@${SDK_SHA::7}" | |
| - name: Set up Node and dependencies | |
| uses: ./.github/actions/node-npm-setup | |
| - name: Install mermaid-cli | |
| run: npm install -g @mermaid-js/mermaid-cli@11 | |
| - name: Clean existing SDK docs | |
| run: | | |
| rm -rf "$SDK_DOCS_TARGET" | |
| - name: Copy SDK docs | |
| run: | | |
| mkdir -p "$SDK_DOCS_TARGET" | |
| # Pages relocated out of this tree into hand-authored content are not | |
| # excluded here — they are removed by the RELOCATED_PAGES map in | |
| # src/workflows/sync-sdk-docs/normalize-sdk-docs.ts, which also | |
| # repoints inbound links at their new URLs. | |
| rsync -av --exclude='.validation/' --exclude='developer-docs/' "$SDK_TMP/docs/" "$SDK_DOCS_TARGET/" | |
| echo "Copied $(find "$SDK_DOCS_TARGET" -name '*.md' | wc -l | tr -d ' ') markdown files" | |
| - name: Normalize content | |
| run: | | |
| npx tsx src/workflows/sync-sdk-docs/normalize-sdk-docs.ts \ | |
| --content-dir content \ | |
| --sdk-docs-dir "$SDK_DOCS_TARGET" | |
| - name: Preserve redirects | |
| run: | | |
| # `--git-ref HEAD` is the record of which URLs are currently live. On | |
| # publishing runs a preceding step has verified HEAD is the default | |
| # branch, and the sync branch is only created later with `checkout -B`. | |
| # On `pull_request` runs HEAD is the merge commit instead, which is fine | |
| # because those runs are dry-run only. | |
| # | |
| # A removed page that needs a redirect decision should still produce a | |
| # PR, because the PR is where that decision gets made and committed. The | |
| # script still writes the at-risk URLs and a copy-pasteable | |
| # `redirect_from` block to the run summary either way. | |
| npx tsx src/workflows/sync-sdk-docs/preserve-redirects.ts \ | |
| --sdk-docs-dir "$SDK_DOCS_TARGET" \ | |
| --git-ref HEAD | |
| - name: Convert Mermaid diagrams to PNG | |
| env: | |
| PUPPETEER_CHROMIUM_REVISION: '' | |
| run: | | |
| # Puppeteer needs --no-sandbox on GitHub Actions runners | |
| echo '{ "args": ["--no-sandbox", "--disable-setuid-sandbox"] }' > /tmp/puppeteer-config.json | |
| npx tsx src/workflows/sync-sdk-docs/convert-mermaid.ts \ | |
| --sdk-docs-dir "$SDK_DOCS_TARGET" \ | |
| --assets-dir "$ASSETS_TARGET" \ | |
| --repo-root . \ | |
| --puppeteer-config /tmp/puppeteer-config.json | |
| - name: Remove stale SDK assets | |
| run: | | |
| if [ ! -d "$ASSETS_TARGET" ]; then | |
| echo "No assets directory — nothing to clean." | |
| exit 0 | |
| fi | |
| # Collect image filenames referenced in the current SDK docs | |
| REFERENCED=$(grep -roh '/assets/images/help/copilot/copilot-sdk/[^)]*' "$SDK_DOCS_TARGET" \ | |
| | sed 's|.*/||' | sort -u) | |
| STALE=0 | |
| for file in "$ASSETS_TARGET"/*.png; do | |
| [ -f "$file" ] || continue | |
| BASENAME=$(basename "$file") | |
| if ! echo "$REFERENCED" | grep -qxF "$BASENAME"; then | |
| echo " STALE: $BASENAME" | |
| rm "$file" | |
| STALE=$((STALE + 1)) | |
| fi | |
| done | |
| echo "Removed $STALE stale asset(s)." | |
| - name: Lint SDK content (non-blocking) | |
| continue-on-error: true | |
| run: | | |
| SDK_FILES=$(find "$SDK_DOCS_TARGET" -name '*.md' | tr '\n' ' ') | |
| if [ -z "$SDK_FILES" ]; then | |
| echo "No SDK markdown files to lint." | |
| exit 0 | |
| fi | |
| echo "Linting $(echo "$SDK_FILES" | wc -w | tr -d ' ') SDK files..." | |
| if npm run lint-content -- --paths $SDK_FILES; then | |
| echo "✅ Content linter passed — no issues found." | |
| else | |
| echo "" | |
| echo "⚠️ Content linter found issues (see above)." | |
| echo "These will appear as CI failures on the generated PR." | |
| echo "Fix them in the copilot-sdk source docs or update the normalize script." | |
| fi | |
| - name: Show diff summary | |
| run: | | |
| git add -A | |
| if git diff --cached --quiet; then | |
| echo "No changes detected." | |
| echo "has_changes=false" >> "$GITHUB_ENV" | |
| else | |
| echo "has_changes=true" >> "$GITHUB_ENV" | |
| echo "Changes detected:" | |
| git diff --cached --stat | |
| fi | |
| # --- PR-only: upload artifacts for review --- | |
| - name: Upload normalized docs (PR validation) | |
| if: github.event_name == 'pull_request' | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: normalized-sdk-docs | |
| path: | | |
| ${{ env.SDK_DOCS_TARGET }} | |
| ${{ env.ASSETS_TARGET }} | |
| retention-days: 7 | |
| # --- Push and PR (only on dispatch, not dry-run, and changes exist) --- | |
| - name: Commit and push | |
| if: >- | |
| env.has_changes == 'true' | |
| && github.event_name != 'pull_request' | |
| && inputs.dry_run != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # Fetch the sync branch if it exists so force-with-lease knows the remote state | |
| git fetch origin "$SYNC_BRANCH" 2>/dev/null || true | |
| git checkout -B "$SYNC_BRANCH" | |
| git commit -m "Sync Copilot SDK docs from copilot-sdk@${SDK_SHA::7} | |
| Source commit: https://github.com/github/copilot-sdk/commit/$SDK_SHA | |
| This commit was automatically generated by the sync-sdk-docs workflow." | |
| git push --force-with-lease origin "$SYNC_BRANCH" | |
| - name: Create or update pull request | |
| if: >- | |
| env.has_changes == 'true' | |
| && github.event_name != 'pull_request' | |
| && inputs.dry_run != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| run: | | |
| PR_TITLE="Sync Copilot SDK docs (auto-generated)" | |
| PR_BODY="## Summary | |
| This PR syncs documentation from [\`github/copilot-sdk/docs\`](https://github.com/github/copilot-sdk/tree/main/docs) to \`content/copilot/how-tos/copilot-sdk/\`. | |
| **Source commit:** https://github.com/github/copilot-sdk/commit/$SDK_SHA | |
| ### What this PR does | |
| - Copies SDK documentation from \`copilot-sdk\` | |
| - Adds YAML frontmatter for docs.github.com publishing | |
| - Converts internal links to AUTOTITLE format | |
| - Converts Mermaid diagrams to PNG images | |
| - Normalizes code fence languages and list formatting | |
| > [!NOTE] | |
| > This PR is auto-generated. Do not edit it directly — make changes in the [copilot-sdk docs](https://github.com/github/copilot-sdk/tree/main/docs) instead. Exception: \`redirect_from\` cannot come from upstream, so add redirects here. | |
| --- | |
| _Generated by the [sync-sdk-docs workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)._" | |
| EXISTING_PR=$(gh pr list --head "$SYNC_BRANCH" --json number --jq '.[0].number' 2>/dev/null || true) | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "Updating existing PR #$EXISTING_PR" | |
| gh pr edit "$EXISTING_PR" --title "$PR_TITLE" --body "$PR_BODY" | |
| gh pr comment "$EXISTING_PR" --body "🔄 Updated with changes from [\`${SDK_SHA::7}\`](https://github.com/github/copilot-sdk/commit/$SDK_SHA)." | |
| else | |
| echo "Creating new PR" | |
| gh pr create \ | |
| --title "$PR_TITLE" \ | |
| --body "$PR_BODY" \ | |
| --base main \ | |
| --head "$SYNC_BRANCH" \ | |
| --label "copilot-sdk-docs,workflow-generated" | |
| fi | |
| - name: Cleanup | |
| if: always() | |
| run: rm -rf "$SDK_TMP" | |
| - uses: ./.github/actions/create-workflow-failure-issue | |
| id: create-failure-issue | |
| if: ${{ failure() && github.event_name != 'workflow_dispatch' }} | |
| with: | |
| token: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| - uses: ./.github/actions/slack-alert | |
| if: ${{ failure() && github.event_name != 'workflow_dispatch' }} | |
| with: | |
| slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} | |
| issue_url: ${{ steps.create-failure-issue.outputs.issue_url }} |