Skip to content

Generate toolkit docs #264

Generate toolkit docs

Generate toolkit docs #264

name: Generate toolkit docs
# Description: Generate toolkit JSON from the public tool catalog, then sync sidebar navigation.
# Flow:
# 1) Generate toolkit JSON into toolkit-docs-generator/data/toolkits
# 2) Sync integrations sidebar _meta.tsx from toolkit-docs-generator/data/toolkits
# 3) Regenerate public/llms.txt so the automation PR is self-contained
# 4) Create or update a PR if changes were produced
#
# Opens the PR with GITHUB_TOKEN. That does not start other workflows, so this
# job generates llms.txt here instead of depending on llmstxt.yml to fire.
on:
repository_dispatch:
types: [porter_deploy_succeeded]
workflow_dispatch:
inputs:
public_catalog_url:
description: "Override public catalog URL (staging only; production is the default)"
required: false
type: string
# 11:00 UTC = 3 AM PST / 4 AM PDT — late enough that DST drift doesn't matter.
schedule:
- cron: "0 11 * * *"
permissions:
contents: write
pull-requests: write
# Read this run's jobs so the Slack alert can deep-link the failing job log
# rather than the run summary.
actions: read
concurrency:
group: generate-toolkit-docs
cancel-in-progress: true
jobs:
generate:
runs-on: ubuntu-latest
# Opt in to Node 24 for JavaScript actions before GitHub forces the
# switch on 2026-06-02. Harmless today; unblocks the cutover.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
outputs:
# Distinguishes "generation itself failed" from "a later step failed".
# Without it, a broken notification would make the alert job announce a
# generation failure that never happened.
generation-succeeded: ${{ steps.generate-docs.outputs.succeeded }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate toolkit docs
id: generate-docs
# Invoked by path rather than through `pnpm exec`, which would reset the
# working directory to the repo root and break the relative paths below.
run: |
../node_modules/.bin/tsx src/cli/index.ts generate \
--all \
--skip-unchanged \
--preserve-last-known-good \
--verbose \
--api-source public-catalog \
--llm-provider anthropic \
--llm-model "claude-sonnet-4-6" \
--llm-api-key "$ANTHROPIC_API_KEY" \
--llm-max-tokens 8192 \
--llm-editor-provider anthropic \
--llm-editor-model "claude-sonnet-4-6" \
--llm-editor-api-key "$ANTHROPIC_API_KEY" \
--toolkit-concurrency 8 \
--llm-concurrency 15 \
--exclude-file ./remove-toolkits.txt \
--ignore-file ./skip-toolkits.txt \
--custom-sections ./curation \
--output data/toolkits
echo "succeeded=true" >>"$GITHUB_OUTPUT"
working-directory: toolkit-docs-generator
env:
# Unset in normal runs: the generator defaults to the production
# experience API. Pass public_catalog_url on workflow_dispatch to
# point a manual run at staging or a local BFF.
PUBLIC_CATALOG_URL: ${{ inputs.public_catalog_url }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- 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 llmstxt.yml, so generate the file here.
# The toolkit generator CLI still does not write llms.txt.
- name: Generate llms.txt
run: pnpm llmstxt
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create pull request
id: cpr
uses: peter-evans/create-pull-request@v7
env:
HUSKY: 0
with:
token: ${{ github.token }}
commit-message: "[AUTO] Adding MCP Servers docs update"
title: "[AUTO] Adding MCP Servers docs update"
body: |
This PR was generated after a Porter deploy succeeded.
- Trigger: ${{ github.event_name }}
- Deploy env: ${{ github.event.client_payload.env || 'unknown' }}
- Deploy SHA: ${{ github.event.client_payload.deploy_sha || 'unknown' }}
- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Stable branch so later runs update the open auto-PR instead of
# opening a new one each time. create-pull-request force-pushes the
# latest generated docs onto this branch.
branch: automation/toolkit-docs
delete-branch: true
# A token without reviewer permission is a real gap, but it must not fail
# the run: the generated PR is already open and useful without a reviewer
# attached. An annotation keeps it visible instead of silent.
- name: Request team review
if: steps.cpr.outputs.pull-request-number != ''
run: |
if ! gh pr edit ${{ steps.cpr.outputs.pull-request-number }} \
--add-reviewer ArcadeAI/engineering-tools-and-dx 2>review-error.log; then
echo "::warning::Could not request review on PR #${{ steps.cpr.outputs.pull-request-number }}: $(cat review-error.log)"
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Upload generation report
if: always()
uses: actions/upload-artifact@v4
with:
name: failed-tools
path: toolkit-docs-generator-verification/logs/failed-tools.json
if-no-files-found: ignore
# No `continue-on-error` here on purpose. A Slack step that dies quietly
# is how a wrong webhook secret went unnoticed: the alert it was meant to
# deliver is exactly what nobody was watching for.
#
# Gated on the generate step rather than on the default "everything so far
# succeeded". Missing or stale pages are worth reporting even when a later
# step broke — under the default gate a failed sidebar sync or PR creation
# would skip this step, while `generation-succeeded` suppressed the alert
# job below, and nobody heard about the toolkits at all. `!cancelled()`
# rather than `always()` because `cancel-in-progress` cancels superseded
# runs, and those have nothing to report.
- name: Report preserved or omitted toolkits to Slack
if: ${{ !cancelled() && steps.generate-docs.outputs.succeeded == 'true' }}
run: |
# The deep link is a nicety; the alert is the point. Actions runs this
# with `-e -o pipefail`, so a failed `gh api` — or a SIGPIPE from
# trimming its output — would abort the step before curl and lose the
# alert entirely. Select the first match in jq instead of piping to
# head, and fall back to the run URL on any failure.
job_url=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \
--jq '[.jobs[] | select(.name == "generate") | .html_url] | .[0] // empty') || job_url=""
payload=$(../node_modules/.bin/tsx src/cli/index.ts alert \
--report ../toolkit-docs-generator-verification/logs/failed-tools.json \
--log-url "${job_url:-$RUN_URL}")
if [ -z "$payload" ]; then
echo "No preserved or omitted toolkits to report."
exit 0
fi
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::SLACK_PROJ_DOCS_WEBHOOK_URL is not configured"
exit 1
fi
curl --fail-with-body --silent --show-error \
-X POST \
-H "Content-Type: application/json" \
--data "$payload" \
"$SLACK_WEBHOOK_URL"
working-directory: toolkit-docs-generator
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_PROJ_DOCS_WEBHOOK_URL }}
alert:
name: Alert on failure
# Every red run gets a message. Which message depends on whether generation
# itself broke or something after it did: those need different people to do
# different things, and calling a post-generation failure a "generation
# failure" sends someone hunting for a validation error that doesn't exist.
if: ${{ !cancelled() && needs.generate.result == 'failure' }}
needs: generate
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Notify #proj-docs in Slack
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_PROJ_DOCS_WEBHOOK_URL }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
TRIGGER: ${{ github.event_name }}
COMMIT: ${{ github.sha }}
GENERATION_SUCCEEDED: ${{ needs.generate.outputs.generation-succeeded }}
run: |
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::SLACK_PROJ_DOCS_WEBHOOK_URL is not configured"
exit 1
fi
if [ "$GENERATION_SUCCEEDED" = "true" ]; then
headline=":warning: Toolkit docs generated, but the workflow failed afterward"
detail="Generation succeeded, so the toolkit JSON is fine. A later workflow step failed — inspect the run to determine whether sidebar sync, llms.txt generation, PR creation, artifact upload, or Slack notification needs attention."
else
headline=":rotating_light: Toolkit docs generation failed"
detail="The failed run contains the exact file path and validation error."
fi
payload=$(jq -n \
--arg headline "$headline" \
--arg detail "$detail" \
--arg run_url "$RUN_URL" \
--arg trigger "$TRIGGER" \
--arg commit "$COMMIT" \
'{text: ($headline + "\n\n*Workflow run:* <" + $run_url + "|Open failed run>\n*Trigger:* " + $trigger + "\n*Commit:* " + $commit + "\n\n" + $detail)}')
curl --fail-with-body --silent --show-error \
-X POST \
-H "Content-Type: application/json" \
--data "$payload" \
"$SLACK_WEBHOOK_URL"