diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f6bc4e7e872e..f80e613e558e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,10 +8,6 @@ on: pull_request: branches: - main - paths: - - '**/*.ts' - - '**/*.tsx' - - '.github/workflows/codeql.yml' # This is so that when CodeQL runs on a pull request, it can compare # against the state of the base branch. push: diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index c16d053027a2..f1876b25adfb 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -146,14 +146,30 @@ jobs: console.log('Merging the pull request') // Admin merge pull request to avoid squash - await github.rest.pulls.merge({ - owner, - repo, - pull_number, - merge_method: 'merge', - }) - // Error loud here, so no try/catch - console.log('Merged the pull request successfully') + // Retry once per minute for up to 15 minutes to wait for required checks (e.g. CodeQL) + const maxAttempts = 15 + const delay = 60_000 // 1 minute + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await github.rest.pulls.merge({ + owner, + repo, + pull_number, + merge_method: 'merge', + }) + console.log('Merged the pull request successfully') + break + } catch (mergeError) { + const msg = mergeError.message || mergeError.response?.data?.message || '' + const isRuleViolation = mergeError.status === 405 && + msg.includes('Repository rule violations') + if (!isRuleViolation || attempt === maxAttempts) { + throw mergeError + } + console.log(`Merge blocked by required checks (attempt ${attempt}/${maxAttempts}), retrying in 60s...`) + await new Promise(resolve => setTimeout(resolve, delay)) + } + } - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} diff --git a/.github/workflows/sync-llms-txt-to-github.yml b/.github/workflows/sync-llms-txt-to-github.yml new file mode 100644 index 000000000000..cce898f3f529 --- /dev/null +++ b/.github/workflows/sync-llms-txt-to-github.yml @@ -0,0 +1,171 @@ +name: Sync llms.txt to github/github + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'data/llms-txt-config.yml' + - 'src/workflows/generate-llms-txt.ts' + schedule: + - cron: '20 16 * * 1-5' # Weekdays at ~9:20am Pacific + +permissions: + contents: read + +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + sync: + name: Sync llms.txt + if: github.repository == 'github/docs-internal' + runs-on: ubuntu-latest + steps: + - name: Checkout docs-internal + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - uses: ./.github/actions/node-npm-setup + + - name: Generate llms.txt + env: + DOCS_BOT_PAT_BASE: ${{ secrets.DOCS_BOT_PAT_BASE }} + run: | + echo "Generating llms.txt from page catalog and popularity data..." + npm run generate-llms-txt --silent > /tmp/llms.txt + echo "Generated llms.txt ($(wc -l < /tmp/llms.txt) lines, $(wc -c < /tmp/llms.txt) bytes)" + + - name: Fetch current llms.txt from github/github + id: fetch + env: + GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} + run: | + echo "Fetching current public/llms.txt from github/github..." + if gh api repos/github/github/contents/public/llms.txt \ + --jq '.content' 2>/dev/null | base64 -d > /tmp/current.txt; then + echo "Fetched current file ($(wc -l < /tmp/current.txt) lines)" + else + echo "No existing file found (first run)" + rm -f /tmp/current.txt + fi + + - name: Diff generated vs current + id: diff + run: | + echo "Comparing generated llms.txt against current..." + if [ -f /tmp/current.txt ] && diff -q /tmp/llms.txt /tmp/current.txt > /dev/null 2>&1; then + echo "No changes detected, skipping push" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "Changes detected:" + diff --unified=0 /tmp/current.txt /tmp/llms.txt | head -30 || true + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Ensure sync branch exists in github/github + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} + run: | + BRANCH="auto/sync-llms-txt" + REPO="github/github" + + echo "Checking if branch '$BRANCH' exists..." + BRANCH_SHA=$(gh api "repos/$REPO/git/ref/heads/$BRANCH" --jq '.object.sha' 2>/dev/null || true) + if [ -n "$BRANCH_SHA" ]; then + echo "Branch exists at $BRANCH_SHA" + else + echo "Branch does not exist, creating from default branch..." + DEFAULT_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch') + BASE_SHA=$(gh api "repos/$REPO/git/ref/heads/$DEFAULT_BRANCH" --jq '.object.sha') + gh api "repos/$REPO/git/refs" \ + --method POST \ + -f ref="refs/heads/$BRANCH" \ + -f sha="$BASE_SHA" + echo "Created branch from $DEFAULT_BRANCH at $BASE_SHA" + fi + + - name: Commit llms.txt to github/github + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} + run: | + BRANCH="auto/sync-llms-txt" + REPO="github/github" + CONTENT=$(base64 -w 0 /tmp/llms.txt) + + echo "Checking for existing file SHA on branch..." + EXISTING_SHA=$(gh api "repos/$REPO/contents/public/llms.txt?ref=$BRANCH" \ + --jq '.sha' 2>/dev/null || true) + if [ -n "$EXISTING_SHA" ]; then + echo "Existing file SHA: $EXISTING_SHA" + else + echo "No existing file on branch (new file)" + fi + + echo "Committing llms.txt to $REPO/$BRANCH..." + COMMIT_ARGS=(-f "message=Sync llms.txt from docs.github.com" + -f "content=$CONTENT" + -f "branch=$BRANCH") + if [ -n "$EXISTING_SHA" ]; then + COMMIT_ARGS+=(-f "sha=$EXISTING_SHA") + fi + gh api "repos/$REPO/contents/public/llms.txt" \ + --method PUT \ + "${COMMIT_ARGS[@]}" --jq '.commit.sha' + echo "Committed successfully" + + - name: Create PR if needed + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} + run: | + BRANCH="auto/sync-llms-txt" + REPO="github/github" + + echo "Checking for existing PR from '$BRANCH'..." + EXISTING_PR=$(gh pr list --repo "$REPO" --head "$BRANCH" \ + --json number --jq '.[0].number' 2>/dev/null || true) + if [ -n "$EXISTING_PR" ]; then + echo "PR #$EXISTING_PR already exists, updated with new commit" + exit 0 + fi + + RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + DEFAULT_BRANCH=$(gh api "repos/$REPO" --jq '.default_branch') + + PR_BODY="The [sync-llms-txt workflow]($RUN_URL) generated this PR. + + Updates \`public/llms.txt\` served at \`github.com/llms.txt\`. The docs-internal script builds this file from the page catalog and popularity data. + + No feature flags. Static file in \`public/\`, no code changes. + + " + + echo "Creating PR..." + gh pr create \ + --repo "$REPO" \ + --title "Sync llms.txt from docs.github.com" \ + --body "$PR_BODY" \ + --head "$BRANCH" \ + --base "$DEFAULT_BRANCH" \ + --label "docs" + echo "PR created successfully" + + - 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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 582140867aad..e73e53f2dd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Docs changelog +**2 March 2026** + +We've added an article about the new `/research` slash command in Copilot CLI: + +[Researching with GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/copilot-cli/research) + +
|<\/p>$/g, '').trim()
+}
+
+// =====================================================================
+// Helpers
+// =====================================================================
+
+export function pageExists(pagePath: string, pages: PageMap): boolean {
+ return `/en/${pagePath}` in pages
+}
+
+export async function getPageTitle(permalink: string, pages: PageMap): Promise