Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/actions/app-token/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Get app token
description: >
Mint a short-lived installation token for the docs automation GitHub App and
resolve the git identity of its bot user.

inputs:
app-id:
description: App ID or client ID of the docs automation app.
required: true
private-key:
description: Private key of the docs automation app.
required: true

outputs:
token:
description: Installation token, scoped to this repository and valid for one hour.
value: ${{ steps.app-token.outputs.token }}
user-name:
description: Git user name of the app's bot user.
value: ${{ steps.bot-user.outputs.user-name }}
user-email:
description: Git email of the app's bot user, which links commits to the bot account.
value: ${{ steps.bot-user.outputs.user-email }}
committer:
description: Bot identity as a single `Name <email>` string, the form create-pull-request expects.
value: ${{ steps.bot-user.outputs.committer }}

runs:
using: composite
steps:
- name: Mint an installation token
id: app-token
uses: actions/create-github-app-token@v2
with:
# `app-id` takes either the numeric app ID or the app's client ID.
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}

- name: Resolve the bot user identity
id: bot-user
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
# Assign before echoing. `echo "id=$(gh api ...)"` reports echo's exit
# code, so a failed lookup would pass the step with an empty id, and
# the resulting address is one git accepts but GitHub cannot link to
# the bot — misattributed commits, no error anywhere.
id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)

name="${APP_SLUG}[bot]"
email="${id}+${APP_SLUG}[bot]@users.noreply.github.com"

{
echo "user-name=${name}"
echo "user-email=${email}"
echo "committer=${name} <${email}>"
} >>"$GITHUB_OUTPUT"
15 changes: 13 additions & 2 deletions .github/workflows/generate-toolkit-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,24 @@ jobs:
- name: Sync toolkit sidebar navigation
run: pnpm exec tsx toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts --remove-empty-sections=false --verbose

# GITHUB_TOKEN PRs do not start other workflows. Docs Bot is a GitHub
# App, so the opened automation PR still runs Test.
- name: Get app token
id: app-token
uses: ./.github/actions/app-token
with:
app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }}
private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }}

- name: Create pull request
id: cpr
uses: peter-evans/create-pull-request@v7
env:
HUSKY: 0
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
author: ${{ steps.app-token.outputs.committer }}
committer: ${{ steps.app-token.outputs.committer }}
commit-message: "[AUTO] Adding MCP Servers docs update"
title: "[AUTO] Adding MCP Servers docs update"
body: |
Expand All @@ -130,7 +141,7 @@ jobs:
echo "::warning::Could not request review on PR #${{ steps.cpr.outputs.pull-request-number }}: $(cat review-error.log)"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Upload generation report
if: always()
Expand Down
34 changes: 27 additions & 7 deletions .github/workflows/llmstxt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref || github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
# No credentials to persist: the app token is minted further down,
# once there is something to push. The commit step below supplies it
# to git explicitly.
persist-credentials: false

- name: Install dependencies
run: npm install -g pnpm
Expand All @@ -59,21 +62,38 @@ jobs:
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

# GITHUB_TOKEN pushes and PRs do not start other workflows. Docs Bot
# is a GitHub App, so Test still runs on the new SHA.
- name: Get app token
if: steps.check-changes.outputs.has_changes == 'true'
id: app-token
uses: ./.github/actions/app-token
with:
app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }}
private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }}

- name: Commit changes to PR
if: steps.check-changes.outputs.has_changes == 'true' && github.event_name == 'pull_request'
env:
GH_APP_TOKEN: ${{ steps.app-token.outputs.token }}
USER_NAME: ${{ steps.app-token.outputs.user-name }}
USER_EMAIL: ${{ steps.app-token.outputs.user-email }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "$USER_NAME"
git config user.email "$USER_EMAIL"
git add public/llms.txt
git commit -m "🤖 Regenerate LLMs.txt"
git push
git push "https://x-access-token:${GH_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${HEAD_REF}"

- name: Create Pull Request (for scheduled/manual runs)
if: steps.check-changes.outputs.has_changes == 'true' && github.event_name != 'pull_request'
id: cpr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
author: ${{ steps.app-token.outputs.committer }}
committer: ${{ steps.app-token.outputs.committer }}
commit-message: Regenerate LLMs.txt and related files
branch: auto-update-llms-txt
delete-branch: true
Expand All @@ -87,11 +107,11 @@ jobs:
continue-on-error: true
run: gh pr edit ${{ steps.cpr.outputs.pull-request-number }} --add-reviewer ArcadeAI/engineering-tools-and-dx
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}

- name: Enable Pull Request Automerge
if: steps.check-changes.outputs.has_changes == 'true' && github.event_name != 'pull_request' && steps.cpr.outputs.pull-request-number != ''
continue-on-error: true
run: gh pr merge --squash --auto ${{ steps.cpr.outputs.pull-request-number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
16 changes: 14 additions & 2 deletions .github/workflows/update-design-system-dependency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ jobs:
pnpm vitest run toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts toolkit-docs-generator/tests/sources/oauth-provider-resolver.test.ts
pnpm run build

# GITHUB_TOKEN PRs do not start other workflows. Docs Bot is a GitHub
# App, so the opened automation PR still runs Test.
- name: Get app token
if: steps.check-changes.outputs.has_changes == 'true'
id: app-token
uses: ./.github/actions/app-token
with:
app-id: ${{ secrets.DOCS_BOT_CLIENT_ID }}
private-key: ${{ secrets.DOCS_BOT_PRIVATE_KEY }}

- name: Create pull request
if: steps.check-changes.outputs.has_changes == 'true'
id: cpr
Expand All @@ -55,7 +65,9 @@ jobs:
HUSKY: 0
SKIP_HUSKY: 1
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
author: ${{ steps.app-token.outputs.committer }}
committer: ${{ steps.app-token.outputs.committer }}
commit-message: "chore: update @arcadeai/design-system to latest"
title: "chore: update @arcadeai/design-system to latest"
body: |
Expand All @@ -77,4 +89,4 @@ jobs:
continue-on-error: true
run: gh pr edit ${{ steps.cpr.outputs.pull-request-number }} --add-reviewer ArcadeAI/engineering-tools-and-dx
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
33 changes: 33 additions & 0 deletions tests/docs-automation-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, test } from "vitest";

const readWorkflow = (name: string) =>
readFileSync(join(process.cwd(), ".github", "workflows", name), "utf-8");

const AUTOMATION_WORKFLOWS = [
"generate-toolkit-docs.yml",
"llmstxt.yml",
"update-design-system-dependency.yml",
] as const;

for (const name of AUTOMATION_WORKFLOWS) {
test(`${name} authenticates PR writes with Docs Bot, not GITHUB_TOKEN`, () => {
const yaml = readWorkflow(name);

expect(yaml).toContain("uses: ./.github/actions/app-token");
expect(yaml).toContain("secrets.DOCS_BOT_CLIENT_ID");
expect(yaml).toContain("secrets.DOCS_BOT_PRIVATE_KEY");
expect(yaml).toContain("steps.app-token.outputs.token");
expect(yaml).not.toContain("secrets.GITHUB_TOKEN");
});
}

test("app-token composite action mints a GitHub App installation token", () => {
const action = readFileSync(
join(process.cwd(), ".github", "actions", "app-token", "action.yml"),
"utf-8"
);

expect(action).toContain("actions/create-github-app-token@v2");
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ test("porter workflow generates docs and opens a PR", () => {
expect(workflowContents).toContain("HUSKY: 0");
expect(workflowContents).toContain("[AUTO] Adding MCP Servers docs update");
expect(workflowContents).toContain("pull-requests: write");
// GITHUB_TOKEN PRs do not start other workflows, so Test would never run
// on the automation PR. Docs Bot is a GitHub App and avoids that restriction.
expect(workflowContents).toContain("uses: ./.github/actions/app-token");
expect(workflowContents).toContain("steps.app-token.outputs.token");
expect(workflowContents).not.toContain("secrets.GITHUB_TOKEN");
});

test("porter workflow does not build the docs generator before running it", () => {
Expand Down
Loading