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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
71 changes: 22 additions & 49 deletions .github/workflows/docs-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
130 changes: 130 additions & 0 deletions tests/docs-agent-native-workflow-contract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php
/**
* Validate the Docs Agent consumer against its pinned native producer chain.
*
* Run with DOCS_AGENT_DIR at Docs Agent #121's merge commit and WP_CODEBOX_DIR
* at WP Codebox #1754's merge commit. This test intentionally fails without
* both checkouts because it verifies the producer interfaces, not just copies
* of their expected values in this repository.
*
* @package AgentsAPI\Tests
*/

declare( strict_types=1 );

const AGENTS_API_DOCS_AGENT_REVISION = '5344a4bfbda4a0553cc92636258e46a715b1c72d';
const AGENTS_API_WP_CODEBOX_REVISION = '54c2f9a7bc3cd1fe20055d496c83efcfb99afb41';

$root = dirname( __DIR__ );
$failures = array();

function agents_api_docs_agent_contract_assert( bool $condition, string $message, array &$failures ): void {
if ( ! $condition ) {
$failures[] = $message;
}
}

function agents_api_docs_agent_contract_directory( string $name, array &$failures ): string {
$directory = getenv( $name );
if ( ! is_string( $directory ) || '' === $directory || ! is_dir( $directory ) ) {
$failures[] = sprintf(
'%1$s must point to the pinned producer checkout. Run DOCS_AGENT_DIR=/path/to/docs-agent WP_CODEBOX_DIR=/path/to/wp-codebox php tests/docs-agent-native-workflow-contract.php.',
$name
);
return '';
}

return rtrim( $directory, '/' );
}

function agents_api_docs_agent_contract_read( string $directory, string $path, array &$failures ): string {
$file = $directory . '/' . $path;
if ( ! is_file( $file ) ) {
$failures[] = "Missing producer file: {$path}.";
return '';
}

return (string) file_get_contents( $file );
}

function agents_api_docs_agent_contract_revision( string $directory, string $expected, string $name, array &$failures ): void {
$revision = trim( (string) shell_exec( 'git -C ' . escapeshellarg( $directory ) . ' rev-parse HEAD 2>/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" );