Content pipelines: Update content #16
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: 'Content pipelines: Update content' | |
| # **What it does**: On a schedule, runs the content pipeline update script for each | |
| # configured entry. The script clones each source repo, detects changes, and | |
| # runs a Copilot agent to update content articles. The workflow handles | |
| # branching, committing, and opening PRs. | |
| # **Why we have it**: Keeps reference documentation in sync with upstream source | |
| # docs without storing copies of those source docs in this repository. | |
| # **Who does it impact**: Docs content writers, docs engineering. | |
| # | |
| # To add a new entry, add it to src/content-pipelines/config.yml and to the matrix | |
| # `include` list below (only `id` is needed). The update logic lives in | |
| # src/content-pipelines/scripts/update.ts, which reads config.yml for all other | |
| # values. Run locally: npx tsx src/content-pipelines/scripts/update.ts --help | |
| on: | |
| schedule: | |
| - cron: '20 16 * * 1-5' # Mon-Fri at 16:20 UTC | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| env: | |
| HUSKY: 0 | |
| jobs: | |
| update: | |
| if: github.repository == 'github/docs-internal' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Each entry only needs `id`. Everything else (source-repo, | |
| # source-path, target-articles, etc.) is read from | |
| # src/content-pipelines/config.yml by the update script. | |
| - id: copilot-cli | |
| # - id: mcp-server | |
| steps: | |
| - name: Checkout docs-internal | |
| uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | |
| - uses: ./.github/actions/node-npm-setup | |
| - name: Install Copilot CLI | |
| run: npm install -g @github/copilot@prerelease | |
| - name: Derive branch name | |
| id: branch | |
| run: echo "update_branch=docs/content-pipeline-${{ matrix.id }}-update" >> "$GITHUB_OUTPUT" | |
| - name: Check for existing PR | |
| id: check-pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| UPDATE_BRANCH: ${{ steps.branch.outputs.update_branch }} | |
| run: | | |
| PR_NUMBER=$(gh pr list --head "$UPDATE_BRANCH" --state open --json number --jq '.[0].number // empty' 2>/dev/null || echo "") | |
| echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| - name: Setup branch | |
| id: setup-branch | |
| env: | |
| UPDATE_BRANCH: ${{ steps.branch.outputs.update_branch }} | |
| PR_NUMBER: ${{ steps.check-pr.outputs.pr_number }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| if git ls-remote --exit-code --heads origin "$UPDATE_BRANCH" > /dev/null 2>&1; then | |
| git fetch origin "$UPDATE_BRANCH" main | |
| git checkout "$UPDATE_BRANCH" | |
| git merge origin/main --no-edit || { | |
| echo "Merge conflict with main — resetting branch to main" | |
| git merge --abort 2>/dev/null || true | |
| git checkout main | |
| git branch -D "$UPDATE_BRANCH" | |
| if [ -z "$PR_NUMBER" ]; then | |
| git push origin --delete "$UPDATE_BRANCH" || true | |
| else | |
| echo "Skipping remote branch delete — PR #$PR_NUMBER is open" | |
| echo "force_push=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| git checkout -b "$UPDATE_BRANCH" | |
| } | |
| else | |
| git checkout -b "$UPDATE_BRANCH" | |
| fi | |
| - name: Run content pipeline update script | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| COPILOT_GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_COPILOT }} | |
| run: npx tsx src/content-pipelines/scripts/update.ts --id "${{ matrix.id }}" | |
| - name: Commit changes | |
| id: commit | |
| env: | |
| ID: ${{ matrix.id }} | |
| run: | | |
| git add content/ data/ | |
| git add "src/content-pipelines/state/${ID}.sha" | |
| if git diff --cached --quiet; then | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| echo "No documentation changes to commit" | |
| else | |
| git commit -m "docs: update ${ID} content from source docs" \ | |
| -m "Updated by the content-pipeline-update agent (${ID}) via GitHub Actions." \ | |
| -m "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Push changes | |
| if: steps.commit.outputs.has_changes == 'true' | |
| env: | |
| UPDATE_BRANCH: ${{ steps.branch.outputs.update_branch }} | |
| FORCE_PUSH: ${{ steps.setup-branch.outputs.force_push }} | |
| run: | | |
| if [ "$FORCE_PUSH" = "true" ]; then | |
| echo "Force-pushing to align branch after merge conflict reset" | |
| git push --force-with-lease origin "$UPDATE_BRANCH" | |
| else | |
| git push origin "$UPDATE_BRANCH" | |
| fi | |
| - name: Read source repo info from config | |
| id: source-info | |
| env: | |
| PIPELINE_ID: ${{ matrix.id }} | |
| run: | | |
| SOURCE_REPO=$(yq -r ".[\"${PIPELINE_ID}\"].\"source-repo\"" src/content-pipelines/config.yml) | |
| SOURCE_PATH=$(yq -r ".[\"${PIPELINE_ID}\"].\"source-path\"" src/content-pipelines/config.yml) | |
| echo "source_repo=$SOURCE_REPO" >> "$GITHUB_OUTPUT" | |
| echo "source_path=$SOURCE_PATH" >> "$GITHUB_OUTPUT" | |
| - name: Create or update PR | |
| if: steps.commit.outputs.has_changes == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} | |
| UPDATE_BRANCH: ${{ steps.branch.outputs.update_branch }} | |
| PIPELINE_ID: ${{ matrix.id }} | |
| SOURCE_REPO: ${{ steps.source-info.outputs.source_repo }} | |
| SOURCE_PATH: ${{ steps.source-info.outputs.source_path }} | |
| run: | | |
| PR_NUMBER="${{ steps.check-pr.outputs.pr_number }}" | |
| PR_TITLE="docs: update ${PIPELINE_ID} content from source docs" | |
| SOURCE_LINK="See the [upstream repo](https://github.com/${SOURCE_REPO}/tree/main/${SOURCE_PATH}) for changes that triggered this update." | |
| if [ -n "$PR_NUMBER" ]; then | |
| echo "PR #$PR_NUMBER already exists" | |
| echo "Ensuring PR #$PR_NUMBER is marked ready for review" | |
| gh pr ready "$PR_NUMBER" || echo "Unable to mark PR #$PR_NUMBER as ready (it may already be ready)" | |
| else | |
| echo "Creating new PR" | |
| PR_BODY="_GitHub Copilot generated this pull request._"$'\n\n' | |
| PR_BODY+="> [!NOTE]"$'\n' | |
| PR_BODY+="> This PR is **automatically generated** by the [content pipeline update workflow](${{ github.server_url }}/${{ github.repository }}/actions/workflows/content-pipelines.yml). Each run adds a new commit with any documentation changes detected."$'\n\n' | |
| PR_BODY+="## What this does"$'\n\n' | |
| PR_BODY+="Runs the \`content-pipeline-update\` agent (${PIPELINE_ID}) against the latest source docs and updates official articles under \`content/\` that have fallen out of sync."$'\n\n' | |
| PR_BODY+="## Source changes"$'\n\n' | |
| PR_BODY+="${SOURCE_LINK}"$'\n\n' | |
| PR_BODY+="## Review"$'\n\n' | |
| PR_BODY+="* Review each commit for accuracy — the agent uses AI, so spot-check important changes"$'\n' | |
| PR_BODY+="* To adjust agent behavior, see [Modifying results](${{ github.server_url }}/${{ github.repository }}/blob/main/src/content-pipelines/README.md#modifying-results)"$'\n' | |
| PR_BODY+="* Once satisfied, merge to keep docs up to date"$'\n' | |
| PR_BODY+="* A new PR will be created on the next run if there are further changes" | |
| gh pr create \ | |
| --title "$PR_TITLE" \ | |
| --body "$PR_BODY" \ | |
| --base main \ | |
| --head "$UPDATE_BRANCH" \ | |
| --label "workflow-generated,content-pipeline-update,ready-for-doc-review,skip FR board" | |
| fi | |
| - uses: ./.github/actions/slack-alert | |
| if: ${{ failure() && github.event_name != 'workflow_dispatch' }} | |
| with: | |
| slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} | |
| slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} | |
| - uses: ./.github/actions/create-workflow-failure-issue | |
| if: ${{ failure() && github.event_name != 'workflow_dispatch' }} | |
| with: | |
| token: ${{ secrets.DOCS_BOT_PAT_BASE }} |