Parent runbook: docs/slo/completed/RUNBOOK-hulumi-k8s.md. Read the runbook's Global Execution Rules + Global Entry Rules + M4's lessons before starting.
Goal: After M5, @hulumi/k8s-baseline.GitHubAppCredential ships as a pulumi.ComponentResource provisioning a Secrets Manager container + IAM read policy for a GitHub App's app_id + private-key PEM, plus two user-facing executable scripts in the package tarball: populate-github-app-secret.sh (writes the JSON {app_id, private_key} to the SM secret out-of-band) and mint-github-app-token.sh (signs a JWT with RS256, calls GET /repos/{owner}/{repo}/installation to discover the installation ID, then POST /app/installations/{id}/access_tokens with scoped permissions, prints the 1-hour installation token to stdout for use under BuildKit's --mount=type=secret). Closes #43. The K8s package's atomic v1.0.0 release lands alongside (@hulumi/baseline v1.2.0, @hulumi/policies v1.2.0, @hulumi/drift v1.2.0, @hulumi/k8s-baseline v1.0.0 — same day, same SLSA-L3 attestation chain).
Context: Build-time credential bundles are universal (every team with private deps re-implements them). The mint script is the non-trivial piece: the JWT shape, the installation-ID discovery, the scoped-token POST are all error-prone individually. The decision record commits the design: GitHubAppCredential is the focused-on-GitHub-App component (not over-generalized to "any OAuth credential"); the scripts are user-facing executables shipped in the npm tarball; the component itself just provisions infrastructure.
Important design rule: The component does NOT mint tokens itself. Tokens are minted at build time, inside docker build, via the BuildKit secret-mount pattern. Pulumi-up has no access to the build context; embedding the mint into a Pulumi resource would hide it from CI consumers who script around it. The component provisions and the scripts use — clean separation. The scripts are read-only at runtime: they read from the SM secret (via the IAM principal attached to the IAM read policy the component creates), they never write back. The populate.sh script is run once out-of-band by the consumer (not by Pulumi-up) — Pulumi-up creates an empty SM container and the consumer fills it.
Refactor budget: Surgical addition only. New files. Modifies packages/k8s-baseline/src/index.ts, packages/k8s-baseline/package.json (add scripts/ to the files array so the executables ship; bump version to 1.0.0), docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker, the cookbook index, README, AGENTS.md, why-hulumi.md, getting-started.md, ARCHITECTURE.md, CHANGELOG.md, the cooling-off scripts (no new deps but the four-package atomic release config), .github/workflows/release.yml if needed, and the docs/issue-candidates.md final strike.
| Field | Value |
|---|---|
| Inputs | new GitHubAppCredential(name, args) where args: GitHubAppCredentialArgs requires: repos: string[] (refused if empty; allows ["*"] to mean all installation repos), permissions: Record<string, "read" | "write" | "admin"> (refused if empty), kmsKeyAlias: pulumi.Input<string> (KMS alias to encrypt the SM secret at rest; consumer can use accountFoundation.kmsKeyAliases.secrets). Optional: secretName?: string (defaults to the component instance name + -github-app), iamPrincipalArn?: pulumi.Input<string> (the IAM principal that gets secretsmanager:GetSecretValue — defaults to the Pulumi process's caller identity, but the consumer typically supplies the BuildKit IAM role ARN explicitly). |
| Outputs | GitHubAppCredentialOutputs exposes secretArn: pulumi.Output<string>, iamReadPolicyArn: pulumi.Output<string>, populateScriptPath: string (filesystem path within node_modules/@hulumi/k8s-baseline/scripts/populate-github-app-secret.sh), mintScriptPath: string (path to mint-github-app-token.sh). |
| Interfaces touched | New stable surface: @hulumi/k8s-baseline#{GitHubAppCredential,GitHubAppCredentialArgs,GitHubAppCredentialOutputs,GITHUB_APP_CREDENTIAL_COMPONENT_TYPE}. Component type: "hulumi:k8s:GitHubAppCredential". Two new shipped script artifacts at packages/k8s-baseline/scripts/{populate-github-app-secret.sh,mint-github-app-token.sh}. |
| Data classification | Restricted — the milestone's load-bearing data flow is GitHub App private keys (RS256 signing material) and short-lived installation tokens. SM secret value transits Pulumi state ONLY at the consumer's choice (the typical flow is: pulumi up creates the empty container, the consumer runs populate.sh out-of-band — the PEM never touches Pulumi state). The mint script holds the PEM in memory only for the duration of the JWT signing call. Compliance frontmatter: compliance: [soc2, asvs] for any consumer importing this component. |
| Proactive controls in play | (a) C1 Define Security Requirements — design record + the issue (#43) record the requirement: GitHub App credential bundle with shipped scripts. (b) C2 / C8 Data protection — KMS key alias is required (no default) so the consumer's KMS choice is explicit; the SM secret is at-rest encrypted with the consumer's CMK; the IAM read policy is scoped to the single ARN, never *. (c) C5 Validate All Inputs — repos/permissions non-empty; kmsKeyAlias non-empty; secretName validated like the M4 K8s name shape. (d) C7 Authorization — IAM read policy is scoped to one ARN, one principal. (e) C9 Implement Security Logging and Monitoring — populate.sh writes a structured log line security_event.github_app_secret_populated (no value bytes) to stderr. (f) C10 Handle All Errors and Exceptions — the scripts use set -euo pipefail and trap to scrub the PEM from temp files on exit; component refuses construction with clear errors. |
| Abuse acceptance scenarios | Five BDD rows in the table below cite tm-hulumi-k8s-abuse-N. Slug-keyed: tm-hulumi-k8s-abuse-iam-policy-wildcard (refuses if the consumer's secretName would resolve to a wildcard ARN), tm-hulumi-k8s-abuse-mint-token-leaks-pem (asserts mint.sh does not echo the PEM under any error path), tm-hulumi-k8s-abuse-script-not-shipped (asserts the scripts are present in the published tarball — caught by the npm pack && tar tf smoke), tm-hulumi-k8s-abuse-shellinjection-in-secretname (secretName is interpolated into AWS CLI calls in populate.sh; refuses metacharacters), tm-hulumi-k8s-abuse-token-exfil-via-stderr (asserts mint.sh writes the token to stdout only, never stderr — even on partial failure). |
| Files allowed to change | New: packages/k8s-baseline/src/{github-app-credential.ts,github-app-credential.args.ts,github-app-credential.outputs.ts}; packages/k8s-baseline/scripts/{populate-github-app-secret.sh,mint-github-app-token.sh}; packages/k8s-baseline/tests/github-app-credential.test.ts; packages/k8s-baseline/tests/scripts/{populate.test.ts,mint.test.ts}; packages/k8s-baseline/tests/integration/release-tarball.test.ts (asserts npm pack includes the scripts); examples/k8s-helm-smoke/ (NEW — minimal Pulumi program importing HardenedHelmRelease); examples/k8s-mesh-bootstrap-smoke/ (NEW — full M2+M3+M4 bootstrap example); docs/cookbooks/k8s-helm-release-rename.md (NEW — migration); docs/cookbooks/eks-meshed-workload-bootstrap.md (NEW — full bootstrap); docs/cookbooks/github-app-private-deps-build.md (NEW — build-credential cookbook); docs/components/github-app-credential.md (NEW — full reference, since M5 is the launch milestone — also fill in the other K8s component reference docs to full reference); docs/slo/lessons/hulumi-k8s-m5.md; docs/slo/completion/hulumi-k8s-m5.md. Modified: packages/k8s-baseline/src/index.ts (re-export); packages/k8s-baseline/package.json (add scripts/ to files, bump version to 1.0.0, add chmod +x script for the bash files); docs/cookbooks/README.md (index 3 new cookbooks); docs/cookbooks/psa-baseline-istio-sidecar.md (replace hand-rolled snippets with IstioFoundation examples); README.md, AGENTS.md, docs/why-hulumi.md, docs/getting-started.md, docs/ARCHITECTURE.md, CHANGELOG.md; docs/components/{hardened-helm-release,eks-subnet-tagger,istio-foundation,alb-meshed-http-entrypoint,kubernetes-secret-from-asm,rds-credential-secret}.md (full reference docs); docs/slo/completed/RUNBOOK-hulumi-k8s.md Milestone Tracker + Doc Update Table fill-in; docs/issue-candidates.md (strike #43, sync with v1.2 release); .github/workflows/weekly-integration.yml (add kind matrix entry); .github/workflows/release.yml (atomic four-package release matrix); pnpm-workspace.yaml if needed for examples/*; .gitignore if needed. Files outside this milestone's allow-list — REFUSE TO TOUCH including any AWS / GitHub source under packages/baseline/src/{aws,github}/, any packages/policies/src/, any packages/drift/src/, M1-M4 K8s component source files (other than index.ts). |
| Files to read before changing anything | docs/slo/completed/RUNBOOK-hulumi-k8s.md; docs/slo/design/hulumi-k8s-surface.md (§ Decision: GitHubAppCredential); GitHub Apps REST API docs (/repos/{owner}/{repo}/installation, /app/installations/{id}/access_tokens); the existing release workflow (.github/workflows/release.yml); docs/cookbooks/verify-provenance.md (the SLSA-L3 verify pattern that extends to the new package). |
| New files allowed | All "New" entries above. |
| New dependencies allowed | none runtime. Test-only: js-yaml if the cookbook needs to assert YAML examples render — decide during execution; default no. |
| Migration allowed | no for the new K8s package. The Helm release-rename cookbook walks consumers through migrating their existing suffixed releases via pulumi replace; this is consumer-side migration documented in a cookbook, not a Hulumi-side migration. |
| Compatibility commitments | GitHubAppCredential + args + outputs + type constant — stable from M5. The script names populate-github-app-secret.sh and mint-github-app-token.sh are stable; renaming requires consumer code changes. The four packages release atomically at v1.2.0/v1.0.0; future K8s-package patches can release independently of AWS/GitHub patches. |
| Forbidden shortcuts | (a) NEVER mint tokens inside the Pulumi component. The component provisions; the scripts use. The split is load-bearing. (b) NEVER scope the IAM read policy to Resource: "*" — always to the single SM secret ARN. (c) NEVER echo the PEM or the minted token to stderr. Only stdout for the token; stderr is for security-event lines that contain no secret material. (d) NEVER ship populate.sh or mint.sh without set -euo pipefail and a trap that scrubs any temp files containing the PEM. (e) NEVER add the scripts to a bin/ field in package.json — they are not Hulumi-process commands; they are user-side artifacts the consumer invokes from CI. (f) NEVER publish the K8s package without the SLSA-L3 attestation pipeline that the existing three packages use. (g) NEVER ship the v1.2.0 release without the cooling-off CI gate green. (h) NEVER ship a default-no-KMS posture; kmsKeyAlias is required. |
- No GHCR / npm-private / Docker-Hub credential variants. GitHub App only at v1 (Rule 0). The cookbook cross-references future variants but ships none.
- No automatic token rotation. Tokens are 1-hour by GitHub design; the consumer's CI re-mints per build.
- No automatic PEM rotation. The consumer rotates the PEM via
populate.shre-run. - No alternative KMS key managers (Cloud HSM, external KMS). AWS KMS only.
- No GitHub Enterprise Server (on-prem) endpoint; GHEC only — the API endpoints differ; revisit on demand.
- No Pulumi-managed installation discovery (the mint script does this; embedding it Pulumi-side hides it from CI).
- Complete the Global Entry Rules.
- Read M4 lessons.
- Read GitHub's Apps REST API docs end-to-end. The JWT claims (
iat,exp,iss), the installation discovery, the scoped POST — each is small but order-of-operations matters. - Copy the Evidence Log.
- Re-state the load-bearing constraints: (i) component provisions; scripts use; (ii) IAM scoped to one ARN; (iii) PEM never on stderr; (iv) scripts are shipped via
files: ["scripts/"]inpackage.json— verify the npm tarball contains them; (v) atomic four-package v1.2.0/v1.0.0 release. - Verify
npm pack --dry-runagainst the K8s package preview includesscripts/files (smoke before write).
Listed comprehensively above in the Contract Block. The most important ones:
| File | Planned Change |
|---|---|
packages/k8s-baseline/src/github-app-credential.ts |
NEW: ComponentResource provisioning SM container + IAM read policy |
packages/k8s-baseline/scripts/populate-github-app-secret.sh |
NEW: bash script (set -euo pipefail, trap) writing JSON to SM |
packages/k8s-baseline/scripts/mint-github-app-token.sh |
NEW: bash script (JWT RS256, installation discovery, scoped POST, stdout-only token) |
packages/k8s-baseline/tests/scripts/{populate,mint}.test.ts |
NEW: tests asserting set -euo pipefail, no PEM-on-stderr, etc. |
packages/k8s-baseline/tests/integration/release-tarball.test.ts |
NEW: npm pack && tar tf includes the two scripts under package/scripts/ |
examples/k8s-helm-smoke/ + examples/k8s-mesh-bootstrap-smoke/ |
NEW examples |
| 3 NEW cookbooks | release-rename + bootstrap + github-app-private-deps |
| 6 component reference docs | full references for all M1–M5 K8s components |
packages/k8s-baseline/package.json |
bump to 1.0.0; add scripts/ to files; add chmod +x postinstall |
.github/workflows/release.yml |
extend the four-package atomic release matrix |
.github/workflows/weekly-integration.yml |
add kind matrix entry exercising M1–M4 components |
- Implement
populate-github-app-secret.shandmint-github-app-token.shas stand-alone bash scripts. Test them in isolation with thetests/scripts/test files (usingbash -nsyntax check +shellcheckif available + happy-path with mocked AWS CLI + abuse rows). - Write
packages/k8s-baseline/tests/github-app-credential.test.tsBDD: happy path (component provisions one SM secret + one IAM policy with single-ARN resource), missing repos refused, missing permissions refused, missing kmsKeyAlias refused, secretName traversal refused. Run — fail for module-not-found. - Implement
GitHubAppCredentialArgs+GitHubAppCredentialOutputs+GitHubAppCredential. The constructor: validates inputs; createsaws.secretsmanager.SecretwithkmsKeyId: kmsKeyAlias; createsaws.iam.PolicywithResource: <secret-arn>andAction: secretsmanager:GetSecretValue; ifiamPrincipalArnis supplied, attaches viaaws.iam.PolicyAttachment; computespopulateScriptPathandmintScriptPathfromrequire.resolve("@hulumi/k8s-baseline/scripts/populate-github-app-secret.sh")(the package ships them). - Wire re-export. Run
pnpm --filter @hulumi/k8s-baseline build && test && typecheck && lint— green. - Write
release-tarball.test.ts: runsnpm pack --pack-destination /tmpfrompackages/k8s-baseline/, thentar tf /tmp/hulumi-k8s-baseline-1.0.0.tgzshould includepackage/scripts/populate-github-app-secret.shandpackage/scripts/mint-github-app-token.sh. - Write the three cookbooks. Update the cookbook index.
- Write the 6 full component reference docs. Update
docs/components/README.md. - Update
README.md,AGENTS.md,docs/why-hulumi.md,docs/getting-started.md,docs/ARCHITECTURE.md,CHANGELOG.md(v1.2.0 entry). - Write
examples/k8s-helm-smoke/+examples/k8s-mesh-bootstrap-smoke/. Each is a runnableindex.ts+package.jsonmirroring the existingexamples/account-foundation-smoke/shape. - Extend
.github/workflows/weekly-integration.ymlwith a kind matrix entry. Extend.github/workflows/release.ymlto release four packages atomically with provenance attestations on all four. - Run the full repo regression sweep + tarball test + cookbook lint. Update Tracker, Doc Update Table fill-in, lessons, completion. Strike #43 in
docs/issue-candidates.md.
Feature: GitHubAppCredential provisions an SM secret + scoped IAM read policy; the shipped scripts populate / mint with redaction discipline; the npm tarball includes the scripts
| Scenario | Category | Given | When | Then | Threat-model row | Control |
|---|---|---|---|---|---|---|
| Happy path — component | happy path | mock-runtime; repos: ["myorg/private-libs"], permissions: { contents: "read" }, kmsKeyAlias: "alias/secrets" |
construction | one aws.secretsmanager.Secret registered with kmsKeyId: "alias/secrets"; one aws.iam.Policy with Statement[0].Resource = the secret ARN, Action: secretsmanager:GetSecretValue; outputs include secretArn, iamReadPolicyArn, populateScriptPath, mintScriptPath |
n/a | n/a |
Happy path — iamPrincipalArn supplied |
happy path | mock-runtime; valid args + iamPrincipalArn: "arn:aws:iam::123:role/buildkit" |
construction | one aws.iam.PolicyAttachment registered linking the policy to the principal |
n/a | C7 |
Invalid input — empty repos |
invalid input | mock-runtime; repos: [] |
construction | constructor throws Error('GitHubAppCredential: repos must be non-empty') |
n/a | C5 |
Invalid input — empty permissions |
invalid input | mock-runtime; permissions: {} |
construction | constructor throws | n/a | C5 |
Invalid input — missing kmsKeyAlias |
invalid input | mock-runtime; args without kmsKeyAlias |
construction | constructor throws — KMS is required, no default | n/a | C5 + Forbidden shortcut (h) |
Invalid input — secretName traversal |
invalid input | mock-runtime; secretName: "../etc/secret" |
construction | constructor throws | tm-hulumi-k8s-abuse-shellinjection-in-secretname |
C5 |
Empty state — repos: ["*"] allowed |
empty state | mock-runtime; repos: ["*"] |
construction | construction succeeds; no special handling required Hulumi-side (the * means "all installation repos" to GitHub's API — interpreted by the mint script, not by the component) |
n/a | n/a |
| Dependency failure — KMS alias does not exist | partial failure | mock-runtime; kmsKeyAlias: "alias/does-not-exist" |
pulumi up against real AWS |
mock-runtime test passes; real pulumi up fails with KMS not-found error from AWS — the component does NOT pre-validate (consumer's responsibility) |
n/a | C10 |
| Abuse case — IAM policy resource is single ARN, not wildcard | abuse case | mock-runtime; valid args | construction | the rendered IAM policy has Statement[0].Resource = the exact secret ARN (string match against the SM resource's ARN output); Statement[0].Resource is NOT "*" |
tm-hulumi-k8s-abuse-iam-policy-wildcard |
Forbidden shortcut (b) |
Abuse case — populate.sh is set -euo pipefail + trap-scrubs |
abuse case | filesystem | bash -n + grep |
first line after shebang is set -euo pipefail; trap line present that scrubs any temp file matching *.pem on EXIT |
n/a | Forbidden shortcut (d) |
Abuse case — mint.sh token-to-stdout-only |
abuse case | filesystem + smoke run with stubbed AWS CLI | run mint.sh against a stub |
the minted token (the ghs_xxx string) appears on stdout exactly once; appears on stderr ZERO times even on partial failure (e.g., AWS CLI throttle); error paths print security_event.mint_failed to stderr (no token bytes) |
tm-hulumi-k8s-abuse-token-exfil-via-stderr |
Forbidden shortcut (c) |
Abuse case — mint.sh does not echo PEM |
abuse case | filesystem grep | static check | no cat or echo of $PEM (or any var holding the PEM) anywhere in the script; the PEM is piped into openssl dgst via stdin, never to stdout/stderr |
tm-hulumi-k8s-abuse-mint-token-leaks-pem |
Forbidden shortcut (c) |
| Abuse case — scripts shipped in npm tarball | abuse case | local | npm pack --pack-destination /tmp then tar tf |
the tarball includes package/scripts/populate-github-app-secret.sh and package/scripts/mint-github-app-token.sh |
tm-hulumi-k8s-abuse-script-not-shipped |
package.json files discipline |
| Compatibility — atomic four-package release | schema / compatibility | release tag v1.2.0 exists |
release workflow runs | four npm packages publish (@hulumi/baseline@1.2.0, @hulumi/policies@1.2.0, @hulumi/drift@1.2.0, @hulumi/k8s-baseline@1.0.0); each with SLSA-L3 attestation; npm view <pkg>@1.2.0 dist-tags reports latest |
n/a | release-pipeline lock |
- All M1–M4 K8s BDD scenarios continue to pass.
- All AWS + GitHub regression suites continue to pass.
pnpm run lint:license-boundarycontinues to pass on the existing surface.pnpm run lint:exact-pin-guardcontinues to pass.gh attestation verify --repo kerberosmansour/hulumi <tarball>for each of the four packages.
-
GitHubAppCredential+ args + outputs + type constant exported. -
populate-github-app-secret.shandmint-github-app-token.shshipped in the tarball (asserted by the integration test). -
pnpm install --frozen-lockfile && pnpm -r build && pnpm -r test && pnpm -r typecheck && pnpm -r lint && pnpm run lint:license-boundary && pnpm run lint:exact-pin-guardgreen. - License header on every new source file.
- Bash scripts begin with
set -euo pipefailand a PEM-scrubbing trap. - All 6 K8s component reference docs filled in to full reference.
-
CHANGELOG.mdv1.2.0 entry covers all four packages. - Atomic four-package release workflow tested in dry-run.
- Cookbook index includes the three new cookbooks.
| E2E Test | What It Proves | Pass Criteria |
| ------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| provisions_sm_secret_and_scoped_policy_against_kind_or_localstack | Component provisions correctly | Test creates the component; assertions on rendered SM resource + IAM policy match the BDD happy-path row |
| populate_script_writes_json_to_sm | Populate script works end-to-end with mocked AWS CLI | Run populate.sh <APP_ID> <PEM_PATH> against the mock; aws secretsmanager get-secret-value returns {"app_id":..., "private_key":...} |
| mint_script_emits_one_hour_token_to_stdout | Mint script works end-to-end with stubbed GitHub API | Run mint.sh against stubbed gh api calls; stdout matches ghs_[A-Za-z0-9_]+; stderr contains the security-event line; no PEM bytes in any output |
| release_tarball_contains_scripts | The shipping discipline holds | npm pack && tar tf | grep scripts/ returns 2 paths |
| four_package_atomic_release_dry_run | The atomic release workflow doesn't accidentally publish 3-of-4 | act -j release-dry-run (or workflow-side dry-run gate) reports all 4 packages would publish in lockstep |
- Full sweep green.
- In a fresh checkout:
npm packfrompackages/k8s-baseline/produces a tarball;tar tfincludes the two scripts. -
bash -n packages/k8s-baseline/scripts/*.shsyntax-check passes. -
git statusclean.
| Step | Command / Check | Expected Result | Actual Result | Pass/Fail | Notes |
|---|---|---|---|---|---|
| Baseline | pnpm -r build && pnpm -r test |
green pre-M5 | filled during execution | pending | |
| Bash scripts written | bash -n packages/k8s-baseline/scripts/*.sh |
OK | filled during execution | pending | |
| Component impl | filesystem | source files present + re-exported | filled during execution | pending | |
| Mock-runtime BDD | pnpm --filter @hulumi/k8s-baseline test |
all rows pass | filled during execution | pending | |
| Tarball test | npm pack --dry-run from packages/k8s-baseline/ |
includes scripts/ |
filled during execution | pending | |
| Examples | pnpm install in each examples/k8s-* dir |
OK | filled during execution | pending | |
| Cookbooks | filesystem + prettier | 3 cookbooks present, formatted | filled during execution | pending | |
| Reference docs | filesystem | 6 component reference docs filled | filled during execution | pending | |
| Changelog | grep v1.2.0 in CHANGELOG.md |
entry present | filled during execution | pending | |
| Release workflow | act -j release-dry-run or equivalent |
dry-run green | filled during execution | pending | |
| Build / typecheck / lint | pnpm -r build && pnpm -r typecheck && pnpm -r lint |
green | filled during execution | pending | |
| Smoke | full sweep | green | filled during execution | pending |
- All M1–M5 BDD scenarios pass.
- Tarball integration test green.
- All cookbooks + reference docs + examples present.
- Atomic four-package release workflow dry-run green.
pnpm -r test && typecheck && lint && lint:license-boundary && lint:exact-pin-guardgreen.git statusclean.- All AWS + GitHub regression suites pass.
docs/slo/lessons/hulumi-k8s-m5.md+completion/hulumi-k8s-m5.mdwritten.- Milestone Tracker →
done. docs/issue-candidates.mdstrikes #43.- README, AGENTS.md, getting-started.md, why-hulumi.md updated to surface the K8s variant.
- Tracker M5 →
done; entire runbook complete. - Doc Update Table — final fill-in.
docs/issue-candidates.mdstrikes #43; the K8s table marks all 8 issues shipped.CHANGELOG.mdv1.2.0 entry covers all four packages.- After tag is cut, re-verify SLSA-L3 attestations on all four published tarballs via
npm run release:verify-attestations.
- The component-provisions / scripts-use split is the load-bearing decision; record in lessons.
- The atomic four-package release is the launch event; if it splits (3-of-4 publish), users get inconsistent versions and the cooling-off + cross-package contracts break. The dry-run gate is the prevention.
- Future K8s-package patches can release independently of AWS/GitHub patches once the v1.0.0 baseline is established.