# CI Documentation

This document describes the current CI workflows for `agent-git-service`.

## Overview

The repository uses a layered CI gate:

- a fast `Regression Gate` for compile-all plus stable backend regressions
- blocking `Unit Tests` for the full backend Go test inventory
- blocking `Integration Tests` for stable real-router packages
- blocking `Compatibility Tests` plus full `E2E Tests`
- blocking smoke jobs that exercise real server startup and a higher-fidelity repo lifecycle path
- a separate `doc-lint.yml` workflow so process drift is caught outside the main test lane

`ci.yml` is the primary merge-gate workflow.
Most previously split checks are now absorbed into the regression, unit, and integration layers there:

- vendored CLI and `cli/_go-gh-local` compile coverage stays in `regression-gate`
- `internal/gitstore` stays in `regression-gate`
- `internal/githttp/...` stays in both `regression-gate` and `integration-tests`

`cli-pr-check.yml` is intentionally retained as a separate workflow with its original behavior.
It still runs the full vendored `cli/` unit-test inventory on pull requests and manual dispatch, but it is not part of the main layered `ci.yml` gate.

## Workflow Files

- `ci.yml` - main layered CI gate for pull requests, pushes to `main`, and manual runs
- `cli-pr-check.yml` - preserves the original full vendored `cli/` unit-test workflow for pull requests and manual runs
- `doc-lint.yml` - verifies core CI/testing docs and workflow inventory stay in sync
- `secret-scan.yml` - runs gitleaks against checked-out files on pull requests and pushes to `main`; manual runs can also scan full git history
- `license-check.yml` - verifies the root Go module dependency license allowlist and reviewed third-party NOTICE files
- `docker-build.yml` - builds and pushes the backend image only after the main `CI` workflow succeeds for a `main` branch push unless `ENABLE_INTERNAL_PUBLISH=false` is set as a repository variable
- `trace-bench.yml` - runs the full `scripts/bench-gh.sh --runs 5 --warmups 1` suite after merges to `main` and appends the result plus merged PR metadata to the configured benchmark history repository unless `ENABLE_TRACE_BENCH=false` is set as a repository variable

## GitHub Actions Versions

All workflows use Node.js 24 compatible action versions (updated April 2026):

- `actions/checkout@v5` - repository checkout
- `actions/setup-go@v6` - Go toolchain setup
- `aws-actions/configure-aws-credentials@v4` - AWS credential configuration
- `aws-actions/amazon-ecr-login@v2` - ECR authentication

## Main CI Workflow

Trigger:

- pull requests targeting `main`
- pushes to `main`
- manual `workflow_dispatch`

Jobs:

1. `lint`
   - checks Go formatting outside the nested `cli/` module
   - runs `go vet ./...`
   - verifies that the container entrypoint supplies legacy runtime defaults only when `AGS_CONFIG_FILE` is unset
2. `regression-gate`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - starts the local TiDB playground before running database-backed regression packages
   - runs `bash scripts/regression_gate.sh`
   - compiles the full backend tree with `go test ./... -run '^$'`
   - compiles the vendored `cli/` tree and `cli/_go-gh-local` without promoting the known-flaky full suites into a blocking behavior gate
   - runs the stable package pack defined in `scripts/regression_gate.list`
   - stops the local TiDB playground after the job
3. `unit-tests`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - runs `make test-unit` in a package matrix so heavy database-backed packages do not contend for the same TiDB playground
   - executes the full backend Go unit-test inventory with `go test -v`, split across the matrix by `GO_TEST_PACKAGES`
   - `make test-unit` starts the local TiDB playground and drops stale per-test schemas before running Go tests
   - stops the local TiDB playground after the job
4. `integration-tests`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - starts the local TiDB playground before running real-router integration packages
   - runs `bash scripts/integration_tests.sh`
   - executes the stable real-router integration pack:
     - `./internal/testharness`
     - `./internal/router`
     - `./internal/githttp/...`
     - curated integration subsets inside `./internal/rest`
     - curated integration subsets inside `./internal/graphql`
   - the shared `internal/testharness` package now accepts `testing.TB` via `NewTB` and `Harness.Server`, so benchmark suites can reuse the same production-wired router harness and cleanup behavior as integration tests
   - stops the local TiDB playground after the job
5. `tidb-migration-smoke`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - runs `make test-migrate-tidb`
   - creates a fresh temporary TiDB database
   - boots `gh-server` against that TiDB DSN and waits for `/readyz`
   - catches TiDB-incompatible migration/bootstrap DDL before the heavier compatibility and E2E suites
6. `compatibility-tests`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - runs `make test-setup` to provision `github.localhost`, `.env`, TiDB playground, and the build
   - boots `gh-server` on port `80` against the TiDB DSN from `.env`
   - runs full `make test`
   - cleans up with `make test-clean-all`
   - executes the full vendored `gh` acceptance inventory under `cli/acceptance/testdata`
7. `e2e-tests`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - runs `make test-setup` to provision `github.localhost`, `.env`, TiDB playground, and the build
   - starts the deterministic mock OIDC issuer used by the OIDC-backed login E2E flows
   - boots `gh-server` on port `80` against the TiDB DSN from `.env`
   - runs full `make test-e2e`
   - cleans up with `make test-clean-all`
   - executes the full shell E2E inventory under `e2e/`
   - the blocking E2E inventory includes `e2e/agent-auth-flow.sh`, including the agent-binding confirm happy path and the human-token `403`, invalid-invite `422`, and consumed-invite `409` regression checks
8. `backend-smoke`
   - installs `tiup` plus the MySQL client for the test-only TiDB playground
   - starts the local TiDB playground before running the smoke flow
   - runs `bash scripts/backend_smoke.sh`
   - builds and starts `gh-server` against a fresh TiDB playground database
   - probes `/readyz`, `/api/v3/`, `/api/v3/meta`, and `/api/v3/rate_limit`
   - creates a repository through the REST API
   - validates Git Smart HTTP clone with token auth
   - stops the local TiDB playground after the job
9. `ci-success`
   - fails if any required CI job fails

This gives the repo one explicit merge gate with a clear layer split:

- `Regression Gate` protects fast, deterministic regressions
- `Unit Tests` protect the full backend Go unit-test inventory
- `Integration Tests` protect stable real-router and Git HTTP paths
- `TiDB Migration Smoke` protects service bootstrap and migrations against a real TiDB dialect
- `Compatibility Tests` protect the full vendored gh CLI compatibility inventory
- `E2E Tests` and `Backend Smoke` prove runtime behavior on real process boundaries
- `doc-lint.yml` keeps the gate definitions and expectations documented

The dedicated repo transfer smoke job has been removed from the main CI gate because
the same `e2e/repo-transfer-lifecycle.sh` coverage already runs inside the blocking
full `make test-e2e` inventory.

### Targeted Wiki Regression Gate

`bash scripts/wiki_regression_gate.sh` provides a focused local gate using the
configured test TiDB instance. It covers stale projections, search, REST links,
interrupted publication recovery, cross-endpoint read consistency, and durable
reconcile worker lease/request fencing. It runs Go tests with 12 package and test
workers and `GOMAXPROCS=12`. These tests also belong to the full backend test
inventory; the script does not add a separate GitHub Actions job. Passing it is
not evidence that the fix has been deployed or a remote wiki has been published.

## Secret Scan Workflow

`secret-scan.yml` runs `scripts/secret_scan.sh --worktree-only` on pull requests
targeting `main` and pushes to `main`. The workflow installs a pinned gitleaks
version (`v8.30.1`) through Go so scan behavior stays deterministic. The
workflow resolves the install path from `$RUNNER_TEMP` inside the job steps
instead of declaring `runner.*` expressions in workflow-level `env`, which
keeps the workflow valid under GitHub Actions expression scoping rules.

Manual `workflow_dispatch` runs include a `full_history` input. When enabled,
the workflow also runs `scripts/secret_scan.sh --history-only` against the full
git history. Use that mode before public releases or after any suspected secret
exposure. The default PR path intentionally scans only checked-out files so the
gate is fast and focused on newly introduced leaks.

## Dependency License Workflow

`license-check.yml` runs `python3 scripts/license_check.py --report
artifacts/dependency-licenses.tsv` on pull requests, pushes to `main`, and
manual dispatch. The script scans the root Go module build list, checks each
module's top-level license files against the reviewed allowlist, and fails on
unknown or disallowed licenses.

The script also reports third-party NOTICE files. Currently reviewed NOTICE
entries must remain present in the root `NOTICE`; newly introduced NOTICE files
fail the workflow until they are reviewed and documented in
[`docs/governance/dependency-licensing.md`](governance/dependency-licensing.md).

## Documentation Workflow

### `doc-lint.yml`

Runs `python3 scripts/doc_lint.py` to verify that:

- required CI/testing docs and workflow files exist
- relative links inside the core CI/test docs resolve
- `docs/ci.md` mentions the active core workflows
- `.github/pull_request_template.md` still points contributors at the current CI/test docs
- the CI helper scripts used by the main gate still exist
- every top-level `internal/*` package has a contract entry in `docs/module-contracts.md` via `scripts/check-module-contracts.sh`

## Container Build Workflow

`docker-build.yml` remains the image publication lane. Its automatic path runs
after the main `CI` workflow completes successfully for a push to `main`, then
checks out the exact `head_sha` that passed `CI` before it:

- logs in to ECR
- builds the backend image
- pushes a short-sha tag and refreshes the mutable `:main` tag

The successful automatic build continues to trigger
`deploy-staging-after-docker.yml`, which deploys that image to staging.

The same build workflow also accepts a manual `workflow_dispatch` with a
required `source_sha`: a full lowercase 40-character commit SHA. Dispatch the
workflow on `main` so it uses the existing AWS OIDC role; the checkout and image
contents come from `source_sha`, which may be a production hotfix outside main.
The workflow verifies the checked-out commit before requesting AWS credentials.
For example:

```bash
gh workflow run docker-build.yml --ref main -f source_sha=<full-commit-sha>
```

Manual builds use the same Dockerfile, build arguments and ECR repository as
automatic builds, but publish only the selected commit's short-sha image tag.
They do not update `:main` or trigger staging or production deployment. Manual
builds have separate concurrency groups and do not cancel automatic main
builds. This path does not require a successful main CI run; review and test
the selected commit before dispatching it.

Production tag deployment is unchanged: pushing a `v*` tag triggers
`deploy-production-tag.yml`, which verifies that the tagged commit is contained
in `origin/main`, then requests production deployment of its short-sha image
through `aws-cd`. The image must already have been published. The timestamp
release-tag convention remains `vYYYYMMDD-HHMMSS`.

The backend image entrypoint supplies the legacy `LISTEN_MODE=production` and
`GIT_REPO_DIR=/data/repos` defaults only when `AGS_CONFIG_FILE` is unset. In
config-file mode, the image leaves those variables unset so the strict runtime
configuration has a single source of truth.

This workflow preserves the current repository behavior by default.
Set the repository variable `ENABLE_INTERNAL_PUBLISH=false` in repositories
that must suppress image publishing, such as public mirrors or forks without
the required AWS secrets.

## Trace Bench Workflow

`trace-bench.yml` is a post-merge observability lane rather than a merge gate. It triggers on:

- pushes to `main`
- manual `workflow_dispatch`

This workflow runs by default for the current repository and can be disabled by
setting the repository variable `ENABLE_TRACE_BENCH=false`.

The workflow:

- provisions the same local GitHub-compatible API environment shape used by the acceptance lane
- starts the trace bench server with `ENABLE_WORKFLOW_EXEC=1` because the benchmark suite exercises workflow dispatch/run/view scenarios and workflow execution is fail-closed by default
- runs the full `scripts/bench-gh.sh --runs 5 --warmups 1` suite against `main`
- resolves the pull request associated with the merged commit SHA
- checks out the repository named by `TRACE_BENCH_HISTORY_REPO` and appends one markdown entry to that repo's `trace_bench.md` containing the merged PR metadata, the PR merged timestamp, and the benchmark summary table
- commits the updated `trace_bench.md` back to the configured benchmark history repository
- requires `TRACE_BENCH_HISTORY_REPO` as a repository variable and a `TRACE_BENCH_PUSH_TOKEN` secret in this repository with `contents:write` access to that history repository

If the triggering `main` commit is not associated with a pull request, the workflow falls back to commit metadata and still records the benchmark entry as `PR unresolved`. `trace_bench.md` entries in the configured history repository are keyed by merge commit SHA so reruns can avoid duplicate rows for the same merge.

## Workflow Execution Sandbox

The repository supports sandboxed workflow execution for GitHub Actions-style workflow runs.
This capability is **fail-closed by default** and must be explicitly enabled via the `ENABLE_WORKFLOW_EXEC` environment variable.

### Configuration

Workflow execution is controlled by the following environment variables:

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_WORKFLOW_EXEC` | `false` | Enable workflow execution sandbox (set to `true` or `1`) |
| `WORKFLOW_EXEC_IMAGE` | `bash:5.2` | Docker image used for workflow step execution |
| `WORKFLOW_EXEC_TIMEOUT` | `2m` | Maximum duration for workflow execution (e.g., `5m`, `10m`) |
| `WORKFLOW_EXEC_CPUS` | `1.0` | CPU limit for workflow containers |
| `WORKFLOW_EXEC_MEMORY` | `256m` | Memory limit for workflow containers |
| `WORKFLOW_EXEC_PIDS_LIMIT` | `128` | Maximum number of processes in workflow containers |
| `WORKFLOW_EXEC_NOFILE` | `1024` | Maximum number of open file descriptors |
| `WORKFLOW_EXEC_TMPFS_SIZE` | `64m` | Size of tmpfs mount for `/tmp` |

### Security Model

Workflow steps run in isolated Docker containers with the following security constraints:

- `--network none` - no network access
- `--read-only` - read-only root filesystem
- `--cap-drop ALL` - drop all Linux capabilities
- `--security-opt no-new-privileges` - prevent privilege escalation
- Resource limits via `--memory`, `--cpus`, `--pids-limit`, `--ulimit nofile`
- Workspace mounted at `/workspace` with bind mount
- `/tmp` mounted as tmpfs with configurable size

The sandbox is designed to execute untrusted workflow YAML from repository contents while preventing host access or resource exhaustion.

### Testing Coverage

Workflow execution is tested through:

- `internal/service/workflow_exec.go` - core execution logic
- `internal/service/workflow_exec_internal_test.go` - unit tests for sandbox file writing and environment parsing
- `internal/service/workflow_dispatch_exec_test.go` - integration tests for workflow dispatch and execution
- `internal/service/workflow_coverage_test.go` - coverage tracking for workflow-related code paths

## Known Gaps

The following capabilities are still intentionally outside the main merge gate:

- the full vendored `cli/` unit-test inventory inside the main layered merge gate; that inventory still runs separately through `cli-pr-check.yml`
- scheduled daily builds, coverage reporting, and release automation
- supply-chain extras such as SBOM generation, signing, or provenance attestations

As additional suites become reliable, promote the smallest stable regressions into `scripts/regression_gate.list` first, then consider widening the required smoke or compatibility lanes.
