diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f07741..a6aa007 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,3 +41,42 @@ jobs: - name: Run ${{ matrix.label }} run: composer ${{ matrix.command }} + + workflow-contracts: + name: Workflow contracts + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6.0.3 + + - name: Checkout Docs Agent producer + uses: actions/checkout@v6.0.3 + with: + repository: Automattic/docs-agent + ref: 5344a4bfbda4a0553cc92636258e46a715b1c72d + path: .docs-agent-producer + + - name: Checkout WP Codebox producer schema + uses: actions/checkout@v6.0.3 + with: + repository: Automattic/wp-codebox + ref: 54c2f9a7bc3cd1fe20055d496c83efcfb99afb41 + path: .wp-codebox-producer + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + + - name: Validate native Docs Agent producer contract + env: + DOCS_AGENT_DIR: ${{ github.workspace }}/.docs-agent-producer + WP_CODEBOX_DIR: ${{ github.workspace }}/.wp-codebox-producer + run: php tests/docs-agent-native-workflow-contract.php + + - name: Lint GitHub Actions workflows + uses: docker://rhysd/actionlint:1.7.12 + with: + # With no paths, actionlint discovers and reads every workflow file. + args: -no-color diff --git a/.github/workflows/docs-agent.yml b/.github/workflows/docs-agent.yml index 5a4d56a..9407675 100644 --- a/.github/workflows/docs-agent.yml +++ b/.github/workflows/docs-agent.yml @@ -4,72 +4,45 @@ on: workflow_dispatch: inputs: prompt: - description: Optional additional instruction for Docs Agent. + description: Optional instruction appended to the technical documentation maintenance task. required: false - default: Generate or update Agents API documentation from repository source. Open a documentation pull request only when changes are needed. - audience: - description: Documentation lane to maintain. - type: choice - required: true - options: - - technical - - user - default: technical - run_kind: - description: Docs Agent run kind. - type: choice - required: true - options: - - maintenance - - bootstrap - default: maintenance - openai_model: - description: OpenAI model to use for Docs Agent. - required: true - default: gpt-5.5 - docs_agent_ref: - description: Automattic/docs-agent ref to use for the Docs Agent recipe. - required: true - default: main - base_ref: - description: Base branch or ref for documentation pull requests. - required: true - default: main - docs_branch: - description: Stable branch reused for the Docs Agent pull request. - required: true - default: docs-agent/docs-upkeep - writable_paths: - description: Comma-separated paths Docs Agent may edit. - required: true - default: README.md,docs/** + default: Generate or update Agents API developer documentation from repository source. Open a documentation pull request only when changes are needed. permissions: contents: write pull-requests: write issues: write +concurrency: + group: docs-agent-${{ github.repository }} + cancel-in-progress: false + jobs: docs-agent: - name: Maintain Documentation - uses: Automattic/docs-agent/.github/workflows/maintain-docs.yml@main + name: Maintain Technical Documentation + uses: Automattic/docs-agent/.github/workflows/maintain-docs.yml@5344a4bfbda4a0553cc92636258e46a715b1c72d with: - audience: ${{ inputs.audience }} - run_kind: ${{ inputs.run_kind }} - base_ref: ${{ inputs.base_ref }} - docs_branch: ${{ inputs.docs_branch }} - writable_paths: ${{ inputs.writable_paths }} - model: ${{ inputs.openai_model }} - docs_agent_ref: ${{ inputs.docs_agent_ref }} + audience: technical + run_kind: maintenance + base_ref: main + docs_branch: docs-agent/agents-api-docs-upkeep + writable_paths: README.md,docs/** verification_commands: | [ + "composer test", "php tests/no-product-imports-smoke.php" ] + drift_checks: | + [ + "git diff --check" + ] prompt: | ${{ inputs.prompt }} Target repository: Automattic/agents-api Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - Preserve the Agents API product-neutral boundary. Use Agents API as the product name in production and package documentation. Keep generated documentation focused on the public API substrate, repository source, developer contracts, setup, tests, and contributor workflows for the selected audience. - secrets: inherit + Maintain source-grounded developer documentation for the public, product-neutral Agents API substrate. Cover developer contracts, setup, tests, and contributor workflows using repository source as evidence. + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + EXTERNAL_PACKAGE_SOURCE_POLICY: ${{ secrets.EXTERNAL_PACKAGE_SOURCE_POLICY }} diff --git a/tests/docs-agent-native-workflow-contract.php b/tests/docs-agent-native-workflow-contract.php new file mode 100644 index 0000000..e86d7c5 --- /dev/null +++ b/tests/docs-agent-native-workflow-contract.php @@ -0,0 +1,130 @@ +/dev/null' ) ); + agents_api_docs_agent_contract_assert( $expected === $revision, "{$name} must be checked out at {$expected}; found {$revision}.", $failures ); +} + +function agents_api_docs_agent_contract_match( string $content, string $pattern, string $message, array &$failures ): void { + agents_api_docs_agent_contract_assert( 1 === preg_match( $pattern, $content ), $message, $failures ); +} + +$docs_agent_dir = agents_api_docs_agent_contract_directory( 'DOCS_AGENT_DIR', $failures ); +$wp_codebox_dir = agents_api_docs_agent_contract_directory( 'WP_CODEBOX_DIR', $failures ); + +if ( $failures ) { + fwrite( STDERR, "Docs Agent native workflow contract failed:\n- " . implode( "\n- ", $failures ) . "\n" ); + exit( 1 ); +} + +agents_api_docs_agent_contract_revision( $docs_agent_dir, AGENTS_API_DOCS_AGENT_REVISION, 'Docs Agent', $failures ); +agents_api_docs_agent_contract_revision( $wp_codebox_dir, AGENTS_API_WP_CODEBOX_REVISION, 'WP Codebox', $failures ); + +$consumer_workflow = (string) file_get_contents( $root . '/.github/workflows/docs-agent.yml' ); +$docs_workflow = agents_api_docs_agent_contract_read( $docs_agent_dir, '.github/workflows/maintain-docs.yml', $failures ); +$schema_json = agents_api_docs_agent_contract_read( $wp_codebox_dir, 'contracts/run-agent-task-reusable-workflow-interface.v1.json', $failures ); +$schema = json_decode( $schema_json, true ); + +agents_api_docs_agent_contract_assert( is_array( $schema ), 'WP Codebox reusable-workflow schema must be valid JSON.', $failures ); +if ( is_array( $schema ) ) { + agents_api_docs_agent_contract_assert( 'wp-codebox/reusable-workflow-interface/v1' === ( $schema['schema'] ?? null ), 'WP Codebox reusable-workflow schema version must match.', $failures ); + + foreach ( + array( + 'external_package_source' => array( true, 'string' ), + 'target_repo' => array( true, 'string' ), + 'prompt' => array( true, 'string' ), + 'writable_paths' => array( false, 'string' ), + 'verification_commands' => array( false, 'string' ), + 'drift_checks' => array( false, 'string' ), + 'success_requires_pr' => array( false, 'boolean' ), + 'access_token_repos' => array( false, 'string' ), + 'require_access_token' => array( false, 'boolean' ), + 'allowed_repos' => array( false, 'string' ), + 'run_agent' => array( false, 'boolean' ), + 'dry_run' => array( false, 'boolean' ), + ) as $input => $contract + ) { + agents_api_docs_agent_contract_assert( $contract[0] === ( $schema['inputs'][ $input ]['required'] ?? null ), "WP Codebox {$input} required contract must match.", $failures ); + agents_api_docs_agent_contract_assert( $contract[1] === ( $schema['inputs'][ $input ]['type'] ?? null ), "WP Codebox {$input} type contract must match.", $failures ); + } + + foreach ( array( 'OPENAI_API_KEY' => false, 'ACCESS_TOKEN' => false, 'EXTERNAL_PACKAGE_SOURCE_POLICY' => true ) as $secret => $required ) { + agents_api_docs_agent_contract_assert( $required === ( $schema['secrets'][ $secret ]['required'] ?? null ), "WP Codebox {$secret} secret contract must match.", $failures ); + } +} + +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*uses:\s*Automattic/docs-agent/\.github/workflows/maintain-docs\.yml@' . AGENTS_API_DOCS_AGENT_REVISION . '\s*$~m', 'Consumer must pin Docs Agent #121.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*audience:\s*technical\s*$~m', 'Consumer audience must be technical.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*run_kind:\s*maintenance\s*$~m', 'Consumer run kind must be maintenance.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*base_ref:\s*main\s*$~m', 'Consumer target base must be main.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*docs_branch:\s*docs-agent/agents-api-docs-upkeep\s*$~m', 'Consumer publication branch must be stable and exact.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*writable_paths:\s*README\.md,docs/\*\*\s*$~m', 'Consumer writable paths must be README.md,docs/**.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~"composer test"\s*,\s*"php tests/no-product-imports-smoke\.php"~s', 'Consumer verification commands must include the test chain.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~"git diff --check"~', 'Consumer drift check must run git diff --check.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*OPENAI_API_KEY:\s*\$\{\{ secrets\.OPENAI_API_KEY \}\}\s*$~m', 'Consumer must forward OPENAI_API_KEY.', $failures ); +agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*EXTERNAL_PACKAGE_SOURCE_POLICY:\s*\$\{\{ secrets\.EXTERNAL_PACKAGE_SOURCE_POLICY \}\}\s*$~m', 'Consumer must forward EXTERNAL_PACKAGE_SOURCE_POLICY.', $failures ); +agents_api_docs_agent_contract_assert( 0 === preg_match( '~secrets\.ACCESS_TOKEN|^\s*ACCESS_TOKEN:\s*~m', $consumer_workflow ), 'Consumer must not require or forward ACCESS_TOKEN.', $failures ); + +foreach ( array( 'contents: write', 'pull-requests: write', 'issues: write' ) as $permission ) { + agents_api_docs_agent_contract_match( $consumer_workflow, '~^\s*' . preg_quote( $permission, '~' ) . '\s*$~m', "Consumer must grant {$permission} for built-in-token publication.", $failures ); +} + +agents_api_docs_agent_contract_match( $docs_workflow, '~technical:maintenance\)\s+package_path="bundles/technical-docs-agent/native/technical-docs-maintenance-agent\.agent\.json"\s+agent_slug="technical-docs-maintenance-agent"\s+package_digest="sha256-bytes-v1:6057aad4eb7c5f0320ccfbce9da93a5fa1d3fc521478b5571ed81c28129325aa"\s+success_requires_pr=false~s', 'Docs Agent #121 technical maintenance lane must map to its exact native package.', $failures ); +agents_api_docs_agent_contract_match( $docs_workflow, '~uses:\s*Automattic/wp-codebox/\.github/workflows/run-agent-task\.yml@' . AGENTS_API_WP_CODEBOX_REVISION . '~', 'Docs Agent must pin WP Codebox #1754.', $failures ); +agents_api_docs_agent_contract_match( $docs_workflow, '~external_package_source:\s*\$\{\{ needs\.prepare\.outputs\.external_package_source \}\}.*?target_repo:\s*\$\{\{ github\.repository \}\}.*?writable_paths:\s*\$\{\{ inputs\.writable_paths \}\}.*?verification_commands:\s*\$\{\{ needs\.prepare\.outputs\.verification_commands \}\}.*?drift_checks:\s*\$\{\{ needs\.prepare\.outputs\.drift_checks \}\}.*?success_requires_pr:\s*\$\{\{ needs\.prepare\.outputs\.success_requires_pr == \'true\' \}\}.*?access_token_repos:\s*\$\{\{ github\.repository \}\}.*?require_access_token:\s*true.*?allowed_repos:\s*\'\["\$\{\{ github\.repository \}\}"\]\'~s', 'Docs Agent must preserve the external-package, target, writable, verification, publication, and access chain.', $failures ); +agents_api_docs_agent_contract_match( $docs_workflow, '~OPENAI_API_KEY:\s*\$\{\{ secrets\.OPENAI_API_KEY \}\}\s+ACCESS_TOKEN:\s*\$\{\{ github\.token \}\}\s+EXTERNAL_PACKAGE_SOURCE_POLICY:\s*\$\{\{ secrets\.EXTERNAL_PACKAGE_SOURCE_POLICY \}\}~s', 'Docs Agent must forward caller credentials and its built-in publication token.', $failures ); + +if ( $failures ) { + fwrite( STDERR, "Docs Agent native workflow contract failed:\n- " . implode( "\n- ", $failures ) . "\n" ); + exit( 1 ); +} + +fwrite( STDOUT, "Docs Agent native workflow contract passed.\n" );