ci.verification_runner — version 1.1
Source: presets
Purpose
Watches an estate's repository: every push must still compile, every night the live organisation must still match.
Two Cloud Build triggers and a scheduler, in the project that HOSTS the runner:
satz-check on every push to main — satz transpile --check <estate>.
A satz release that no longer compiles the estate fails
the estate's next push.
satz-compliance nightly, via Cloud Scheduler — satz report-compliance --format markdown --out <file> --fail-on <statuses>. An
org policy widened in the console fails the next nightly run.
The runner is a service account of its own. It holds nothing about the
estate: satz's first act inside the build is to exchange the runner's identity
for the ESTATE's IaC service account, exactly as it does on a workstation. The
grant that permits that lives on the estate's account and is the companion
pack, ci — separate because in the hosted
shape the two resources belong to two different parties.
Where it runs decides who it is for:
customer-hosted ci_runner_project = the estate's own infra project; use this pack AND the grant pack in the same estate. MSP-hosted ci_runner_project = the MSP's project, which must be the Cloud Source Repositories project the estate repo lives in (a trigger can only watch a repository in its own project); the MSP's estate uses THIS pack — with ci_repo_name, ci_estate_file and ci_target_infra_project naming the customer's — and the customer's estate uses the grant pack with ci_runner_service_account set to the MSP runner's email. No credential crosses; one IAM binding does.
The build steps are INLINE in the trigger, not a cloudbuild.yaml in the watched repository. Whoever controls the build file controls what runs as the runner's identity; in the hosted shape that file would sit in a repository the MSP does not own. Here the pipeline is defined by whoever applies this pack.
The runner reports through the build's exit code and log. It writes no evidence back into the repository: that needs write access to a repository the runner may not own.
Requires the estate's IaC service account to exist (post-init), and, for the
live nightly run, Cloud Asset Inventory enabled on the estate's infra project
with an asset-viewer role at organization level — the same two read
dependencies report-compliance needs from a workstation.
Use it
params {
// the pack's own defaults — copy only the lines you change
ci_runner_project = infra_project_name
ci_region = default_region
ci_target_infra_project = infra_project_name
ci_repo_name = "{customer_shortname}-{customer_id}"
ci_estate_file = "{customer_id}.satz"
ci_framework = "cis-gcp-4.0"
ci_fail_on = "drifted,not-enforced"
ci_schedule = "0 3 * * *"
ci_time_zone = "Europe/Berlin"
ci_satz_release = "latest"
}
use "presets/ci/verification-runner.satz"
Needs from outside the pack:
customer_id— declared byestate_core; use one of them alongside, or declare it in the estate.customer_shortname— declared byestate_core; use one of them alongside, or declare it in the estate.default_region— declared byestate_core; use one of them alongside, or declare it in the estate.infra_project_name— declared byestate_core; use one of them alongside, or declare it in the estate.
Params
| param | default |
|---|---|
ci_runner_project |
infra_project_name |
ci_region |
default_region |
ci_target_infra_project |
infra_project_name |
ci_repo_name |
"{customer |
ci_estate_file |
"{customer_id}.satz" |
ci_framework |
"cis-gcp-4.0" |
ci_fail_on |
"drifted,not-enforced" |
ci_schedule |
"0 3 * * *" |
ci_time_zone |
"Europe/Berlin" |
ci_satz_release |
"latest" |
Questions
What to ask before this pack is configured, and what changing the answer costs.
| asks about | prompt | reversal | blast | recommends |
|---|---|---|---|---|
ci_runner_project |
The project that hosts the triggers and the runner service account | recreate | low | — |
ci_target_infra_project |
The watched estate's infrastructure project — where the nightly read is billed and Cloud Asset Inventory is enabled | edit | low | — |
ci_repo_name |
The repository the triggers watch, as Cloud Source Repositories names it (a name, not a URL) | edit | low | — |
ci_estate_file |
The estate file inside that repository's yaml_dir | edit | none | — |
ci_runner_project — For a Cloud Source Repositories estate this must be the repository's own project — Cloud Build can only watch a repository in its project. Moving it later recreates the triggers, the runner account and every grant made to it.
ci_target_infra_project — The same as this estate's own project when the customer hosts the runner; an MSP names the customer's. A wrong one fails every nightly run with a quota-project error rather than a finding.
ci_repo_name — The default is the convention <shortname>-<customer id>; a repository named otherwise is watched by nothing, and the check that never runs looks exactly like a check that passes.
ci_estate_file — The push trigger compiles this file and the nightly run judges it. A wrong name fails on the first push — loudly, which is the good case.
Contributes
| resource type | labels |
|---|---|
google_cloud_scheduler_job |
satz_nightly |
google_cloudbuild_trigger |
satz_check, satz_compliance |
google_project_iam_member |
service |
google_project_service |
cloudbuild, cloudscheduler |
google_service_account |
satz_runner |
Claims
None — this pack proves no control by itself.
History
| version | date | change |
|---|---|---|
| 1.1 | 2026-09-10 | four question blocks — the hosting project, the watched estate's infra project, the repository name, the estate file: what the pack cannot know when an MSP hosts the runner. Schedule, time zone, region, catalog, fail-on and release stay technical defaults. Nothing emitted changes |
| 1.0 | 2026-09-09 | first version: continuous verification as a pack. Two Cloud Build triggers in the hosting project — satz-check on every push (transpile --check) and satz-compliance nightly via Cloud Scheduler (report-compliance --fail-on) — plus the runner service account and its two project roles. Build steps are INLINE in the trigger, not a file in the watched repository, so control of the pipeline follows ownership of the service account; satz is installed at build time from the release (ci_satz_release, default latest). The runner never acts as itself — satz exchanges its identity for the estate's IaC account, which the companion grant pack permits. v1 reports through the exit code and log; no evidence write-back |
The whole library's history: the changelog in presets/CHANGELOG.md.
Notes
The build steps are inline in the trigger, not in a cloudbuild.yaml in the repository — whoever can edit the trigger controls what runs as the runner's identity. A Cloud Source Repositories trigger only watches a repository in its own project, so the runner lives in the project that holds the repository.
The customer-hosted shape uses this pack together with ci in the same estate; in the MSP-hosted shape the runner is in the MSP's project and only the grant is in the customer's estate.