Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
--project test-app \
--registry BAD \
--engine-ref "${engine_ref}" \
--approval-evidence "ticket:RT-1042 registry review" \
--output "${temporary_directory}/invalid.json"; then
echo 'initializer accepted an invalid registry' >&2
exit 1
Expand All @@ -86,5 +87,16 @@ jobs:
--organization test-company \
--project test-app \
--engine-ref "${engine_ref}" \
--approval-evidence "ticket:RT-1042 release approved in ops tracker" \
--output "${temporary_directory}/fleet.json"
test "$(stat -c '%a' "${temporary_directory}/fleet.json")" = 644
# The placeholder evidence must fail strict until replaced:
if ./scripts/init.sh \
--organization test-company \
--project test-app \
--engine-ref "${engine_ref}" \
--output "${temporary_directory}/placeholder.json"; then
echo 'initializer accepted a missing approval locator' >&2
exit 1
fi
test ! -e "${temporary_directory}/placeholder.json"
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ flowchart LR
--location primary-site \
--capacity-budget 1 \
--max-runners 1 \
--engine-ref <reviewed-ci-fleet-commit>
--engine-ref <reviewed-ci-fleet-commit> \
--approval-evidence "ticket:RT-1042 where your production approval is recorded"
```

3. Edit `fleet.json` to add the organization's real logical mappings.
Expand All @@ -46,7 +47,17 @@ flowchart LR
./scripts/validate.sh --strict
```

5. Configure secret **values** in GitHub Environments, root-owned host files, or an external secret manager. The repository stores only names such as `DEPLOY_AUTH`.
5. Configure secret **values** in GitHub Environments (only if your plan provides protected Environments for private repositories — GitHub Free does not), root-owned host files, or an external secret manager. The repository stores only names such as `DEPLOY_AUTH`.

## Capability-aware deployment policy

- `organization.github_plan` declares what your GitHub plan can actually provide: `free`, `team`, or `enterprise`. Omitted means `free` (fail closed). Each environment then names its real approval gate:

- `approval_mechanism: github-environment` requires a plan that supports protected Environments **and required reviewers** on private repositories — that is Enterprise only. The validator rejects this on Free, Team, or an undeclared plan.
- `approval_mechanism: manual-external` is the fallback for everyone else. When `requires_approval` is true, `approval_evidence` must record where the exact reviewed commit SHA was approved before any privileged host accepts artifact inputs — a signed release ticket, not a secret and never the CI run itself.
- Omitting `approval_mechanism` infers the fail-closed gate from the declared plan, so pre-existing schema-v3 environments remain valid without edits.

Host-group roles (`deployment`, `persistent-testing`, `image-build`) are logical inventory labels only; core ci-fleet #22 and #23 own the executable privileged-role installers, so this template deliberately does not define deployer or tester runtime fields yet. The validator enforces the isolation that exists today: ordinary-CI routing labels must be unique across pools and must not collide with a privileged host group's name or role, environments may target only deployment-role host groups, and production credentials or authority are never granted to ordinary CI.

The initializer refuses to replace a configured file unless `--force` is explicit. Run `./scripts/init.sh --help` for repository, registry, runner-group, controller, location, capacity, resource, and output options.

Expand Down Expand Up @@ -113,8 +124,9 @@ Deleting one generic controller must not require application workflow changes. L
- Every matrix job has a five-minute hard timeout, while expected test payload targets four minutes or less to reserve startup and reporting time.
- Application workflows submit all independent jobs; infrastructure configuration alone controls worker capacity.
- A GitHub runner group is assigned to exactly one runner pool.
- CI runner pools and deployment host groups are separate trust roles.
- Production deployment is manual and requires GitHub Environment approval.
- CI runner pools and deployment host groups are separate trust roles. Host-group `role` values (`deployment`, `persistent-testing`, `image-build`) are logical inventory labels; ordinary-CI routing labels must never collide with a privileged host group's name or role, and the executable privileged-role boundary lands with core ci-fleet #22/#23.
- Production deployment is manual and gated by a real approval outside the requesting CI identity. The gate is either a protected GitHub Environment with required reviewers (available only when `organization.github_plan` is `enterprise`; required reviewers on private repositories need Enterprise) or an explicit `manual-external` exact-head approval recorded in `approval_evidence`. On GitHub Free and Team, private repositories have no required-reviewer Environment gates — the template rejects any configuration claiming otherwise.
- Approval evidence records where an exact reviewed commit was approved (for example, a signed release ticket). It never contains secret values, and it never names ordinary-CI state as its own approval. Environments may omit `approval_mechanism`: the validator then infers the fail-closed gate from the declared plan, so existing schema-v3 configurations keep validating.
- Controller engine revisions, reusable workflows, and third-party actions are pinned to immutable commits.
- Configuration contains logical identifiers only. Secret values, private host details, and credentials never enter Git.
- Promoted artifacts are container image digests; production does not rebuild a different image.
Expand Down
10 changes: 9 additions & 1 deletion examples/multi-host/fleet.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"slug": "sample-company",
"registry": "ghcr.io/sample-company",
"delivery_engine": "RandomDevelopment/ci-fleet",
"workflow_ref_policy": "immutable-commit"
"workflow_ref_policy": "immutable-commit",
"github_plan": "enterprise"
},
"runner_pools": {
"trusted-ci": {
Expand Down Expand Up @@ -59,25 +60,32 @@
"production-primary": {
"role": "deployment",
"environment_class": "production"
},
"image-build-shared": {
"role": "image-build",
"environment_class": "staging"
}
},
"environments": {
"development": {
"host_group": "development-east",
"automatic": true,
"requires_approval": false,
"approval_mechanism": "github-environment",
"required_secret_names": ["DEPLOY_AUTH"]
},
"staging": {
"host_group": "staging-east",
"automatic": false,
"requires_approval": true,
"approval_mechanism": "github-environment",
"required_secret_names": ["DEPLOY_AUTH"]
},
"production": {
"host_group": "production-primary",
"automatic": false,
"requires_approval": true,
"approval_mechanism": "github-environment",
"required_secret_names": ["DEPLOY_AUTH"]
}
},
Expand Down
10 changes: 9 additions & 1 deletion fleet.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"slug": "example-org",
"registry": "ghcr.io/example-org",
"delivery_engine": "RandomDevelopment/ci-fleet",
"workflow_ref_policy": "immutable-commit"
"workflow_ref_policy": "immutable-commit",
"github_plan": "free"
},
"runner_pools": {
"trusted-ci": {
Expand Down Expand Up @@ -41,19 +42,26 @@
"production-apps": {
"role": "deployment",
"environment_class": "production"
},
"persistent-test-apps": {
"role": "persistent-testing",
"environment_class": "staging"
}
},
"environments": {
"development": {
"host_group": "development-apps",
"automatic": true,
"requires_approval": false,
"approval_mechanism": "manual-external",
"required_secret_names": ["DEPLOY_AUTH"]
},
"production": {
"host_group": "production-apps",
"automatic": false,
"requires_approval": true,
"approval_mechanism": "manual-external",
"approval_evidence": "ticket:RT-1042 signed release approval recording the exact reviewed commit SHA before any privileged host accepts artifact inputs",
"required_secret_names": ["DEPLOY_AUTH"]
}
},
Expand Down
19 changes: 17 additions & 2 deletions fleet.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
"slug": {"type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,38}$"},
"registry": {"type": "string", "pattern": "^[a-z0-9.-]+/[a-z0-9._/-]+$"},
"delivery_engine": {"const": "RandomDevelopment/ci-fleet"},
"workflow_ref_policy": {"const": "immutable-commit"}
"workflow_ref_policy": {"const": "immutable-commit"},
"github_plan": {
"description": "GitHub plan capability class. Omitted means free: protected Environments are unavailable for private repositories and approval_mechanism must be manual-external.",
"enum": ["free", "team", "enterprise"]
}
}
},
"runner_pools": {
Expand Down Expand Up @@ -97,7 +101,10 @@
"additionalProperties": false,
"required": ["role", "environment_class"],
"properties": {
"role": {"const": "deployment"},
"role": {
"description": "Logical inventory label only. Ordinary CI runner pools must never carry a deployment, persistent-testing, or image-build role; the executable role boundary is owned by core ci-fleet #22/#23.",
"enum": ["deployment", "persistent-testing", "image-build"]
},
"environment_class": {"enum": ["development", "staging", "production"]}
}
},
Expand All @@ -109,6 +116,14 @@
"host_group": {"type": "string"},
"automatic": {"type": "boolean"},
"requires_approval": {"type": "boolean"},
"approval_mechanism": {
"description": "github-environment requires organization.github_plan enterprise (required reviewers on private repositories); manual-external records exact-head approval evidence outside GitHub. Omitted (valid for schema-v3 compatibility) infers the fail-closed mechanism for the declared plan.",
"enum": ["github-environment", "manual-external"]
},
"approval_evidence": {
"description": "For manual-external with requires_approval true: where the exact-head approval is recorded (logical reference only, never secret values).",
"type": "string"
},
"required_secret_names": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]*$"}}
}
},
Expand Down
15 changes: 15 additions & 0 deletions scripts/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--runner-cpu-cores", type=positive_integer, default=2, help="CPU cores available to each runner")
parser.add_argument("--runner-memory-mib", type=positive_integer, default=4096, help="memory available to each runner")
parser.add_argument("--engine-ref", required=True, help="reviewed full ci-fleet commit SHA")
parser.add_argument("--github-plan", choices=("free", "team", "enterprise"), default="free", help="GitHub plan capability class; free forbids protected-Environment approvals")
parser.add_argument("--approval-evidence", help="where the exact-head production approval is recorded (ticket, path, or system reference); required for manual-external production so the generated configuration passes --strict")
parser.add_argument("--output", type=Path, default=ROOT / "fleet.json", help="output configuration path")
parser.add_argument("--force", action="store_true", help="replace an existing non-example output file")
return parser.parse_args()
Expand Down Expand Up @@ -69,6 +71,14 @@ def main() -> int:
fail("--max-runners must not exceed --capacity-budget")
if args.runner_memory_mib < 512:
fail("--runner-memory-mib must be at least 512")
if args.github_plan != "enterprise" and not args.approval_evidence:
fail("--approval-evidence is required when --github-plan is not enterprise (structured locator: type:value, e.g. ticket:RT-1042 or url:https://tracker.example/RT-1042)")
Comment on lines +74 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Supply evidence in the invalid-registry workflow case

When the Validate configuration workflow exercises its invalid-registry case at .github/workflows/validate.yml:75-83, it omits --approval-evidence, so this new early exit rejects the invocation before BAD reaches registry validation. I confirmed that the command now fails solely with the missing-evidence error, meaning the workflow remains green even if initializer registry validation regresses; pass valid approval evidence in that negative test so it fails for the condition it claims to cover.

AGENTS.md reference: AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

approval_mechanism = "github-environment" if args.github_plan == "enterprise" else "manual-external"
# Structured locator required for manual-external: type:value (ticket, url, system, doc)
# The placeholder is deliberately rejected by --strict until replaced with a real locator.
production_evidence = args.approval_evidence or (
"REPLACE-ME: structured locator for the exact reviewed commit SHA approval (type:value)"
)
Comment thread
Nickfost marked this conversation as resolved.

repository = args.repository or f"{args.organization}/{args.project}"
registry = (args.registry or f"ghcr.io/{args.organization}").rstrip("/")
Expand All @@ -90,6 +100,7 @@ def main() -> int:
"registry": registry,
"delivery_engine": "RandomDevelopment/ci-fleet",
"workflow_ref_policy": "immutable-commit",
"github_plan": args.github_plan,
},
"runner_pools": {
"trusted-ci": {
Expand Down Expand Up @@ -126,12 +137,16 @@ def main() -> int:
"host_group": "development-apps",
"automatic": True,
"requires_approval": False,
"approval_mechanism": approval_mechanism,
**({"approval_evidence": "signed release ticket recording the exact reviewed commit SHA"} if approval_mechanism == "manual-external" else {}),
"required_secret_names": ["DEPLOY_AUTH"],
},
"production": {
"host_group": "production-apps",
"automatic": False,
"requires_approval": True,
"approval_mechanism": approval_mechanism,
**({"approval_evidence": production_evidence} if approval_mechanism == "manual-external" else {}),
"required_secret_names": ["DEPLOY_AUTH"],
Comment thread
Nickfost marked this conversation as resolved.
},
},
Expand Down
Loading