diff --git a/.github/workflows/docs-ci-agent.yml b/.github/workflows/docs-ci-agent.yml new file mode 100644 index 00000000..959d63b1 --- /dev/null +++ b/.github/workflows/docs-ci-agent.yml @@ -0,0 +1,191 @@ +# This workflow runs an AI agent to complete one Linear docs ticket. +# A person starts the workflow and gives a Linear ticket ID. +# The agent reads the ticket, changes the docs, and verifies the build. +# The workflow then opens a pull request with the changes. +name: Docs CI Agent + +on: + workflow_dispatch: + inputs: + ticket_id: + description: "Linear ticket ID (example: DOC-363)" + required: true + type: string + +# Run one job for each ticket at a time. A new dispatch stops an active run. +concurrency: + group: docs-ci-agent-${{ github.event.inputs.ticket_id }} + cancel-in-progress: true + +# The pull request steps use a personal access token, not GITHUB_TOKEN. +permissions: + contents: read + +jobs: + implement-docs-ticket: + name: Implement the docs ticket + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Validate the ticket ID + id: ticket + env: + TICKET_ID_INPUT: ${{ github.event.inputs.ticket_id }} + run: | + # Accept only IDs with the form TEAM-123. Stop on bad input. + if [[ ! "$TICKET_ID_INPUT" =~ ^[A-Za-z]+-[0-9]+$ ]]; then + echo "::error::Ticket ID '$TICKET_ID_INPUT' is not valid. Give an ID such as DOC-363." + exit 1 + fi + echo "id=${TICKET_ID_INPUT^^}" >> "$GITHUB_OUTPUT" + echo "id_lower=${TICKET_ID_INPUT,,}" >> "$GITHUB_OUTPUT" + + - name: Checkout docs + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + + - name: Set up Node.js 22 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: "22" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install the Claude Code CLI + run: npm install -g @anthropic-ai/claude-code@2.1.218 + + - name: Write the MCP configuration + # Keep this file out of the repository tree. The file contains secrets. + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: | + cat > "$RUNNER_TEMP/mcp-config.json" <". Write /tmp/docs-ci-agent/pr-body.md with a summary of the changes and the audit trail that agents.md requires. + 6. Do not commit. Do not push. Do not create branches. Do not open pull requests. The workflow does these steps after you stop. + 7. Use the LocalStack MCP tools only to search documentation and service metadata. Do not start containers. Do not deploy infrastructure. Do not run AWS commands. + PROMPT_EOF + )" + PROMPT="${PROMPT//__TICKET_ID__/$TICKET_ID}" + + claude -p "$PROMPT" \ + --model claude-sonnet-4-5-20250929 \ + --max-turns 75 \ + --mcp-config "$RUNNER_TEMP/mcp-config.json" \ + --strict-mcp-config \ + --add-dir /tmp/docs-ci-agent \ + --allowedTools "Read,Grep,Glob,Edit,Write,WebFetch,WebSearch,Bash(npm ci),Bash(npm run build),Bash(git status),Bash(git diff:*),Bash(git log:*),Bash(git restore:*),Bash(ls:*),mcp__linear,mcp__localstack" \ + --output-format json \ + > "$RUNNER_TEMP/agent-output.json" + + - name: Stop when the agent made no changes + run: | + # An empty git status means the agent made no changes. Fail loudly. + if [ -z "$(git status --porcelain)" ]; then + { + echo "## Docs CI Agent: no changes" + echo "" + echo "The agent made no file changes. No pull request was created." + echo "" + echo "### Final message from the agent" + echo "" + jq -r '.result // "No final message is available."' "$RUNNER_TEMP/agent-output.json" + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::The agent made no file changes. Read the run summary for the reason." + exit 1 + fi + + - name: Compose the pull request title and body + id: pr_content + env: + TICKET_ID: ${{ steps.ticket.outputs.id }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + # Use the agent's title when present. Use a default title when absent. + if [ -s /tmp/docs-ci-agent/pr-title.txt ]; then + TITLE="$(head -n 1 /tmp/docs-ci-agent/pr-title.txt)" + else + TITLE="$TICKET_ID: Automated documentation update" + fi + echo "title=$TITLE" >> "$GITHUB_OUTPUT" + + # Use the agent's body when present. Always append the fixed footer. + BODY_FILE="$RUNNER_TEMP/pr-body.md" + if [ -s /tmp/docs-ci-agent/pr-body.md ]; then + cat /tmp/docs-ci-agent/pr-body.md > "$BODY_FILE" + else + echo "Automated documentation update for $TICKET_ID." > "$BODY_FILE" + fi + cat >> "$BODY_FILE" < [!IMPORTANT] + > An AI agent generated this pull request. Review all changes before you merge. + + - Linear ticket: [$TICKET_ID](https://linear.app/localstack/issue/$TICKET_ID) + - Workflow run: $RUN_URL + EOF + echo "body_path=$BODY_FILE" >> "$GITHUB_OUTPUT" + + - name: Create the pull request + id: create_pr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.PRO_ACCESS_TOKEN }} + title: ${{ steps.pr_content.outputs.title }} + body-path: ${{ steps.pr_content.outputs.body_path }} + branch: docs-agent/${{ steps.ticket.outputs.id_lower }} + author: "LocalStack Bot " + committer: "LocalStack Bot " + commit-message: "${{ steps.ticket.outputs.id }}: automated docs update" + + - name: Write the pull request link to the summary + env: + PR_URL: ${{ steps.create_pr.outputs.pull-request-url }} + run: | + { + echo "## Docs CI Agent: pull request created" + echo "" + echo "Pull request: $PR_URL" + } >> "$GITHUB_STEP_SUMMARY"