Skip to content

Harden agent file exports and owner reports #5898

Harden agent file exports and owner reports

Harden agent file exports and owner reports #5898

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
# Auto-cancel previous runs on the same PR/branch
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# Needed for posting test summaries/annotations
permissions:
contents: read
checks: write
pull-requests: write
jobs:
tag-guard:
name: Tag Guard
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Verify all tests are tagged and tags are in CI
run: |
python3 scripts/check_test_tags.py
complexity-guardrails:
name: Complexity Guardrails
runs-on: ubuntu-latest
timeout-minutes: 10
env:
UV_PYTHON_DOWNLOADS: "never"
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Check source LoC budget
run: |
python3 scripts/check_complexity_budgets.py --loc-only
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Set up uv (with cache)
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
ignore-nothing-to-cache: true
- name: Sync deps (locked)
if: ${{ hashFiles('uv.lock') != '' }}
run: |
uv sync --frozen --all-extras --dev
- name: Sync deps
if: ${{ hashFiles('uv.lock') == '' }}
run: |
uv sync --all-extras --dev
- name: Check prompt-size budgets
run: |
uv run python scripts/check_complexity_budgets.py --prompt-only
frontend-tests:
name: Frontend Tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [tag-guard, complexity-guardrails]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Run frontend tests
working-directory: frontend
run: npm test
sandbox-server-tests:
name: Sandbox Server Tests
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [tag-guard, complexity-guardrails]
env:
TEST_OUTPUT_DIR: test-results
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Set up uv (with cache)
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
ignore-nothing-to-cache: true
- name: Create sandbox venv
run: uv venv .venv-sandbox
- name: Install sandbox test dependencies
run: |
uv pip install --python .venv-sandbox/bin/python -q -e ./sandbox_server unittest-xml-reporting
- name: Run sandbox server tests
run: |
set -euo pipefail
mkdir -p "$TEST_OUTPUT_DIR"
.venv-sandbox/bin/python -m xmlrunner discover \
-s sandbox_server/tests \
-p 'test_*.py' \
-o "$TEST_OUTPUT_DIR"
- name: Upload test results artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-sandbox-server
path: test-results/**
if-no-files-found: warn
retention-days: 7
tests:
name: tests (${{ matrix.batch }})
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [tag-guard, complexity-guardrails]
strategy:
fail-fast: false
max-parallel: 10
matrix:
include:
- batch: shard_01
tag: batch_pages batch_sms batch_console_email_oauth batch_token_usage batch_event_parallel batch_allowlist_direction batch_tool_costs batch_output_schema batch_soft_expiration batch_console_api_keys batch_console_allowlist batch_pa_shutdown_triggers complexity_guardrails_batch
- batch: shard_02
tag: batch_mcp_tools batch_human_input batch_eval_fingerprint batch_user_flags batch_api_agents batch_api_tasks pipedream_jit_connect batch_browser_task_db batch_agent_transfer batch_periodic batch_mcp_admin mcp_org_assignment_batch
- batch: shard_03
tag: batch_pages_signals batch_console_agents batch_organizations referral_batch batch_subscription batch_email_body batch_support_turnstile batch_billing_rollup batch_agent_collaborators batch_api_org_keys batch_spawn_depth batch_compaction batch_browser_agent_max_tokens
- batch: shard_04
tag: batch_agent_chat batch_console_api batch_agent_webhooks batch_usage_api batch_forward_detection pipedream_connect http_request_batch batch_system_settings batch_schedule batch_plan_versioning smtp batch_browser_config batch_org_billing
- batch: shard_05
tag: batch_event_processing batch_result_analysis batch_promptree llm_routing_profiles_batch global_secrets_batch batch_agent_invite batch_api_tasks_reads batch_secrets batch_celery_redbeat batch_setup_cookies batch_redis_budget_cleanup batch_dedicated_proxy_models batch_charter_tools
- batch: shard_06
tag: batch_event_processing_credits batch_marketing_events batch_email batch_video_generation batch_proxy_selection batch_public_templates batch_email_blocklist batch_global_skill_evals batch_xvfb batch_websocket batch_stripe_config batch_peer_intro
- batch: shard_07
tag: batch_sqlite batch_agent_lifecycle batch_console_mcp_servers batch_agent_short_description batch_api_tasks_mutations batch_email_verification batch_peer_dm agent_credit_soft_target_batch batch_console_mcp_oauth batch_native_integrations batch_agent_tags eval_sim batch_recruitment_sourcing batch_image_generation_skill
- batch: shard_08
tag: batch_agent_tools sqlite_analysis batch_event_llm batch_agent_secrets_ctx batch_task_credits batch_allowlist_rules batch_api_decodo batch_llm_intelligence batch_llm_admin_cleanup batch_dedicated_proxy_service batch_pa_step_credits oss_readiness_batch batch_api_serializer batch_fbp_middleware
- batch: shard_09
tag: context_hints_batch batch_text_sanitization batch_billing batch_outbound_delivery batch_console_context batch_agent_limits batch_email_sender_db batch_secrets_profiles batch_contact_requests batch_outbound_dedupe batch_email_allowlist batch_redis_leaks
- batch: shard_10
tag: batch_agent_filesystem batch_api_persistent_agents batch_console_agents_management batch_tool_results batch_owner_billing batch_attachment_guidance batch_link_shortener batch_step_compaction batch_browser_profile batch_email_footer batch_outbound_email batch_web_task_followup
env:
# Ensure Django loads test settings; manage.py still imports config.settings first
DJANGO_SETTINGS_MODULE: config.test_settings
PYTHONUNBUFFERED: "1"
TEST_OUTPUT_DIR: test-results
UV_PYTHON_DOWNLOADS: "never" # use setup-python managed interpreter
# Satisfy config.settings import before test settings override DB to SQLite
DJANGO_SECRET_KEY: test-secret
GOBII_ENCRYPTION_KEY: dummy-encryption-key-for-testing
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_HOST: localhost
POSTGRES_PORT: "5432"
REDIS_URL: redis://localhost:6379/0
# Satisfy LLM fail-fast check; any one provider key is enough
OPENAI_API_KEY: dummy
# Disable Celery worker self-shutdown logic in tests to avoid interfering with CI runner
BROWSER_USE_TASK_MAX_COUNT: "0"
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Set up uv (with cache)
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
ignore-nothing-to-cache: true
- name: Restore venv cache
id: restore-venv
uses: actions/cache/restore@v5
with:
path: .venv
key: venv-${{ runner.os }}-py3.12-${{ hashFiles('uv.lock') }}
restore-keys: |
venv-${{ runner.os }}-py3.12-
# Install dependencies into a project venv using uv, leveraging cache.
# Prefer lockfile if present; otherwise resolve from pyproject.
- name: Sync deps (locked)
if: ${{ hashFiles('uv.lock') != '' }}
run: |
uv sync --frozen --all-extras --dev
- name: Sync deps
if: ${{ hashFiles('uv.lock') == '' }}
run: |
uv sync --all-extras --dev
- name: Add JUnit test runner to venv
run: |
uv pip install -q unittest-xml-reporting
- name: Save venv cache
if: steps.restore-venv.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: .venv
key: venv-${{ runner.os }}-py3.12-${{ hashFiles('uv.lock') }}
- name: Run batch tests (${{ matrix.batch }})
run: |
set -euo pipefail
BATCH_OUTPUT_DIR="$TEST_OUTPUT_DIR/${{ matrix.batch }}"
mkdir -p "$BATCH_OUTPUT_DIR"
TAGS="${{ matrix.tag }}"
TAG_ARGS=()
for TAG in $TAGS; do
TAG_ARGS+=(--tag "$TAG")
done
echo "Running tests with tags: $TAGS"
uv run python manage.py test "${TAG_ARGS[@]}" \
--settings=config.test_settings \
--parallel 1 \
--testrunner=xmlrunner.extra.djangotestrunner.XMLTestRunner
# xmlrunner uses suite name + second-level timestamp for filenames, so
# batch-specific subdirs avoid collisions when multiple matrix jobs
# emit the same TEST-*.xml basename.
shopt -s nullglob || true
for f in TEST-*.xml; do mv "$f" "$BATCH_OUTPUT_DIR/"; done || true
- name: Upload test results artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.batch }}
path: test-results/**
if-no-files-found: warn
retention-days: 7
# No per-batch PR checks to reduce noise; artifacts are uploaded above.
report:
name: Combined Test Results
runs-on: ubuntu-latest
if: ${{ always() }}
needs: [tests, sandbox-server-tests]
steps:
- name: Download all test result artifacts
uses: actions/download-artifact@v8
with:
pattern: test-results-*
merge-multiple: true
path: all-results
- name: Publish combined test results
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: all-results/**/*.xml
check_name: "Combined Test Results"
comment_mode: off
publish-static-assets:
name: Publish Static Assets
runs-on: ubuntu-latest
# Test jobs report independently; staging deploy only needs deploy inputs.
needs: [tag-guard, complexity-guardrails]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
env:
STATIC_BUCKET: gobii-platform-static
STATIC_ASSET_BASE_URL: https://static.gobii.ai/frontend/releases
VITE_BUILD_BASE_URL: https://static.gobii.ai/frontend/releases/${{ github.sha }}/
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install AWS CLI
run: |
python3 -m pip install --user awscli
- name: Build frontend assets
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
npm ci --prefix frontend
npm run build --prefix frontend
test -f static/frontend/manifest.json
python3 scripts/vite_release_manifest.py \
--manifest static/frontend/manifest.json \
--root static/frontend \
--check-local > vite_release_paths.txt
- name: Upload immutable release assets
env:
RELEASE_PREFIX: s3://${{ env.STATIC_BUCKET }}/frontend/releases/${{ github.sha }}
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
aws s3 sync static/frontend/assets "${RELEASE_PREFIX}/assets" \
--endpoint-url "${R2_ENDPOINT}" \
--cache-control "public, max-age=31536000, immutable"
aws s3 cp static/frontend/manifest.json "${RELEASE_PREFIX}/manifest.json" \
--endpoint-url "${R2_ENDPOINT}" \
--cache-control "no-cache, max-age=0, must-revalidate" \
--content-type "application/json"
- name: Verify uploaded release assets
env:
RELEASE_PREFIX: frontend/releases/${{ github.sha }}
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
aws s3api head-object \
--bucket "${STATIC_BUCKET}" \
--key "${RELEASE_PREFIX}/manifest.json" \
--endpoint-url "${R2_ENDPOINT}" >/dev/null
while IFS= read -r relative_path; do
aws s3api head-object \
--bucket "${STATIC_BUCKET}" \
--key "${RELEASE_PREFIX}/${relative_path}" \
--endpoint-url "${R2_ENDPOINT}" >/dev/null
done < vite_release_paths.txt
- name: Verify public asset URL
run: |
set -euo pipefail
curl -fsSI "${STATIC_ASSET_BASE_URL}/${GITHUB_SHA}/manifest.json" >/dev/null
trigger-preview:
name: Dispatch Preview Deployment
runs-on: ubuntu-latest
# Failed tests should not prevent preview environments from updating.
needs: [tag-guard, complexity-guardrails]
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: gobii-ai
repositories: gobii
- name: Notify infra repo
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ steps.app-token.outputs.token }}
repository: gobii-ai/gobii
event-type: preview-ready
client-payload: |
{
"owner": "${{ github.repository_owner }}",
"repo": "${{ github.event.repository.name }}",
"pr_number": ${{ github.event.pull_request.number }},
"head_sha": "${{ github.event.pull_request.head.sha }}",
"head_ref": "${{ github.event.pull_request.head.ref }}",
"html_url": "${{ github.event.pull_request.html_url }}",
"trigger_run_id": ${{ github.run_id }},
"trigger_run_attempt": ${{ github.run_attempt }},
"actor": "${{ github.actor }}"
}
trigger-staging:
name: Dispatch Staging Deployment
runs-on: ubuntu-latest
# Failed tests should not prevent staging from deploying a published asset set.
needs: [tag-guard, complexity-guardrails, publish-static-assets]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: gobii-ai
repositories: gobii
- name: Notify infra repo
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ steps.app-token.outputs.token }}
repository: gobii-ai/gobii
event-type: staging-ready
client-payload: |
{
"owner": "${{ github.repository_owner }}",
"repo": "${{ github.event.repository.name }}",
"ref": "${{ github.ref }}",
"head_sha": "${{ github.sha }}",
"trigger_run_id": ${{ github.run_id }},
"trigger_run_attempt": ${{ github.run_attempt }},
"pusher": "${{ github.actor }}",
"compare_url": "${{ github.event.compare || '' }}"
}