diff --git a/README.md b/README.md index 296cda8..0a331f1 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,6 @@ The boundary and source material come from: - `declarations/rail.yaml` — source-controlled rail contract - `docs/` — wave-1 contract and import plan +- `schemas/` — machine-readable workload contract schema +- `examples/` — canonical example workload declarations for the rail - `workplans/` — repo-local execution work diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index a266b74..98e3aed 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,4 +12,4 @@ | task | RAIL-K8S-WP-0001-T01 | done | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md | | task | RAIL-K8S-WP-0001-T02 | progress | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md | | task | RAIL-K8S-WP-0001-T03 | wait | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md | -| task | RAIL-K8S-WP-0001-T04 | wait | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md | +| task | RAIL-K8S-WP-0001-T04 | progress | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md | diff --git a/docs/README.md b/docs/README.md index 6534413..1c50899 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,6 @@ # rail-kubernetes Docs +- `app-toml-contract.md` — canonical workload declaration contract for the rail +- `deployment-lifecycle.md` — canonical stage 1 / 2 / 3 lifecycle contract - `wave-1-contract.md` — current rail contract and boundary summary - `source-import-plan.md` — source material and migration direction from existing repos diff --git a/docs/app-toml-contract.md b/docs/app-toml-contract.md new file mode 100644 index 0000000..3088047 --- /dev/null +++ b/docs/app-toml-contract.md @@ -0,0 +1,240 @@ +# Railiance app.toml Contract + +This document defines the repository-local `railiance/app.toml` contract used by +Railiance staged promotion tooling. The file tells Railiance how a workload +moves through Stage 1 local validation, Stage 2 production canary, and Stage 3 +production promotion without relying on bespoke operator notes. + +The contract is intentionally declarative. Commands, health checks, platform +dependencies, and secret references are described by stable names. Plaintext +secrets, bearer tokens, kubeconfigs, and private key material must never appear +in `railiance/app.toml`. + +Ownership note: this contract now lives in `rail-kubernetes` as part of the +wave-1 split. `railiance-cluster` may keep compatibility copies during the +migration window, but long-term ownership belongs with the generic +workload-on-Kubernetes contract here rather than with S2 cluster substrate +ownership. + +The machine-readable schema lives at `schemas/railiance-app.schema.json`. A +minimal example lives at `examples/railiance/app.toml`. + +## File Location + +Participating workload repositories declare the contract at: + +```text +railiance/app.toml +``` + +Overlay repositories for third-party applications use the same path in the +overlay repo, not in the upstream source repository. + +## Versioning + +Every file must include: + +```toml +schema_version = "railiance.app.v1" +``` + +Breaking contract changes require a new schema version. Tooling must fail closed +when it sees an unsupported `schema_version`. + +## Top-Level Sections + +### app + +Identifies the workload and its ownership boundary. + +Required fields: + +- `id`: stable lowercase id using letters, numbers, and hyphens. +- `name`: human-readable workload name. +- `repo`: owning source or overlay repository slug. +- `owner`: owning team, domain, or operator group. +- `criticality`: one of `low`, `medium`, `high`, or `critical`. +- `description`: short purpose statement. + +Production-critical workloads include source forge, identity, State Hub, +Inter-Hub, databases, object stores, backup systems, ingress, and cluster-wide +policy controllers. For those workloads, `criticality = "critical"` requires +explicit human approval before Stage 2 traffic exposure and Stage 3 promotion. + +### source + +Identifies the candidate under promotion. + +Required fields: + +- `revision`: commit id, tag, or immutable source revision expression. +- `artifact`: artifact kind, normally `image`, `helm-chart`, or `bundle`. +- `digest_policy`: one of `required`, `preferred`, or `not-applicable`. + +If an image is promoted, Stage 2 and Stage 3 tooling should prefer immutable +image digests over mutable tags. + +### platform.dependencies + +Declares platform services required before canary or production promotion. + +Each dependency has: + +- `name`: stable service name. +- `kind`: dependency kind such as `postgres`, `redis`, `object-store`, + `identity`, `state-hub`, `inter-hub`, `network`, or `other`. +- `required`: boolean. +- `stage`: earliest stage that needs it, one of `stage1`, `stage2`, `stage3`. +- `evidence`: non-secret evidence expected before promotion, such as a health + endpoint result, Kubernetes Ready condition, or State Hub progress id. + +### secrets.references + +Declares required secret references without secret values. + +Each reference has: + +- `name`: workload-local secret name. +- `route`: approved credential route id, for example `openbao-api-key`, + `key-cape-oidc-login`, or `activity-core-issue-sink`. +- `target`: non-secret target reference such as a Kubernetes Secret name, + ExternalSecret name, OpenBao path, or environment variable name. +- `stage`: earliest stage that needs the secret. +- `required`: boolean. + +Forbidden fields include plaintext values, tokens, passwords, kubeconfigs, or +private keys. Tooling must reject suspicious field names such as `value`, +`token`, `password`, `secret`, `private_key`, or `kubeconfig` inside secret +reference objects unless they are part of the approved non-secret `target` text. + +### observability + +Defines how promotion tooling proves the workload is alive and observable. + +Required fields: + +- `health_endpoints`: one or more HTTP health endpoint declarations. +- `metrics`: optional metrics endpoint or query references. +- `logs`: optional log selectors or query references. + +Health endpoint declarations include `name`, `url`, `stage`, and expected +status code. URLs may be internal service URLs for Stage 2/3; they must not +embed credentials. + +### rollback + +Defines how the workload returns to a previous stable state. + +Required fields: + +- `strategy`: one of `helm-revision`, `image-digest`, `traffic-shift`, + `manual-runbook`, or `none`. +- `command`: command name or runbook path. This may be a placeholder before + T07 implements automation, but it must tell the operator where rollback lives. +- `verification`: non-secret check to confirm rollback succeeded. + +`strategy = "none"` is allowed only for Stage 1-only workloads and must not be +used for production-critical workloads. + +## Stage Sections + +The contract has one table for each stage: + +```toml +[stages.stage1] +[stages.stage2] +[stages.stage3] +``` + +Each stage includes: + +- `enabled`: boolean. +- `namespace`: target Kubernetes namespace, or a local namespace for Stage 1. +- `release`: release identity. +- `commands`: ordered command aliases or shell commands that tooling may run. +- `checks`: ordered check ids to evaluate. +- `evidence`: expected non-secret evidence outputs. +- `requires_approval`: boolean. + +Stage 2 additionally includes `canary_mode`, one of `weighted`, `header`, +`path`, `shadow`, or `isolated`, plus `observation_minutes` and optional +`traffic_percent` when weighted routing is used. + +Stage 3 additionally includes `promotion_mode`, one of `traffic-shift`, +`release-replace`, `selector-switch`, or `workflow`, plus `previous_stable`. + +## Check Definitions + +Checks live under `[[checks]]` entries and are referenced by stage `checks`. + +Required fields: + +- `id`: stable check id. +- `type`: one of `command`, `http`, `kubernetes`, `helm`, `metric`, `log`, or + `manual`. +- `stage`: earliest stage that may run the check. +- `description`: human-readable purpose. +- `required`: boolean. + +Type-specific fields: + +- `command`: `run` command string and optional `timeout_seconds`. +- `http`: `url`, `expected_status`, and optional `timeout_seconds`. +- `kubernetes`: `namespace`, `resource`, and `condition`. +- `helm`: `chart`, `values`, and `mode` such as `template` or + `server-dry-run`. +- `metric`: `query`, `window_minutes`, and `threshold`. +- `log`: `selector`, `window_minutes`, and `forbidden_patterns`. +- `manual`: `evidence_required` text. + +Checks must not print secrets. If a check needs secret-backed access, the result +records only the route, target object, and pass/fail state. + +## Command Semantics + +Commands in `app.toml` are declarations for Railiance tooling. Stage 1, Stage +2, and Stage 3 commands now have local CLI support; workload scripts may still +wrap them for service-specific checks. + +Expected mapping: + +- Stage 1 commands are consumed by `bin/railiance run `. +- Stage 2 commands are consumed by `bin/railiance deploy --stage 2 ` + and `bin/railiance observe --stage 2 `. +- Stage 3 commands are consumed by `bin/railiance promote ` and + `bin/railiance rollback `. + +Tooling must emit machine-readable results with workload identity, candidate +revision, checks run, pass/fail status, non-secret evidence, rollback target, +and approval state. + +## Minimal Example + +See `examples/railiance/app.toml`. It declares a critical internal service with: + +- immutable image digest requirement; +- Stage 1 local validation; +- Stage 2 isolated canary; +- Stage 3 release replacement; +- OpenBao-routed secret references without values; +- HTTP, Helm, Kubernetes, and manual approval checks. + +## Adoption Rules + +A workload can enter Stage 1 when `app.toml` passes schema validation and all +Stage 1 required checks are declared. + +A workload can enter Stage 2 only when: + +- Stage 1 passed for the same candidate artifact; +- Stage 2 namespace, release, canary mode, health checks, dependencies, and + rollback target are declared; +- secret references use approved routes and contain no values; +- production-critical workloads have explicit approval. + +A workload can enter Stage 3 only when: + +- Stage 2 acceptance gates passed for the same candidate artifact; +- `previous_stable` and rollback verification are recorded; +- backup/restore posture is current for stateful workloads; +- production-critical workloads have explicit human approval. diff --git a/docs/deployment-lifecycle.md b/docs/deployment-lifecycle.md new file mode 100644 index 0000000..58f2939 --- /dev/null +++ b/docs/deployment-lifecycle.md @@ -0,0 +1,340 @@ +# Railiance Deployment Lifecycle + +This document defines the Railiance three-stage promotion lifecycle for +workloads that run on the Railiance Kubernetes substrate. + +The lifecycle exists so production workloads move through repeatable gates +instead of one-off operator memory. It is intentionally conservative: every +stage must leave evidence, every promotion must have a rollback path, and +critical workloads require explicit human approval before production traffic is +changed. + +Ownership note: this specification now lives in `rail-kubernetes` as the +wave-1 home of the default Kubernetes execution rail. `railiance-cluster` may +retain compatibility shims during migration, but long-term ownership of the +generic lifecycle contract belongs here rather than in the S2 substrate repo. + +## Scope + +This specification now lives beside the Kubernetes rail contract rather than +beside the cluster substrate. The compatibility tooling may still live +elsewhere during migration, but long-term ownership is not S2. + +Repo boundaries: + +- `rail-kubernetes` owns lifecycle semantics, generic promotion gates, + canary validation, production routing expectations, rollback expectations, + and the generic workload contract. +- `railiance-cluster` owns only the substrate prerequisites published to that + rail: cluster connectivity, namespace behavior, ingress availability, and + operator/runtime expectations. +- `railiance-apps` owns workload-specific Helm values, application release + definitions, and production workload configuration. +- `railiance-platform` owns shared platform services such as databases, + caches, object storage, and backup targets. +- `railiance-enablement` owns developer-facing templates, CI workflows, and + local ergonomics. +- `railiance-infra` owns host provisioning, OS hardening, SSH, firewall, and + node bootstrap below Kubernetes. + +## Lifecycle Overview + +Railiance promotes a workload through three stages: + +1. Stage 1: local validation. +2. Stage 2: production canary. +3. Stage 3: production promotion. + +The stages are sequential. A workload may return to an earlier stage at any +time, but it must not skip a stage when moving toward production unless an +operator records an emergency exception in State Hub. + +Each stage emits a machine-readable result with: + +- workload identity; +- source revision or image digest; +- target environment; +- checks run; +- pass/fail status; +- non-secret evidence references; +- rollback target, when applicable; +- approving human or explicit "not required" decision. + +## Workload Declaration + +Each participating workload should declare its promotion contract in a +repository-local `railiance/app.toml`. The contract is defined in +`docs/app-toml-contract.md`, with a machine-readable schema at +`schemas/railiance-app.schema.json`. This lifecycle expects every workload +declaration to provide at least: + +- stable workload name and owning repo; +- source revision, image tag, or image digest policy; +- stage-specific namespaces or release names; +- health checks and observability endpoints; +- ingress or routing targets; +- platform dependencies; +- rollback command or previous-stable reference; +- secret references by name or path, never plaintext secret values. + +If a workload cannot provide a machine-readable declaration yet, it may still +use this lifecycle through a written operator runbook, but that is a temporary +compatibility path. The runbook must identify the missing declaration fields. + +## Stage 1: Local Validation + +Stage 1 proves that the workload can be built, configured, and checked outside +production traffic. + +Typical Stage 1 targets: + +- local container runtime; +- local Kubernetes such as k3d, kind, or a disposable namespace; +- dry-run Helm rendering; +- unit, integration, migration, and smoke checks that do not require production + credentials. + +Required Stage 1 checks: + +- source revision is cleanly identified; +- build or artifact selection is deterministic; +- Helm templates or manifests render without invalid Kubernetes objects; +- local health checks pass; +- required secrets are referenced by name only and are not printed; +- database migrations, if any, are classified as reversible, forward-only, or + requiring human approval; +- a Stage 2 candidate artifact is named by immutable digest or equivalent + immutable revision. + +Stage 1 fails closed when: + +- local checks are skipped without an approved reason; +- generated manifests contain plaintext secrets; +- the artifact cannot be traced to source; +- the workload cannot state how it will be observed in Stage 2. + +Stage 1 completion does not authorize production traffic. It only makes a +workload eligible for Stage 2 review. + +## Stage 2: Production Canary + +Stage 2 deploys the candidate to production infrastructure with limited or +isolated exposure. The goal is to observe the candidate against real platform +dependencies while keeping blast radius small. + +Acceptable canary forms: + +- weighted ingress split between stable and canary; +- header-based or path-based routing for operator traffic only; +- shadow deployment receiving replicated non-mutating traffic; +- isolated production namespace with manually triggered probes. + +The selected canary form must be declared before deployment. If weighted +routing is unavailable, the fallback must preserve the same safety property: +the candidate can be observed without silently replacing stable production. + +Required Stage 2 prechecks: + +- Stage 1 result passed for the same candidate artifact; +- cluster connectivity and namespace readiness are verified; +- target image digest or immutable tag exists in the registry; +- Helm server-side dry-run succeeds; +- ingress, certificate, and DNS prerequisites are present where applicable; +- platform dependencies are healthy or explicitly degraded with operator + approval; +- rollback target is known before the canary is applied; +- monitoring and log queries are available for the canary release. + +Required Stage 2 evidence: + +- rendered release identity; +- applied namespace and release name; +- pod readiness and restart status; +- ingress or routing state; +- key health endpoint result; +- relevant metrics window or explicit "metrics unavailable" note; +- State Hub progress note with non-secret evidence; +- operator approval when the workload is production-critical. + +Canary acceptance gates: + +- canary pods remain ready for the configured observation window; +- no crash loops, repeated restarts, or pending pods remain unexplained; +- health checks pass from inside and outside the cluster when both are + applicable; +- error rate, latency, and saturation do not regress beyond the workload's + declared threshold; +- no unexpected schema, storage, or queue side effects are observed; +- logs show no secret leakage and no repeated authorization failures; +- rollback has been tested previously or is a single documented command with a + known previous-stable target. + +Default observation windows: + +- non-critical internal service: 15 minutes; +- user-facing or shared platform service: 30 minutes; +- production-critical infrastructure such as Forgejo, identity, registry, or + State Hub: operator-defined window, minimum 60 minutes unless explicitly + waived. + +Stage 2 fails closed when: + +- the canary cannot be distinguished from stable production; +- production routing changes more traffic than intended; +- any required evidence is missing and no operator waiver is recorded; +- rollback target is unknown; +- the candidate needs a secret, credential, or platform dependency that was not + declared before the canary. + +## Stage 3: Production Promotion + +Stage 3 promotes the accepted candidate to the stable production path. + +Promotion may mean: + +- shifting weighted traffic to the canary release; +- replacing the stable Helm release with the accepted candidate; +- changing an ingress selector or service target; +- activating an operator-approved rollout workflow. + +Required Stage 3 prechecks: + +- Stage 2 acceptance gates passed for the same candidate artifact; +- the previous stable version is recorded; +- backup and restore posture is current for stateful workloads; +- migrations are approved and sequenced; +- production-critical workloads have explicit human approval; +- a rollback command and rollback verification check are available. + +Required Stage 3 evidence: + +- promotion command or workflow id; +- previous stable version; +- new stable version; +- production routing state after promotion; +- smoke result after promotion; +- rollback target retained; +- State Hub progress note with non-secret evidence. + +Stage 3 is complete only after the post-promotion smoke passes and the +workload's stable routing points at the promoted candidate. + +## Rollback Expectations + +Rollback is part of every promotion, not an afterthought. + +Every Stage 2 and Stage 3 action must identify one of: + +- previous stable Helm release revision; +- previous image digest and values file; +- previous ingress/routing configuration; +- documented manual recovery path when automation is not yet safe. + +Rollback must be immediate when: + +- production availability is degraded; +- canary traffic escapes the declared blast radius; +- the workload emits repeated authorization or secret-handling errors; +- data integrity is at risk; +- an operator revokes approval during the observation window. + +Rollback may be deferred only when the rollback itself is more dangerous than +the incident state. That decision requires a State Hub note and human approval. + +After rollback, record: + +- triggering symptom; +- rollback action; +- final stable version; +- remaining cleanup; +- whether the failed candidate is blocked, abandoned, or returned to Stage 1. + +## Human Approval Gates + +Human approval is required before production traffic changes for +production-critical workloads. + +Production-critical workloads include: + +- source forge and package registry workloads such as Forgejo or Gitea; +- identity, MFA, SSO, or authorization systems; +- State Hub, Inter-Hub, and operator coordination services; +- databases, object stores, and backup systems; +- ingress, certificate, or cluster-wide policy controllers; +- any workload whose failure blocks multiple repos or domains. + +Approval must be recorded as a non-secret State Hub note or task comment. The +approval record should name: + +- approving operator; +- candidate artifact; +- stage being approved; +- observation window; +- rollback target; +- any waived gates and why. + +Emergency approval can be retrospective only when delaying the action would +increase production risk. Retrospective approval must be recorded immediately +after stabilization. + +## Evidence And Secret Handling + +Lifecycle evidence must be useful without being sensitive. + +Allowed evidence: + +- commit ids; +- image tags and digests; +- workflow ids; +- Kubernetes object names; +- pod status summaries; +- HTTP status codes; +- timestamps; +- State Hub progress ids; +- pass/fail summaries. + +Forbidden evidence: + +- plaintext secrets; +- bearer tokens; +- static API keys; +- kubeconfigs; +- private key material; +- full environment dumps; +- logs that contain credentials or user private data. + +When a check needs secret-backed access, record only the access path and result, +for example: "OpenBao path configured, token exchange returned 200". + +## Forgejo Readiness Interpretation + +This lifecycle is clear enough for Forgejo when a future Forgejo workplan can +answer these questions before production cutover: + +- What source revision and image digest are being promoted? +- What local checks prove the candidate is viable? +- How is the production canary isolated or traffic-limited? +- Which health, registry, SSH, web, Actions, and email recovery checks define + acceptance? +- Who approves the Stage 3 traffic switch? +- What is the previous stable target? +- How is repository data protected before and after promotion? +- How will rollback be verified without losing package or repository state? + +If any answer is missing, Forgejo remains in Stage 1 or Stage 2 preparation and +must not cut over to Stage 3. + +## Minimum Command Contract + +The Railiance CLI makes these lifecycle operations repeatable: + +```text +bin/railiance run # Stage 1 local validation +bin/railiance deploy --stage 2 --plan # Stage 2 canary plan +bin/railiance observe --stage 2 --plan # Stage 2 evidence targets +bin/railiance promote --plan # Stage 3 production promotion +bin/railiance rollback --plan # rollback to previous stable +``` + +The exact command names may change as implementation lands, but the behavior +must preserve the stage gates and evidence requirements in this document. diff --git a/docs/source-import-plan.md b/docs/source-import-plan.md index 8a6cf7b..591047f 100644 --- a/docs/source-import-plan.md +++ b/docs/source-import-plan.md @@ -4,12 +4,22 @@ Record which reviewed source materials are expected to seed this repo. -## First-Wave Imports +## Imported In The Current Wave -Expected source documents from `railiance-cluster`: +Imported source documents from `railiance-cluster`: - `docs/deployment-lifecycle.md` - `docs/app-toml-contract.md` + +Imported source assets from `railiance-cluster`: + +- `schemas/railiance-app.schema.json` +- `examples/railiance/app.toml` + +## Pending First-Wave Imports + +Expected source documents from `railiance-cluster`: + - `docs/overlay-repo-pattern.md` - `docs/canary-helm-template.md` - `docs/stage2-deploy-observe.md` @@ -18,8 +28,6 @@ Expected source documents from `railiance-cluster`: Expected source assets from `railiance-cluster`: -- `schemas/railiance-app.schema.json` -- `examples/railiance/app.toml` - `tools/create_railiance_overlay_repo.sh` - `tools/cmd/railiance-run` - `tools/cmd/railiance-stage2` diff --git a/examples/railiance/app.toml b/examples/railiance/app.toml new file mode 100644 index 0000000..d97f37b --- /dev/null +++ b/examples/railiance/app.toml @@ -0,0 +1,176 @@ +schema_version = "railiance.app.v1" + +[app] +id = "example-service" +name = "Example Service" +repo = "railiance-apps/example-service" +owner = "platform" +criticality = "critical" +description = "Reference declaration for the Railiance staged promotion lifecycle." + +[source] +revision = "git:main" +artifact = "image" +digest_policy = "required" + +[rollback] +strategy = "helm-revision" +command = "bin/railiance rollback example-service" +verification = "GET /health returns 200 on the restored stable release." + +[[platform.dependencies]] +name = "state-hub" +kind = "state-hub" +required = true +stage = "stage2" +evidence = "State Hub /healthz returns ok from the cluster path." + +[[platform.dependencies]] +name = "postgres" +kind = "postgres" +required = true +stage = "stage2" +evidence = "Target database reports Ready and backup posture is current." + +[[secrets.references]] +name = "runtime-api-key" +route = "openbao-api-key" +target = "ExternalSecret/example-service-runtime" +stage = "stage2" +required = true + +[[observability.health_endpoints]] +name = "local-health" +url = "http://127.0.0.1:8080/health" +stage = "stage1" +expected_status = 200 + +[[observability.health_endpoints]] +name = "cluster-health" +url = "http://example-service.example-service.svc.cluster.local:8080/health" +stage = "stage2" +expected_status = 200 + +[[observability.metrics]] +name = "request-errors" +reference = 'promql:rate(http_requests_total{status=~"5.."}[5m])' +stage = "stage2" + +[[observability.logs]] +name = "secret-leak-scan" +reference = "kubectl logs -n example-service deploy/example-service-canary" +stage = "stage2" + +[stages.stage1] +enabled = true +namespace = "local" +release = "example-service-local" +commands = ["make test", "helm template charts/example-service"] +checks = ["unit-tests", "helm-template", "local-health"] +evidence = ["pytest output", "helm template success", "local health 200"] +requires_approval = false + +[stages.stage2] +enabled = true +namespace = "example-service" +release = "example-service-canary" +commands = ["bin/railiance deploy --stage 2 example-service", "bin/railiance observe example-service"] +checks = ["server-dry-run", "canary-ready", "cluster-health", "operator-approval"] +evidence = ["release name", "pod readiness", "health 200", "State Hub progress id"] +requires_approval = true +canary_mode = "isolated" +observation_minutes = 60 + +[stages.stage3] +enabled = true +namespace = "example-service" +release = "example-service" +commands = ["bin/railiance promote example-service", "bin/railiance observe example-service"] +checks = ["stage2-accepted", "rollback-target", "cluster-health", "operator-approval"] +evidence = ["promotion command id", "new stable digest", "post-promotion smoke"] +requires_approval = true +promotion_mode = "release-replace" +previous_stable = "helm:example-service:previous" + +[[checks]] +id = "unit-tests" +type = "command" +stage = "stage1" +description = "Run repository unit tests." +required = true +run = "make test" +timeout_seconds = 600 + +[[checks]] +id = "helm-template" +type = "helm" +stage = "stage1" +description = "Render Helm templates locally." +required = true +chart = "charts/example-service" +values = "values/local.yaml" +mode = "template" + +[[checks]] +id = "local-health" +type = "http" +stage = "stage1" +description = "Confirm local service health." +required = true +url = "http://127.0.0.1:8080/health" +expected_status = 200 +timeout_seconds = 10 + +[[checks]] +id = "server-dry-run" +type = "helm" +stage = "stage2" +description = "Render and submit a server-side dry run before canary." +required = true +chart = "charts/example-service" +values = "values/canary.yaml" +mode = "server-dry-run" + +[[checks]] +id = "canary-ready" +type = "kubernetes" +stage = "stage2" +description = "Canary deployment reaches Available." +required = true +namespace = "example-service" +resource = "deploy/example-service-canary" +condition = "Available" + +[[checks]] +id = "cluster-health" +type = "http" +stage = "stage2" +description = "Cluster health endpoint returns 200." +required = true +url = "http://example-service.example-service.svc.cluster.local:8080/health" +expected_status = 200 +timeout_seconds = 10 + +[[checks]] +id = "operator-approval" +type = "manual" +stage = "stage2" +description = "Human approval is recorded before production-critical traffic changes." +required = true +evidence_required = "State Hub approval note id, candidate digest, rollback target." + +[[checks]] +id = "stage2-accepted" +type = "manual" +stage = "stage3" +description = "Stage 2 gates passed for the same candidate artifact." +required = true +evidence_required = "State Hub Stage 2 acceptance progress id." + +[[checks]] +id = "rollback-target" +type = "manual" +stage = "stage3" +description = "Previous stable release is recorded before promotion." +required = true +evidence_required = "Previous Helm revision or image digest." diff --git a/schemas/railiance-app.schema.json b/schemas/railiance-app.schema.json new file mode 100644 index 0000000..2ff92a5 --- /dev/null +++ b/schemas/railiance-app.schema.json @@ -0,0 +1,596 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://railiance.local/schemas/railiance-app.schema.json", + "title": "Railiance app.toml contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "app", + "source", + "platform", + "secrets", + "observability", + "rollback", + "stages", + "checks" + ], + "properties": { + "schema_version": { + "const": "railiance.app.v1" + }, + "app": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "name", + "repo", + "owner", + "criticality", + "description" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "repo": { + "type": "string", + "minLength": 1 + }, + "owner": { + "type": "string", + "minLength": 1 + }, + "criticality": { + "enum": [ + "low", + "medium", + "high", + "critical" + ] + }, + "description": { + "type": "string", + "minLength": 1 + } + } + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": [ + "revision", + "artifact", + "digest_policy" + ], + "properties": { + "revision": { + "type": "string", + "minLength": 1 + }, + "artifact": { + "enum": [ + "image", + "helm-chart", + "bundle", + "manifest", + "other" + ] + }, + "digest_policy": { + "enum": [ + "required", + "preferred", + "not-applicable" + ] + } + } + }, + "platform": { + "type": "object", + "additionalProperties": false, + "required": [ + "dependencies" + ], + "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/$defs/dependency" + } + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "required": [ + "references" + ], + "properties": { + "references": { + "type": "array", + "items": { + "$ref": "#/$defs/secretReference" + } + } + } + }, + "observability": { + "type": "object", + "additionalProperties": false, + "required": [ + "health_endpoints" + ], + "properties": { + "health_endpoints": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/healthEndpoint" + } + }, + "metrics": { + "type": "array", + "default": [], + "items": { + "$ref": "#/$defs/observationReference" + } + }, + "logs": { + "type": "array", + "default": [], + "items": { + "$ref": "#/$defs/observationReference" + } + } + } + }, + "rollback": { + "type": "object", + "additionalProperties": false, + "required": [ + "strategy", + "command", + "verification" + ], + "properties": { + "strategy": { + "enum": [ + "helm-revision", + "image-digest", + "traffic-shift", + "manual-runbook", + "none" + ] + }, + "command": { + "type": "string", + "minLength": 1 + }, + "verification": { + "type": "string", + "minLength": 1 + } + } + }, + "stages": { + "type": "object", + "additionalProperties": false, + "required": [ + "stage1", + "stage2", + "stage3" + ], + "properties": { + "stage1": { + "$ref": "#/$defs/stage1" + }, + "stage2": { + "$ref": "#/$defs/stage2" + }, + "stage3": { + "$ref": "#/$defs/stage3" + } + } + }, + "checks": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/check" + } + } + }, + "$defs": { + "stageName": { + "enum": [ + "stage1", + "stage2", + "stage3" + ] + }, + "dependency": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "required", + "stage", + "evidence" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "kind": { + "enum": [ + "postgres", + "redis", + "object-store", + "identity", + "state-hub", + "inter-hub", + "network", + "other" + ] + }, + "required": { + "type": "boolean" + }, + "stage": { + "$ref": "#/$defs/stageName" + }, + "evidence": { + "type": "string", + "minLength": 1 + } + } + }, + "secretReference": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "route", + "target", + "stage", + "required" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "route": { + "type": "string", + "minLength": 1 + }, + "target": { + "type": "string", + "minLength": 1 + }, + "stage": { + "$ref": "#/$defs/stageName" + }, + "required": { + "type": "boolean" + } + }, + "not": { + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "token" + ] + }, + { + "required": [ + "password" + ] + }, + { + "required": [ + "secret" + ] + }, + { + "required": [ + "private_key" + ] + }, + { + "required": [ + "kubeconfig" + ] + } + ] + } + }, + "healthEndpoint": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "url", + "stage", + "expected_status" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "minLength": 1 + }, + "stage": { + "$ref": "#/$defs/stageName" + }, + "expected_status": { + "type": "integer", + "minimum": 100, + "maximum": 599 + } + } + }, + "observationReference": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "reference", + "stage" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "reference": { + "type": "string", + "minLength": 1 + }, + "stage": { + "$ref": "#/$defs/stageName" + } + } + }, + "check": { + "type": "object", + "additionalProperties": true, + "required": [ + "id", + "type", + "stage", + "description", + "required" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "type": { + "enum": [ + "command", + "http", + "kubernetes", + "helm", + "metric", + "log", + "manual" + ] + }, + "stage": { + "$ref": "#/$defs/stageName" + }, + "description": { + "type": "string", + "minLength": 1 + }, + "required": { + "type": "boolean" + } + } + }, + "stage1": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "namespace", + "release", + "commands", + "checks", + "evidence", + "requires_approval" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "namespace": { + "type": "string", + "minLength": 1 + }, + "release": { + "type": "string", + "minLength": 1 + }, + "commands": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "checks": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "evidence": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "requires_approval": { + "type": "boolean" + } + } + }, + "stage2": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "namespace", + "release", + "commands", + "checks", + "evidence", + "requires_approval", + "canary_mode", + "observation_minutes" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "namespace": { + "type": "string", + "minLength": 1 + }, + "release": { + "type": "string", + "minLength": 1 + }, + "commands": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "checks": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "evidence": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "requires_approval": { + "type": "boolean" + }, + "canary_mode": { + "enum": [ + "weighted", + "header", + "path", + "shadow", + "isolated" + ] + }, + "observation_minutes": { + "type": "integer", + "minimum": 1 + }, + "traffic_percent": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } + }, + "stage3": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "namespace", + "release", + "commands", + "checks", + "evidence", + "requires_approval", + "promotion_mode", + "previous_stable" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "namespace": { + "type": "string", + "minLength": 1 + }, + "release": { + "type": "string", + "minLength": 1 + }, + "commands": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "checks": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "evidence": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "requires_approval": { + "type": "boolean" + }, + "promotion_mode": { + "enum": [ + "traffic-shift", + "release-replace", + "selector-switch", + "workflow" + ] + }, + "previous_stable": { + "type": "string", + "minLength": 1 + } + } + } + } +} diff --git a/workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md b/workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md index c401528..2c4237a 100644 --- a/workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md +++ b/workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md @@ -73,6 +73,12 @@ Acceptance: import plan. Full import of docs, schema, examples, and helper surfaces is still pending. +2026-07-25: Imported the first core generic contract artifacts from +`railiance-cluster`: `docs/deployment-lifecycle.md`, +`docs/app-toml-contract.md`, `schemas/railiance-app.schema.json`, and +`examples/railiance/app.toml`. Helper command surfaces and the remaining +supporting docs are still pending. + ## T03 - Prepare the compatibility handoff from `railiance-cluster` ```task