# Main Branch Delivery Flows This document explains what runs when code is proposed to `main`, merged into `main`, and released via tags. Use this with: - [`docs/ci-map.md`](../../docs/ci-map.md) - [`docs/pr-workflow.md`](../../docs/pr-workflow.md) ## Event Summary | Event | Main workflows | | --- | --- | | PR activity (`pull_request_target`) | `pr-intake-checks.yml`, `pr-labeler.yml`, `pr-auto-response.yml` | | PR activity (`pull_request`) | `ci-run.yml`, `sec-audit.yml`, plus path-scoped `pub-docker-img.yml`, `workflow-sanity.yml`, `pr-label-policy-check.yml` | | Push to `main` | `ci-run.yml`, `sec-audit.yml`, plus path-scoped workflows | | Tag push (`v*`) | `pub-release.yml`, `pub-docker-img.yml` publish job | | Scheduled/manual | `sec-codeql.yml`, `feature-matrix.yml`, `test-fuzz.yml`, `pr-check-stale.yml`, `pr-check-status.yml`, `sync-contributors.yml`, `test-benchmarks.yml`, `test-e2e.yml` | ## Runtime and Docker Matrix Observed averages below are from recent completed runs (sampled from GitHub Actions on February 17, 2026). Values are directional, not SLA. | Workflow | Typical trigger in main flow | Avg runtime | Docker build? | Docker run? | Docker push? | | --- | --- | ---:| --- | --- | --- | | `pr-intake-checks.yml` | PR open/update (`pull_request_target`) | 14.5s | No | No | No | | `pr-labeler.yml` | PR open/update (`pull_request_target`) | 53.7s | No | No | No | | `pr-auto-response.yml` | PR/issue automation | 24.3s | No | No | No | | `ci-run.yml` | PR + push to `main` | 74.7s | No | No | No | | `sec-audit.yml` | PR + push to `main` | 127.2s | No | No | No | | `workflow-sanity.yml` | Workflow-file changes | 34.2s | No | No | No | | `pr-label-policy-check.yml` | Label policy/automation changes | 14.7s | No | No | No | | `pub-docker-img.yml` (`pull_request`) | Docker-related PR changes | 240.4s | Yes | Yes | No | | `pub-docker-img.yml` (`push` tag) | Tag push `v*` | 139.9s | Yes | No | Yes | | `pub-release.yml` | Tag push `v*` | N/A in recent sample | No | No | No | Notes: 1. `pub-docker-img.yml` is the only workflow in the main PR/push path that builds Docker images. 2. Container runtime verification (`docker run`) occurs in PR smoke only. 3. Container registry push occurs on tag pushes only. 4. `ci-run.yml` "Build (Smoke)" builds Rust binaries, not Docker images. ## Step-By-Step ### 1) PR from branch in this repository -> `main` 1. Contributor opens or updates PR against `main`. 2. `pull_request_target` automation runs (typical runtime): - `pr-intake-checks.yml` posts intake warnings/errors. - `pr-labeler.yml` sets size/risk/scope labels. - `pr-auto-response.yml` runs first-interaction and label routes. 3. `pull_request` CI workflows start: - `ci-run.yml` - `sec-audit.yml` - path-scoped workflows if matching files changed: - `pub-docker-img.yml` (Docker-related paths only) - `workflow-sanity.yml` (workflow files only) - `pr-label-policy-check.yml` (label-policy files only) 4. In `ci-run.yml`, `changes` computes: - `docs_only` - `docs_changed` - `rust_changed` - `workflow_changed` 5. `build` runs for Rust-impacting changes. 6. On PRs, full lint/test/docs checks run when PR has label `ci:full`: - `lint` - `lint-strict-delta` - `test` - `docs-quality` 7. If `.github/workflows/**` changed, `workflow-owner-approval` must pass. 8. `lint-feedback` posts actionable comment if lint/docs gates fail. 9. `CI Required Gate` aggregates results to final pass/fail. 10. Maintainer merges PR once checks and review policy are satisfied. 11. Merge emits a `push` event on `main` (see scenario 3). ### 2) PR from fork -> `main` 1. External contributor opens PR from `fork/` into `zeroclaw:main`. 2. Immediately on `opened`: - `pull_request_target` workflows start with base-repo context and base-repo token: - `pr-intake-checks.yml` - `pr-labeler.yml` - `pr-auto-response.yml` - `pull_request` workflows are queued for the fork head commit: - `ci-run.yml` - `sec-audit.yml` - path-scoped workflows (`pub-docker-img.yml`, `workflow-sanity.yml`, `pr-label-policy-check.yml`) if changed files match. 3. Fork-specific permission behavior in `pull_request` workflows: - token is restricted (read-focused), so jobs that try to write PR comments/status extras can be limited. - secrets from the base repo are not exposed to fork PR `pull_request` jobs. 4. Approval gate possibility: - if Actions settings require maintainer approval for fork workflows, the `pull_request` run stays in `action_required`/waiting state until approved. 5. Event fan-out after labeling: - `pr-labeler.yml` and manual label changes emit `labeled`/`unlabeled` events. - those events retrigger `pull_request_target` automation (`pr-labeler.yml` and `pr-auto-response.yml`), creating extra run volume/noise. 6. When contributor pushes new commits to fork branch (`synchronize`): - reruns: `pr-intake-checks.yml`, `pr-labeler.yml`, `ci-run.yml`, `sec-audit.yml`, and matching path-scoped PR workflows. - does not rerun `pr-auto-response.yml` unless label/open events occur. 7. `ci-run.yml` execution details for fork PR: - `changes` computes `docs_only`, `docs_changed`, `rust_changed`, `workflow_changed`. - `build` runs for Rust-impacting changes. - `lint`/`lint-strict-delta`/`test`/`docs-quality` run on PR when `ci:full` label exists. - `workflow-owner-approval` runs when `.github/workflows/**` changed. - `CI Required Gate` emits final pass/fail for the PR head. 8. Fork PR merge blockers to check first when diagnosing stalls: - run approval pending for fork workflows. - `workflow-owner-approval` failing on workflow-file changes. - `CI Required Gate` failure caused by upstream jobs. - repeated `pull_request_target` reruns from label churn causing noisy signals. 9. After merge, normal `push` workflows on `main` execute (scenario 3). ### 3) Push to `main` (including after merge) 1. Commit reaches `main` (usually from a merged PR). 2. `ci-run.yml` runs on `push`. 3. `sec-audit.yml` runs on `push`. 4. Path-filtered workflows run only if touched files match their filters. 5. In `ci-run.yml`, push behavior differs from PR behavior: - Rust path: `lint`, `lint-strict-delta`, `test`, `build` are expected. - Docs/non-rust paths: fast-path behavior applies. 6. `CI Required Gate` computes overall push result. ## Docker Publish Logic Workflow: `.github/workflows/pub-docker-img.yml` ### PR behavior 1. Triggered on `pull_request` to `main` when Docker-related paths change. 2. Runs `PR Docker Smoke` job: - Builds local smoke image with Blacksmith builder. - Verifies container with `docker run ... --version`. 3. Typical runtime in recent sample: ~240.4s. 4. No registry push happens on PR events. ### Push behavior 1. `publish` job runs only when `github.event_name == 'push'` and ref starts with `refs/tags/`. 2. Tag format expected by workflow trigger is `v*`. 3. Login to `ghcr.io` uses `${{ github.actor }}` and `${{ secrets.GITHUB_TOKEN }}`. 4. Tag computation includes: - semantic tag from pushed git tag (`vX.Y.Z`) - SHA tag (`sha-<12 chars>`) 5. Multi-platform publish is used for tag pushes (`linux/amd64,linux/arm64`). 6. Typical runtime in recent sample: ~139.9s. 7. Result: pushed image tags under `ghcr.io//`. Important: Docker publish does not run on plain `main` pushes anymore. ## Release Logic Workflow: `.github/workflows/pub-release.yml` 1. Triggered only on tag push `v*`. 2. Builds release artifacts across matrix targets. 3. Generates SBOM (`CycloneDX` + `SPDX`). 4. Generates `SHA256SUMS`. 5. Signs artifacts with keyless cosign. 6. Publishes GitHub Release with artifacts. ## Merge/Policy Notes 1. Workflow-file changes (`.github/workflows/**`) activate owner-approval gate in `ci-run.yml`. 2. PR lint/test strictness is intentionally controlled by `ci:full` label. 3. `sec-audit.yml` runs on both PR and push, plus scheduled weekly. 4. Some workflows are operational and non-merge-path (`pr-check-stale`, `pr-check-status`, `sync-contributors`, etc.). 5. Workflow-specific JavaScript helpers are organized under `.github/workflows/scripts/`. ## Mermaid Diagrams ### PR to Main (Internal/Fork) ```mermaid flowchart TD A["PR opened or updated -> main"] --> B["pull_request_target lane"] B --> B1["pr-intake-checks.yml"] B --> B2["pr-labeler.yml"] B --> B3["pr-auto-response.yml"] A --> C["pull_request CI lane"] C --> C1["ci-run.yml"] C --> C2["sec-audit.yml"] C --> C3["pub-docker-img.yml (if Docker paths changed)"] C --> C4["workflow-sanity.yml (if workflow files changed)"] C --> C5["pr-label-policy-check.yml (if policy files changed)"] C1 --> D["CI Required Gate"] D --> E{"Checks + review policy pass?"} E -->|No| F["PR stays open"] E -->|Yes| G["Merge PR"] G --> H["push event on main"] ``` ### Push/Tag Delivery ```mermaid flowchart TD A["Commit reaches main"] --> B["ci-run.yml"] A --> C["sec-audit.yml"] A --> D["path-scoped workflows (if matched)"] T["Tag push v*"] --> R["pub-release.yml"] T --> P["pub-docker-img.yml publish job"] R --> R1["Artifacts + SBOM + checksums + signatures + GitHub Release"] P --> P1["Push ghcr image tags (version + sha)"] ``` ## Quick Troubleshooting 1. Unexpected skipped jobs: inspect `scripts/ci/detect_change_scope.sh` outputs. 2. Workflow-change PR blocked: verify `WORKFLOW_OWNER_LOGINS` and approvals. 3. Fork PR appears stalled: check whether Actions run approval is pending. 4. Docker not published: confirm a `v*` tag push happened (plain `main` push will not publish).