Updated 2026-05-01 per Flaw 2 (production-only tag is wrong). M1 now ships two components together:
Ec2PatchBaseline(the per-wave primitive) andEc2PatchWaves(the wave-composer). They share ~90% of implementation; landing them together in one milestone keeps the runbook at 5 milestones total. The wave model adds dev → staging → production sequencing with a CloudWatch composite-alarm health gate between waves — no Lambda.
Parent runbook: docs/slo/completed/RUNBOOK-hulumi-operations.md. Read the runbook's Carmack-Style Best Practices, the Global Execution Rules (especially Rule 0 — the Hulumi-Operations scope contract, Rule 8 — tier defaults must encode the breach risk, Rule 10 — MonitoringFoundation is the only SNS topic this runbook touches, and Rules 11–14 — the v4 assertions/bounds/static-analysis/debugger discipline) + the Global Entry Rules before starting.
Goal: After M1, two shippable components exist:
@hulumi/baseline.aws.Ec2PatchBaseline— wrapsaws.ssm.PatchBaseline+aws.ssm.PatchGroup+aws.ssm.MaintenanceWindow+aws.ssm.MaintenanceWindowTarget+aws.ssm.MaintenanceWindowTask+aws.ssm.ResourceDataSync+ the IAM service role into a singlepulumi.ComponentResourcewith tier-aware defaults. Tag value enum:Patch:Group ∈ {dev, staging, production}(tightened from free-form per Flaw 2). CRC32-bucket staggering, compliance metric routed to a consumer-suppliedMonitoringFoundationSNS topic ARN.@hulumi/baseline.aws.Ec2PatchWaves— composes up to threeEc2PatchBaselineinstances (one per wave: dev → staging → production) with sequencedMaintenanceWindowschedules and a CloudWatch composite-alarm health gate between waves. The gate is aaws.cloudwatch.CompositeAlarmwhoseOKstate wires via PulumiOutput<bool>chain into the next wave'sMaintenanceWindow.enabledfield. No Lambda. No Step Functions. Sandbox tier degrades to single-wave (dev only); StartupHardened tier requires all three waves.
No new SNS topics. No Hulumi-authored Lambda. The wedge of the runbook — once M1 lands, a Hulumi consumer can pulumi up an account-level wave-based patch posture in 30 lines of TypeScript.
Context: The design record docs/slo/design/hulumi-for-operations.md commits the API shape (§ Decision: Ec2PatchBaseline shape — every line of the proposed args block in that doc is contract); the RebootOption tier-default decision (§ Decision: RebootOption default per tier — both tiers default to RebootIfNeeded); the staggering decision (§ Decision: synchronized-reboot mitigation — CRC32 mod bucketCount, fail-loud at StartupHardened). The threat model docs/slo/design/hulumi-for-operations-threat-model.md commits the seven STRIDE-derived abuse-case rows for this surface. M1 ships that exact API surface — no more, no less. The component mirrors packages/baseline/src/aws/secure-bucket.ts's pulumi.ComponentResource discipline (child registration via { parent: this }; tag triple hulumi:component/hulumi:tier/hulumi:controls emitted on resources where it makes sense; assertValidTier() reused unchanged).
Carmack-style reliability goal: Strengthen Rule 5 — make invalid states unrepresentable (the RebootOption discriminated union is the showcase of v4 in this runbook) and Rule 4 — bounded resources (the staggering.bucketCount upper limit + the complianceMetric.severities length cap are the showcase). Plus Rule 1 — debugger over guessing for the integration test: when the SSM Patch Baseline doesn't apply on the sandbox EC2, aws ssm describe-patch-baselines is the inspection command — not log-grep.
Important design rule: Ec2PatchBaseline's RebootOption default is RebootIfNeeded at BOTH tiers. The NoReboot option is available only when the consumer writes rebootOption: { kind: "NoReboot", hulumi_decision_comment: "..." } — a discriminated union that requires an explanatory comment in code. This is the load-bearing decision M1 ships. The "Sandbox tier defaulting to NoReboot to avoid surprising tenants" framing is explicitly rejected because the silent-un-patching trap (sixty-day-old kernel exploited in the wild) is a worse failure than a 04:00 UTC reboot. See hulumi-for-operations-threat-model.md § Top risks — Breach and abuse-case row tm-hulumi-ops-abuse-noreboot-without-decision.
Refactor budget: Surgical addition only. New files under packages/baseline/src/aws/ec2-patch-baseline.{ts,args.ts,outputs.ts}, packages/baseline/src/aws/ec2-patch-waves.{ts,args.ts,outputs.ts} and tests for both: packages/baseline/tests/aws/ec2-patch-baseline.test.ts, packages/baseline/tests/aws/ec2-patch-waves.test.ts plus real-AWS sandbox integration tests under packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts and ec2-patch-waves.aws-ops.test.ts. Existing files modified: packages/baseline/src/aws/index.ts (re-export both), docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker, docs/ARCHITECTURE.md (one paragraph addition), docs/components/ec2-patch-baseline.md + docs/components/ec2-patch-waves.md (one-line stubs). No changes to any existing packages/baseline/src/aws/{secure-bucket,account-foundation,monitoring-foundation,identity-alarms}.ts, no changes to packages/policies/, packages/drift/, packages/k8s-baseline/, or any existing test.
| Field | Value |
|---|---|
| Inputs | Ec2PatchBaseline: new Ec2PatchBaseline(name, args) where args: Ec2PatchBaselineArgs requires: tier: Tier (Sandbox | StartupHardened); patchGroupTagValue: "dev" | "staging" | "production" (enum per Flaw 2 — refused if any other value); operatingSystem: "AMAZON_LINUX_2023" | "UBUNTU" | "WINDOWS" | "AMAZON_LINUX_2" | "REDHAT_ENTERPRISE_LINUX"; approvalRules, maintenanceWindow, complianceMetric per the prior shape. Optional staggering — required at StartupHardened. Ec2PatchWaves: new Ec2PatchWaves(name, args) where args: Ec2PatchWavesArgs requires: tier: Tier; waves: { dev?: WaveArgs; staging?: WaveArgs; production?: WaveArgs } where WaveArgs is Ec2PatchBaselineArgs minus tier (inherited) and patchGroupTagValue (set by the wave key); complianceMetric: { snsTopicArn; severityThreshold } shared across all waves; optional waveHealthGate: { appHealthAlarmArns: pulumi.Input<string>[]; onAlarmFireDisableNextWave: boolean }. Tier-degradation: at Sandbox, only waves.dev is consumed; waves.staging / waves.production are accepted but produce no resources. At StartupHardened, all three keys must be present and non-empty. |
| Outputs | Ec2PatchBaselineOutputs per prior shape. Ec2PatchWavesOutputs exposes: waves: { dev?: Ec2PatchBaselineOutputs; staging?: Ec2PatchBaselineOutputs; production?: Ec2PatchBaselineOutputs }, compositeAlarmArns: pulumi.Output<{ devToStaging?: string; stagingToProduction?: string }> (only present at StartupHardened with full wave set), wavesEnabled: pulumi.Output<{ dev: boolean; staging: boolean; production: boolean }> (echoes the MaintenanceWindow.enabled resolution after the gate evaluates). |
| Interfaces touched | New stable surface: @hulumi/baseline.aws#{Ec2PatchBaseline,Ec2PatchBaselineArgs,Ec2PatchBaselineOutputs,EC2_PATCH_BASELINE_COMPONENT_TYPE,Ec2PatchWaves,Ec2PatchWavesArgs,Ec2PatchWavesOutputs,EC2_PATCH_WAVES_COMPONENT_TYPE}. Component types: "hulumi:aws:Ec2PatchBaseline", "hulumi:aws:Ec2PatchWaves". Both re-exported through the package's public API. The package's published version stays at 1.1.x; v1.2.0 ships in M5. |
| Data classification | Internal. The milestone provisions account-level Patch Manager configuration in a sandbox AWS account during integration tests. No PII, no customer data. Patch baselines reference patch IDs and AWS-managed-baseline names — neither carries Confidential data. The complianceMetric.snsTopicArn is a reference to a topic the consumer owns; Hulumi never reads message contents. |
| Proactive controls in play | (a) C1 Define Security Requirements — the design record IS the security-requirements record; M1 implements every "Decision" line for Ec2PatchBaseline. (b) C5 Validate All Inputs — every args field is type-narrowed; cron(...) schedule regex-validated at construction; rebootOption discriminated union refuses bare strings. (c) @hulumi/baseline.aws.SecureBucket (existing precedent) — Ec2PatchBaseline mirrors its ComponentResource discipline. (d) C9 Implement Security Logging and Monitoring — complianceMetric routes patch-compliance failures to MonitoringFoundation SNS, the load-bearing routing decision. (e) C10 Handle All Errors and Exceptions — components throw plain Error for input violations (matches existing AWS pattern); no detail-leakage paths. (f) @hulumi/baseline.aws.MonitoringFoundation (existing precedent) — Ec2PatchBaseline's complianceMetric.snsTopicArn is wired exactly as IdentityAlarms wires to MonitoringFoundation. |
| Abuse acceptance scenarios | Six BDD rows in the table below cite tm-hulumi-ops-abuse-N from the threat model. Slug-keyed: tm-hulumi-ops-abuse-noreboot-without-decision (NoReboot requires hulumi_decision_comment — bare { kind: "NoReboot" } refused), tm-hulumi-ops-abuse-stagger-fail-loud (StartupHardened without staggering arg refused at construction), tm-hulumi-ops-abuse-baseline-tamper (CloudTrail captures ssm:UpdatePatchBaseline — covered by existing MonitoringFoundation wiring; M1 documents that the wiring exists, no new code), tm-hulumi-ops-abuse-cw-log-delivery-alarm (M1 ships a MetricAlarm on LogDelivery.Errors for the patch-compliance metric filter), tm-hulumi-ops-abuse-no-runcmd-secrets (M1 uses only AWS-managed AWS-RunPatchBaseline SSM document — no Hulumi-authored Run Command document is allowed in the codebase), tm-hulumi-ops-abuse-runaway-window (advisory CrossGuard rule lands in M4 as O_PATCH_3; M1 does NOT enforce — but the test asserts that a window scheduled cron(*/1 * * * ? *) constructs successfully so the M4 rule has data to flag), tm-hulumi-ops-abuse-service-role-least-priv (Maintenance Window service role policy is hard-coded to the minimum: ssm:SendCommand + ssm:GetCommandInvocation + ec2:DescribeInstances only). Added 2026-05-01 per Flaw 2: tm-hulumi-ops-abuse-tag-outside-enum (patchGroupTagValue outside {dev, staging, production} refused at construction) and tm-hulumi-ops-abuse-skip-wave-gate (mid-incident manual flip of a wave's MaintenanceWindow.enabled is observable via CloudTrail routed through MonitoringFoundation.high — Hulumi documents the override path, does not block it). |
| Files allowed to change | NEW: packages/baseline/src/aws/ec2-patch-baseline.ts, packages/baseline/src/aws/ec2-patch-baseline.args.ts, packages/baseline/src/aws/ec2-patch-baseline.outputs.ts, packages/baseline/tests/aws/ec2-patch-baseline.test.ts, packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts, docs/components/ec2-patch-baseline.md (one-line stub), docs/slo/lessons/hulumi-operations-m1.md, docs/slo/completion/hulumi-operations-m1.md. MODIFIED: packages/baseline/src/aws/index.ts (one re-export line), docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker, docs/slo/runbook-milestones/hulumi-operations-m1.md (this file — Evidence Log only, in execution), docs/ARCHITECTURE.md (one-paragraph addition under "AWS account-level baseline" section), .gitignore IF needed (Pulumi checkpoints under tests/integration/aws-ops/). Files outside this milestone's allow-list — REFUSE TO TOUCH: any packages/baseline/src/{secure-bucket,account-foundation,monitoring-foundation,identity-alarms,detective-services-enable,audit-trail}.{ts,args.ts,outputs.ts} (M2/M3 own those — adding detective-services-enable here would couple M1 to M2's open question); any packages/policies/, packages/drift/, packages/k8s-baseline/; any existing skills/; any existing examples/; any package.json at repo root. |
| Files to read before changing anything | docs/slo/completed/RUNBOOK-hulumi-operations.md (Global Execution Rules + this milestone in full); docs/slo/design/hulumi-for-operations.md (entire doc, especially § Decision blocks); docs/slo/design/hulumi-for-operations-threat-model.md (entire doc, especially the Ec2PatchBaseline STRIDE rows); packages/baseline/src/aws/secure-bucket.ts (pattern precedent); packages/baseline/src/aws/secure-bucket.args.ts; packages/baseline/src/aws/secure-bucket.outputs.ts; packages/baseline/src/aws/monitoring-foundation.ts (the SNS-topic-output pattern Ec2PatchBaseline consumes); packages/baseline/src/aws/identity-alarms.ts (the metric-filter → alarm → SNS pattern Ec2PatchBaseline mirrors); packages/baseline/src/aws/tier.ts (the Tier enum and assertValidTier helper); packages/baseline/tests/aws/secure-bucket.test.ts (test-shape precedent); packages/baseline/tests/aws/identity-alarms.test.ts (closest precedent for testing metric-filter wiring); packages/baseline/package.json (peer-dep + dev-dep style); scripts/exact-pin-guard.mjs (verify no-op for this milestone — the runbook adds zero new @pulumi/* deps). |
| New files allowed | All "NEW" entries in Files allowed to change. |
| New dependencies allowed | none. Every aws.ssm.* resource used is already in @pulumi/aws (a peer dep declared by @hulumi/baseline). No @aws-sdk/* runtime imports — Ec2PatchBaseline is pure declarative IaC. |
| Migration allowed | no — additive only. New component; no migration of existing code. |
| Compatibility commitments | Ec2PatchBaseline, Ec2PatchBaselineArgs, Ec2PatchBaselineOutputs, EC2_PATCH_BASELINE_COMPONENT_TYPE are stable from M1 (no rename in v1.x). The RebootOption discriminated union shape ({ kind: "RebootIfNeeded" } | { kind: "NoReboot", hulumi_decision_comment: string }) is load-bearing and irreversible: changing it to a bare string would invert the breach-risk decision the design record commits to. Existing AWS / GitHub / K8s interfaces from Hulumi v1.x unchanged. The Tier enum, MonitoringFoundation.high.arn output, assertValidTier are all consumed unchanged. |
| Resource bounds introduced/changed (v4 Rule 4) | (a) staggering.bucketCount: expected 1–10, hard limit 10, behavior at limit: refuse construction with Error("Ec2PatchBaseline.staggering.bucketCount: max 10"). Test row: bucketCount = 11 rejected. (b) approvalRules.severities length: expected 1–4, hard limit 4 (Critical, Important, Medium, Low), behavior: refuse duplicate / unknown values. Test row: ["Critical", "Critical"] rejected; ["Trivial"] rejected. (c) maintenanceWindow.durationHours × cutoffHours: AWS-side limits enforced (cutoff < duration); Hulumi adds a runtime check cutoffHours < durationHours and refuses construction otherwise. Test row: durationHours: 1, cutoffHours: 2 rejected. (d) The component creates exactly bucketCount + 5 AWS resources (1 Patch Baseline + 1 Patch Group + 1 Maintenance Window + 1 Service Role + 1 ResourceDataSync + bucketCount Maintenance Window Tasks); evidenced in test by a child-resource count assertion. |
| Invariants/assertions required (v4 Rule 3) | Ec2PatchBaseline: (i) args.tier in ["Sandbox", "StartupHardened"] via assertValidTier(args.tier). (ii) cron(...) schedule regex-validated against six-field cron. (iii) rebootOption.kind === "NoReboot" → typeof hulumi_decision_comment === "string" && hulumi_decision_comment.length > 0. (iv) tier === "StartupHardened" → staggering !== undefined. (v) staggering.bucketCount > 0 && <= 10. (vi) complianceMetric.snsTopicArn resolves non-empty. (vii) NEW 2026-05-01: args.patchGroupTagValue ∈ {"dev","staging","production"} — refused otherwise. Ec2PatchWaves: (viii) tier === "StartupHardened" → waves.dev !== undefined && waves.staging !== undefined && waves.production !== undefined. (ix) Wave schedules sequence correctly — dev's window cron resolves to an earlier weekday than staging's, staging earlier than production's (asserted at construction time via cron-day-extraction helper). (x) waveHealthGate.appHealthAlarmArns (when supplied) is a non-empty list of valid alarm ARNs (regex-validated). (xi) Each wave's complianceMetric.snsTopicArn echoes the parent Ec2PatchWaves.complianceMetric.snsTopicArn exactly (the wave-composer enforces shared routing — assertion fails if a wave overrides). |
| Debugger / inspection expectation (v4 Rule 1) | The Pulumi mock-runtime test must allow inspection via pnpm --filter @hulumi/baseline test:debug -- ec2-patch-baseline if a non-obvious failure surfaces; the integration test runs against a real AWS sandbox account whose state is inspectable via aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1- --region <region>, aws ssm describe-maintenance-windows --filters Key=Name,Values=hulumi-ops-m1-* --region <region>, and aws iam get-role --role-name <serviceRoleArn-name>. If a test fails non-obviously, the executing agent runs the relevant aws describe-* and pastes output into the Evidence Log before making any speculative code change. |
| Static analysis gates (v4 Rule 2) | pnpm -r format:check (Prettier 3.x) — must pass; pnpm -r typecheck — must pass with strict mode covering the new files; pnpm -r lint (eslint) — must pass; pnpm run lint:license-boundary — must pass (no verbatim CIS / NIST / PCI-DSS text in source); pnpm run lint:exact-pin-guard — must pass (no new @pulumi/* integrity-hash drift). No dependency-audit run needed — the dep graph is unchanged. |
| Forbidden shortcuts | (a) NEVER ship Ec2PatchBaseline with RebootOption defaulting to { kind: "NoReboot" } at any tier — the breach-risk lever the design record commits to depends on RebootIfNeeded being the default. (b) NEVER accept a bare string "NoReboot" for rebootOption — the discriminated union with hulumi_decision_comment is load-bearing. (c) NEVER create a new SNS topic in this component (Rule 10) — complianceMetric.snsTopicArn is consumer-supplied. (d) NEVER ship a Hulumi-authored Lambda (Rule 0) — the compliance metric is a CW Logs metric filter, not a Lambda function. (e) NEVER use child_process.exec, eval, or @aws-sdk/* runtime imports in the component. (f) NEVER ship the component without staggering required at tier: StartupHardened — fail-loud is the synchronized-reboot mitigation. (g) NEVER widen the Maintenance Window service-role IAM policy beyond ssm:SendCommand + ssm:GetCommandInvocation + ec2:DescribeInstances (the abuse-case row service-role-least-priv requires the minimum). (h) NEVER swallow construction errors — every input violation throws a structured Error with the field name in the message; tests assert error messages contain field names. (i) NEVER leave a pulumi.log.warn in production paths after debug — debug pulumi.log.debug only, and remove before milestone close. (j) NEVER use a Hulumi-authored SSM Run Command document — only AWS-managed AWS-RunPatchBaseline. (k) NEVER add a placeholder for "TODO: support cross-account targets in v1.2" — open-question Q4 in the design record gates that decision; either ship cross-account in M1 (with full BDD coverage) or do NOT add the arg. M1 ships single-account only. NEW 2026-05-01 per Flaw 2: (l) NEVER accept patchGroupTagValue outside the {dev, staging, production} enum — even if a consumer asks for qa or pre-prod, refuse construction. Extending the enum is a v1.3 contract change, not an in-place addition. (m) NEVER ship Ec2PatchWaves with a Lambda or Step Function powering the gate — the gate is aws.cloudwatch.CompositeAlarm + Pulumi Output<bool> chain into MaintenanceWindow.enabled; if the implementation reaches for runtime code, the design has been departed from. (n) NEVER allow a wave to override the parent complianceMetric.snsTopicArn — shared routing is the discipline; wave-level routing override is a v1.3 ask, not M1. |
- No
DetectiveServicesEnable— that's M2. - No
AuditTrail— that's M3. - No additional
patchGroupTagValueenum values beyonddev/staging/production— extending the enum is a v1.3 contract change. - No wave-level routing override —
Ec2PatchWavesenforces sharedcomplianceMetric.snsTopicArnacross all waves. - No Lambda or Step Function in the wave health gate — pure CompositeAlarm + Pulumi
Output<bool>chain. - No
HulumiOperationsHardeningPack(O_*policy rules) — that's M4. - No
/hulumi-threat-modelscenario foraws-patch-compliance-lapse— that's M5. - No new SNS topics. EVER. (Rule 10.)
- No Hulumi-authored Lambda. EVER. (Rule 0.)
- No
Ec2PatchBaseline.findingsRoutingSnsArn→ that field name belongs toDetectiveServicesEnable; here it'scomplianceMetric.snsTopicArn. Don't conflate. - No cross-account
MaintenanceWindowTarget.Targets[].TargetAccountIds— open-question Q4 in the design record gates this decision; M1 ships single-account only. - No
examples/ec2-patch-baseline-smoke/— that's M5 launch-readiness work. - No Patch Manager AppConfig / Patch Manager
Applicationresource wrapping — focus only on Baseline + MaintenanceWindow + Target + Task + ResourceDataSync + service role. - No license-boundary additions in
docs/mappings/— those land in M4 alongside the policy pack. - No CI integration of the real-AWS sandbox test in
weekly-integration.yml— that lands in M5 alongside the release. M1 ships the test file; CI gating is M5.
- Complete the Global Entry Rules in
../RUNBOOK-hulumi-operations.md. - No
docs/slo/lessons/hulumi-operations-m0.mdexists. Skip "read prior lessons" with a note in the Evidence Log. - Read the design record
../design/hulumi-for-operations.mdend-to-end — every API decision in M1 is committed there. - Read the threat model
../design/hulumi-for-operations-threat-model.md— every BDD abuse-case row in this milestone cites atm-hulumi-ops-abuse-Nrow. - Read files listed in
Files to read before changing anything. - Copy the Evidence Log template into the milestone's Evidence Log section (already present below — clone the row shape).
- Re-state in working notes the seven load-bearing constraints: (i) scope contract — no new SNS topics, no Lambda, no cross-account; (ii)
RebootOptiondiscriminated union —RebootIfNeededdefault at both tiers,NoRebootrequireshulumi_decision_comment; (iii)staggeringrequired atStartupHardened— fail-loud; (iv)bucketCount≤ 10 (resource bound); (v) service-role IAM policy minimum — three actions only; (vi) AWS-managed-document only — no Hulumi-authored SSM Run Command document; (vii) single-account only at M1 — cross-account is open-question Q4 deferred to v1.2. - Verify the AWS CLI is available locally + the sandbox account profile is configured:
aws sts get-caller-identity --profile <hulumi-sandbox-profile>should return the sandbox account ID. If not, the integration test gracefully skips with a clear message; ship the unit + mock-runtime tests anyway.
| File | Planned Change |
|---|---|
packages/baseline/src/aws/ec2-patch-baseline.ts |
NEW: Ec2PatchBaseline extends pulumi.ComponentResource; mirrors secure-bucket.ts shape |
packages/baseline/src/aws/ec2-patch-baseline.args.ts |
NEW: Ec2PatchBaselineArgs type — every field per the Contract Block; patchGroupTagValue is a string-literal union enum |
packages/baseline/src/aws/ec2-patch-baseline.outputs.ts |
NEW: Ec2PatchBaselineOutputs type |
packages/baseline/src/aws/ec2-patch-waves.ts |
NEW 2026-05-01: Ec2PatchWaves extends pulumi.ComponentResource; composes up to 3 Ec2PatchBaseline instances + 2 aws.cloudwatch.CompositeAlarm gates (dev→staging, staging→production) |
packages/baseline/src/aws/ec2-patch-waves.args.ts |
NEW 2026-05-01: Ec2PatchWavesArgs type — waves: { dev?: WaveArgs; staging?: WaveArgs; production?: WaveArgs } shape; WaveArgs re-uses Ec2PatchBaselineArgs minus tier and patchGroupTagValue |
packages/baseline/src/aws/ec2-patch-waves.outputs.ts |
NEW 2026-05-01: Ec2PatchWavesOutputs type |
packages/baseline/src/aws/index.ts |
MODIFY: re-export Ec2PatchBaseline* + Ec2PatchWaves* symbols (8 total names) |
packages/baseline/tests/aws/ec2-patch-baseline.test.ts |
NEW: Vitest BDD covering happy path × tier × OS, invalid input rows, abuse-case rows including tag-outside-enum |
packages/baseline/tests/aws/ec2-patch-waves.test.ts |
NEW 2026-05-01: Vitest BDD covering: Sandbox single-wave degradation, Hardened all-three-wave requirement, composite alarm wiring, gate-fires-disables-next-wave, gate-clears-re-enables-next-wave, schedule-ordering invariant (ix), shared-SNS-routing invariant (xi) |
packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts |
NEW: real-AWS sandbox test for Ec2PatchBaseline per prior shape |
packages/baseline/tests/integration/aws-ops/ec2-patch-waves.aws-ops.test.ts |
NEW 2026-05-01: real-AWS sandbox test creating all three waves with staggering per wave; asserts aws cloudwatch describe-alarms --alarm-name-prefix hulumi-ops-m1- returns 2 composite alarms with the documented metric set |
docs/slo/completed/RUNBOOK-hulumi-operations.md Milestone Tracker |
MODIFY: M1 row → in_progress on start, done on exit |
docs/slo/runbook-milestones/hulumi-operations-m1.md |
MODIFY (during execution only): fill Evidence Log rows |
docs/slo/lessons/hulumi-operations-m1.md |
NEW (during exit): per the v4 lessons template |
docs/slo/completion/hulumi-operations-m1.md |
NEW (during exit): per the v4 completion template |
docs/components/ec2-patch-baseline.md |
NEW (one-line stub): "Patch Manager wrapper enforcing tier-aware reboot + stagger + compliance routing; full reference at M5." |
docs/ARCHITECTURE.md |
MODIFY: append one paragraph describing the new Ec2PatchBaseline under the AWS account-level baseline section (link to design record) |
.gitignore |
MODIFY (only if needed): add patterns for any Pulumi checkpoints under the integration-test directory |
- Re-state milestone constraints in working notes (the seven Pre-Flight constraints). Confirm the AWS sandbox profile resolves; if not, document in Evidence Log and proceed with mock-runtime tests only.
- Write
packages/baseline/tests/aws/ec2-patch-baseline.test.tsmock-runtime tests covering every BDD row in the table below. Run — expect failures for "module not found". - Implement
Ec2PatchBaselineArgs(packages/baseline/src/aws/ec2-patch-baseline.args.ts) — every field strictly typed; theRebootOptiondiscriminated union and thestaggering: StartupHardened-requiredconstraint expressed via TypeScript discriminated unions where possible (thestaggeringconstraint is a runtime check, not a type-level one). - Implement
Ec2PatchBaselineOutputs(packages/baseline/src/aws/ec2-patch-baseline.outputs.ts). - Implement
Ec2PatchBaseline(packages/baseline/src/aws/ec2-patch-baseline.ts) —pulumi.ComponentResourceconstructor that: (a) runs the six runtime invariants from the Contract Block; (b) creates the IAM service role with the three-action minimum policy; (c) creates the Patch Baseline withapprovalRulesmapped toaws.ssm.PatchBaseline.approvalRules; (d) creates the Patch Group; (e) creates the Maintenance Window withcron(...)schedule + duration + cutoff; (f) createsbucketCountMaintenance Window Tasks each with the documented hash-bucket selector + offset; (g) creates the ResourceDataSync writing compliance to a CW Logs group; (h) creates the metric filter on that log group; (i) creates the alarm on the metric filter routed tocomplianceMetric.snsTopicArn; (j) emits the tag triple on every resource where it makes sense. - Re-export from
packages/baseline/src/aws/index.ts. - Run
pnpm --filter @hulumi/baseline test -- ec2-patch-baseline— expect green for the mock-runtime BDD rows. - Write
packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts— gated onHULUMI_INTEGRATION=1+HULUMI_AWS_SANDBOX_PROFILE=<name>env vars; uses prefixhulumi-ops-m1-<test-id>-on every resource name;afterAllaws ssm delete-patch-baseline+delete-maintenance-windowby prefix; teardown survives partial failure. Run locally if sandbox available, otherwise document the skip. - Run the full repo test suite:
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-guard. All green. - Complete the Self-Review Gate (top-level runbook § Self-Review Gate). Update Milestone Tracker, write lessons + completion files, update
docs/ARCHITECTURE.mdwith the one-paragraph addition.
Feature: @hulumi/baseline.aws.Ec2PatchBaseline — tier-aware reboot + stagger + compliance routing
| Scenario | Category | Given | When | Then | Threat-model row | Control |
|---|---|---|---|---|---|---|
Happy path — Sandbox tier with all required args |
happy path | mock-runtime; tier: Sandbox, patchGroupTagValue: "production", operatingSystem: "AMAZON_LINUX_2023", approvalRules with severities: ["Critical","Important"], maintenanceWindow.rebootOption: { kind: "RebootIfNeeded" }, complianceMetric wired |
new Ec2PatchBaseline("my-patch", args) is constructed |
child aws.ssm.PatchBaseline registered with approvalRules containing both severities; child Maintenance Window registered with cron schedule; one child Maintenance Window Task registered (default bucketCount: 1); IAM service role has policy with exactly ssm:SendCommand, ssm:GetCommandInvocation, ec2:DescribeInstances; outputs expose all six documented fields |
n/a (happy path) | n/a |
Happy path — StartupHardened tier with explicit staggering |
happy path | mock-runtime; same as above but tier: StartupHardened, explicit staggering: { bucketCount: 4, bucketWindowOffsetMinutes: 15 }, explicit maintenanceWindow.schedule |
new Ec2PatchBaseline("my-patch", args) is constructed |
4 child Maintenance Window Tasks registered (one per bucket); each Task has targetParameters filter referencing a different CRC32-mod-4 bucket; offsets are 0/15/30/45 minutes from the base cron; outputs bucketCount = 4 |
n/a (happy path) | n/a |
Happy path — RebootIfNeeded is the default at both tiers |
happy path | mock-runtime; args.maintenanceWindow.rebootOption set explicitly to { kind: "RebootIfNeeded" } |
new Ec2PatchBaseline(...) is constructed |
the underlying MaintenanceWindowTask.taskInvocationParameters.runCommandParameters.parameters.RebootOption resolves to ["RebootIfNeeded"] (the AWS SSM document parameter shape) |
n/a (happy path; reinforces the design decision) | n/a |
Happy path — complianceMetric.snsTopicArn resolves |
happy path | mock-runtime; complianceMetric.snsTopicArn: pulumi.output("arn:aws:sns:eu-west-2:111:topic-high") |
constructor | child aws.cloudwatch.MetricAlarm.alarmActions contains the resolved ARN; the metric filter is on a CW Logs group whose name starts with the component instance name |
n/a | C9 logging |
Invalid input — missing tier |
invalid input | mock-runtime; args cast through as any to bypass TS, no tier field |
constructor | throws Error('Ec2PatchBaseline: tier must be one of [Sandbox, StartupHardened]'); no child resources registered |
n/a (invalid input) | C5 input validation |
Invalid input — bad cron(...) schedule |
invalid input | mock-runtime; maintenanceWindow.schedule: "every Wednesday" |
constructor | throws Error('Ec2PatchBaseline: maintenanceWindow.schedule must be a six-field cron(...) expression'); no child resources |
n/a | C5 |
| Invalid input — duplicate severity | invalid input | mock-runtime; approvalRules.severities: ["Critical", "Critical"] |
constructor | throws Error('Ec2PatchBaseline: approvalRules.severities must not contain duplicates'); no child resources |
n/a | C5 |
Invalid input — cutoffHours >= durationHours |
invalid input | mock-runtime; maintenanceWindow.durationHours: 1, cutoffHours: 2 |
constructor | throws Error('Ec2PatchBaseline: maintenanceWindow.cutoffHours must be less than durationHours'); no child resources |
n/a | C5 |
Empty state — no EC2 instances tagged with Patch:Group value |
empty state | real-AWS integration; no EC2 has Patch:Group=production tag |
pulumi up succeeds; aws ssm describe-patch-baselines returns the new baseline |
no patches applied at first Maintenance Window run (zero targets); compliance metric is 0 events; alarm does not fire |
n/a | n/a |
Dependency failure — complianceMetric.snsTopicArn resolves to empty string |
partial failure | mock-runtime; complianceMetric.snsTopicArn: pulumi.output("") |
constructor | invariant assertion (vi) fires; throws Error('Ec2PatchBaseline: complianceMetric.snsTopicArn must resolve to a non-empty string') |
n/a | C5 + invariant (Rule 11) |
Resource bound — staggering.bucketCount = 11 rejected |
resource bound (Rule 4) | mock-runtime; tier: StartupHardened, staggering: { bucketCount: 11, bucketWindowOffsetMinutes: 5 } |
constructor | throws Error('Ec2PatchBaseline.staggering.bucketCount: max 10'); no child resources |
n/a | Rule 4 + C5 |
Invariant — tier: StartupHardened without staggering rejected |
assertion violation | mock-runtime; tier: StartupHardened, staggering: undefined |
constructor | throws Error('Ec2PatchBaseline: staggering is required at tier StartupHardened (synchronized-reboot mitigation)'); no child resources |
tm-hulumi-ops-abuse-stagger-fail-loud |
Rule 11 + C5 |
Compatibility — existing MonitoringFoundation outputs still consumed unchanged |
compatibility | mock-runtime; component receives a MonitoringFoundation.high.arn output as complianceMetric.snsTopicArn |
constructor | the ARN is wired into MetricAlarm.alarmActions exactly once; no copy of the topic policy is added; no new SNS resource is registered |
n/a | Rule 10 |
Abuse case — NoReboot without hulumi_decision_comment rejected |
abuse case | mock-runtime; args cast through as any to write rebootOption: { kind: "NoReboot" } (no comment) |
constructor | throws Error('Ec2PatchBaseline.maintenanceWindow.rebootOption: kind=\"NoReboot\" requires hulumi_decision_comment to be a non-empty string'); no child resources |
tm-hulumi-ops-abuse-noreboot-without-decision |
Forbidden shortcut (b) + Rule 11 |
| Abuse case — service role IAM policy is exactly the three-action minimum | abuse case | mock-runtime; happy-path args | constructor | child aws.iam.RolePolicy.policy JSON document parses to exactly one Statement with Action: ["ssm:SendCommand", "ssm:GetCommandInvocation", "ec2:DescribeInstances"] (sorted) and Resource: "*"; assertion fails the test if any other action is present |
tm-hulumi-ops-abuse-service-role-least-priv |
Forbidden shortcut (g) + Rule 11 |
Abuse case — uses AWS-managed AWS-RunPatchBaseline document only |
abuse case | mock-runtime; happy-path args | constructor | every MaintenanceWindowTask.taskArn resolves to literal "AWS-RunPatchBaseline" (the AWS-managed SSM document name); no Hulumi-authored document is referenced |
tm-hulumi-ops-abuse-no-runcmd-secrets |
Forbidden shortcut (j) |
| Abuse case — CW Logs delivery-failure alarm exists | abuse case | mock-runtime; happy-path args | constructor | a child aws.cloudwatch.MetricAlarm is registered targeting the LogDelivery.Errors metric on the compliance log group, alarm action routed to complianceMetric.snsTopicArn |
tm-hulumi-ops-abuse-cw-log-delivery-alarm |
Rule 11 |
Schema / compatibility — Ec2PatchBaselineArgs shape lock |
schema / compatibility | packages/baseline/src/aws/ec2-patch-baseline.args.ts exists |
tests/skill-bdd/operations-args.test.ts (added in this milestone if absent) runs |
every documented field present with documented types; tier, patchGroupTagValue, operatingSystem, approvalRules, maintenanceWindow, complianceMetric are required; staggering is optional; the RebootOption discriminated union is exposed as a public type |
n/a | type-layer schema lock |
Feature: @hulumi/baseline.aws.Ec2PatchWaves — wave-sequenced patching with composite-alarm health gate
| Scenario | Category | Given | When | Then | Threat-model row | Control |
|---|---|---|---|---|---|---|
| Happy path — Sandbox single-wave degradation | happy path | tier: Sandbox, waves: { dev: { ... } } only |
constructor | one Ec2PatchBaseline registered (the dev wave); zero aws.cloudwatch.CompositeAlarm; outputs wavesEnabled: { dev: true, staging: false, production: false } |
n/a | tier-degradation discipline |
| Happy path — StartupHardened all-three-wave | happy path | tier: StartupHardened, all three waves keys with valid WaveArgs |
constructor | three Ec2PatchBaseline children registered; two aws.cloudwatch.CompositeAlarm registered (devToStaging, stagingToProduction); each composite alarm aggregates prior wave's SSM-Compliance-Failed metric + consumer's appHealthAlarmArns |
n/a | n/a |
| Happy path — gate fires → next wave's MaintenanceWindow disabled | happy path | tier: StartupHardened; mock-runtime: devToStaging composite alarm in ALARM state |
re-run pulumi up with mock alarm state |
the staging wave's MaintenanceWindow.enabled resolves to false via Output<bool> chain; outputs wavesEnabled.staging: false |
n/a | rollback-as-IaC (Rule 11) |
Happy path — gate clears → next wave re-enabled on next pulumi up |
happy path | gate previously fired, then alarm returns to OK |
next pulumi up |
staging wave's MaintenanceWindow.enabled resolves to true |
n/a | flop-not-latch semantics |
| Invalid input — StartupHardened missing one of the three waves | invalid input | tier: StartupHardened, waves: { dev: { ... }, production: { ... } } (no staging) |
constructor | throws Error('Ec2PatchWaves: tier StartupHardened requires all three waves (dev, staging, production); missing: staging') |
n/a | invariant (viii) |
Invalid input — wave overrides complianceMetric.snsTopicArn |
invalid input | one wave's WaveArgs includes a complianceMetric key (which WaveArgs should refuse at the type level) |
constructor | TypeScript rejects at compile time (preferred); at runtime via as any, throws Error('Ec2PatchWaves: per-wave complianceMetric override not supported in v1.2; shared routing only') |
n/a | invariant (xi) |
Abuse case — patchGroupTagValue outside enum refused |
abuse case | mock-runtime: Ec2PatchBaseline constructed with patchGroupTagValue: "experiment" (cast through as any) |
constructor | throws Error('Ec2PatchBaseline: patchGroupTagValue must be one of [dev, staging, production]; got: "experiment"') |
tm-hulumi-ops-abuse-tag-outside-enum |
invariant (vii) + Forbidden (l) |
| Abuse case — manual flip of MaintenanceWindow.enabled is observable | abuse case | real-AWS integration: a human operator runs aws ssm update-maintenance-window --window-id <id> --enabled mid-incident |
(operator action, not Hulumi code path) | CloudTrail captures the ssm:UpdateMaintenanceWindow event; existing MonitoringFoundation wiring (M5/#46) routes it through MonitoringFoundation.high SNS; assertion in integration test checks the CloudTrail event lands in CW Logs |
tm-hulumi-ops-abuse-skip-wave-gate |
observability discipline |
| Resource bound — 4 waves rejected | resource bound | waves includes a 4th key (e.g., qa) — TS should reject; via as any |
constructor | throws Error('Ec2PatchWaves: only dev / staging / production waves supported in v1.2') |
n/a | Rule 4 |
Compatibility — Ec2PatchBaseline standalone usage unchanged |
compatibility | consumer instantiates a single Ec2PatchBaseline directly (no Ec2PatchWaves) |
constructor | works exactly as the original M1 contract block describes; tag-outside-enum is the only added refusal vs. v0 of M1 |
n/a | additive surface |
- All AWS BDD scenarios in
packages/baseline/tests/aws/continue to pass (no rename, no behavioral change toSecureBucket,AccountFoundation,MonitoringFoundation,IdentityAlarms). - All GitHub BDD scenarios in
packages/baseline/tests/github/continue to pass. - All K8s BDD scenarios in
packages/k8s-baseline/tests/continue to pass. pnpm run lint:license-boundarycontinues to pass on the existing surface.pnpm run lint:exact-pin-guardpasses; no new@pulumi/*deps introduced.- Skill
SKILL.mdcontinues to validate against the agentskills.io schema (no skill changes in M1). packages/drift/tests/tla-alignment.test.tscontinues to pass (this milestone does not touchverdict.tsorHulumiDrift.tla).
-
Ec2PatchBaseline,Ec2PatchBaselineArgs,Ec2PatchBaselineOutputs,EC2_PATCH_BASELINE_COMPONENT_TYPEdocumented indocs/components/ec2-patch-baseline.md(one-line stub adequate; full reference doc in M5). - No new dependencies introduced —
pnpm-lock.yamldiff has no@pulumi/*additions or version bumps. -
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 on Node 20 LTS. - License header present on every new
.tssource file. - DCO sign-off required on every commit (CI enforcement carries over).
- All existing AWS + GitHub + K8s BDD scenarios produce valid output unchanged.
- No
child_process.exec,eval,@aws-sdk/*runtime imports, or shell-interpolation in any new file —tests/no-shell-exec.test.ts(existing) coverspackages/*/src/; verify glob includes the new file. -
Tierenum unchanged;MonitoringFoundationoutputs unchanged;assertValidTierunchanged.
File: packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts.
| E2E Test | What It Proves | Pass Criteria |
|---|---|---|
creates_patch_baseline_against_real_sandbox |
Ec2PatchBaseline works end-to-end against real AWS |
Test creates a Pulumi stack with Ec2PatchBaseline("hulumi-ops-m1-<test-id>", { tier: Sandbox, ... }); aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1-<test-id>- --region <region> returns exactly one entry whose OperatingSystem matches input; teardown deletes the baseline and Maintenance Window cleanly. |
staggering_bucketCount_4_creates_4_window_tasks |
The CRC32-bucket-stagger decision is honored on real AWS | Test creates tier: StartupHardened with staggering.bucketCount: 4; aws ssm describe-maintenance-window-tasks --window-id <id> returns exactly 4 tasks; each task's Targets[0].Values[0] filter is distinct and references one of the four hash-bucket values; offsets are 0/15/30/45 minutes apart in Schedule parameter. |
service_role_policy_is_three_action_minimum |
The least-privilege abuse case holds end-to-end | After pulumi up, aws iam list-attached-role-policies --role-name <serviceRoleName> returns one inline policy whose Action is exactly ["ssm:SendCommand", "ssm:GetCommandInvocation", "ec2:DescribeInstances"]; assertion fails if any additional action present. |
cw_log_delivery_alarm_exists_with_correct_target |
The CW Logs delivery-failure alarm wiring is correct | After pulumi up, aws cloudwatch describe-alarms --alarm-name-prefix hulumi-ops-m1-<test-id>- returns at least one alarm with MetricName: LogDelivery.Errors, AlarmActions containing the complianceMetric.snsTopicArn that was passed in. |
The integration test gracefully skips with a clear message when HULUMI_INTEGRATION is not set or when aws sts get-caller-identity --profile <hulumi-sandbox-profile> fails. The test runs locally during execution and lands in the weekly-integration matrix in M5.
-
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-guard→ all green. - (Optional, requires AWS sandbox)
HULUMI_INTEGRATION=1 HULUMI_AWS_SANDBOX_PROFILE=<name> pnpm --filter @hulumi/baseline test:integration:aws-ops→ integration test green; resources deleted after run. - In a Pulumi program importing
@hulumi/baseline:new Ec2PatchBaseline("foo", { tier: "StartupHardened", ... no staggering ... } as any)causespulumi previewto fail with the documented missing-staggering error. - In a Pulumi program:
new Ec2PatchBaseline("foo", { ..., maintenanceWindow: { ..., rebootOption: { kind: "NoReboot" } } } as any)causespulumi previewto fail with the documented missing-comment error. -
git statusshows no untracked test artifacts. -
.gitignorecovers any new generated files (Pulumi checkpoints undertests/integration/aws-ops/are gitignored if they appear).
| Step | Command / Check | Expected Result | Actual Result | Pass/Fail | Notes |
|---|---|---|---|---|---|
| Baseline tests | pnpm -r build && pnpm -r test |
green pre-M1 | filled during execution | pending | Captures pre-M1 baseline so any regression is attributable to M1 |
| BDD tests created | pnpm --filter @hulumi/baseline test -- ec2-patch-baseline (pre-impl) |
fail with module-not-found / wrong-shape errors | filled during execution | pending | Pre-implementation failure shape captured |
| E2E stubs created | cat packages/baseline/tests/integration/aws-ops/ec2-patch-baseline.aws-ops.test.ts |
file present, it.skip(...) rows for every E2E table row |
filled during execution | pending | Stubs land before implementation |
| Implementation | filesystem | source files present and re-exported from index.ts |
filled during execution | pending | After implementation |
| Formatter | pnpm -r format:check |
clean | filled during execution | pending | |
| Typecheck | pnpm -r typecheck |
clean | filled during execution | pending | |
| Static analyzer | pnpm -r lint |
clean (no new warnings) | filled during execution | pending | |
| License-boundary lint | pnpm run lint:license-boundary |
OK | filled during execution | pending | Existing lint scope unchanged in M1 |
| Exact-pin guard | pnpm run lint:exact-pin-guard |
OK with no new @pulumi/* deps |
filled during execution | pending | No-op for this milestone |
| Dependency audit (if deps changed) | pnpm audit --audit-level=high |
n/a — dep graph unchanged | filled during execution | pending | Skip with note |
| Mock-runtime BDD | pnpm --filter @hulumi/baseline test -- ec2-patch-baseline |
all BDD rows pass | filled during execution | pending | Every row from the BDD table covered |
| Full tests | pnpm -r test |
green | filled during execution | pending | |
| Real-AWS integration test | HULUMI_INTEGRATION=1 HULUMI_AWS_SANDBOX_PROFILE=<name> pnpm --filter @hulumi/baseline test:integration:aws-ops |
green or skipped | filled during execution | pending | Skip cleanly if AWS sandbox unavailable |
| Build/boot | pnpm -r build |
builds cleanly; dist/ present |
filled during execution | pending | |
| Resource-bound verification (Rule 4) | pnpm --filter @hulumi/baseline test -- ec2-patch-baseline -t "bucketCount = 11" |
bound encoded; near-limit tested | filled during execution | pending | Asserts bucketCount=10 succeeds, =11 rejected |
| Invariant/assertion verification (Rule 3) | pnpm --filter @hulumi/baseline test -- ec2-patch-baseline -t "staggering required at StartupHardened" |
assertion fires; structured error returned | filled during execution | pending | Covers the StartupHardened-without-staggering invariant |
| Debugger / state inspection (Rule 1) | aws ssm describe-patch-baselines --filters Key=NAME_PREFIX,Values=hulumi-ops-m1- --region <region> |
hypothesis confirmed before any speculative code change | filled during execution | pending | Used only if integration test fails non-obviously |
| Smoke tests | (manual list above) | all checked | filled during execution | pending | |
| Test artifact cleanup | git status --short |
only intentional new files + modified tracker | filled during execution | pending | No transient test artifacts |
| .gitignore review | existing .gitignore covers Node/pnpm/Vitest/TLA+/Pulumi-checkpoint |
no change needed unless aws-ops integration test drops state in repo | filled during execution | pending | |
| Compatibility checks | (manual list above) | no regressions | filled during execution | pending |
The milestone is done only when all of the following are objectively true:
- All BDD scenarios pass (mock-runtime always; real-AWS integration when sandbox available).
- All E2E runtime validation tests pass (or skip cleanly with documented reason).
pnpm -r format:check,pnpm -r typecheck,pnpm -r lint,pnpm run lint:license-boundary,pnpm run lint:exact-pin-guardall green.- Smoke tests checked off.
- Compatibility checklist complete.
- Resource bounds (v4 Rule 4) encoded and tested (
bucketCount≤ 10 +severitieslength 1–4 +cutoffHours < durationHours). - Invariants/assertions (v4 Rule 3) encoded and tested (the six invariants in the Contract Block).
- No forbidden shortcuts present.
git statusclean..gitignorecovers all new generated files.- All existing AWS + GitHub + K8s BDD scenarios produce valid output unchanged (regression-tested).
- Self-Review Gate (top-level runbook) answered with
yeson every question or documented exception. docs/slo/lessons/hulumi-operations-m1.mdwritten per the v4 lessons template (incl. assumptions verified / unresolved, invariants added, resource bounds, debugging notes).docs/slo/completion/hulumi-operations-m1.mdwritten per the v4 completion template.- Milestone Tracker in
docs/slo/completed/RUNBOOK-hulumi-operations.mdupdated todone.
docs/slo/completed/RUNBOOK-hulumi-operations.mdMilestone Tracker → M1done.docs/slo/completed/RUNBOOK-hulumi-operations.mdComponent Summary Table — verify M1 row matches what was actually shipped.docs/components/ec2-patch-baseline.md— one-line stub if not present (full reference doc in M5).docs/ARCHITECTURE.md— one-paragraph description ofEc2PatchBaselineunder the AWS account-level baseline section (link to design record).docs/issue-candidates.md— no strikes in M1 (M1 doesn't close any open issue; #47 closes in M3, #49 closes in M2).
- This milestone ships no
DetectiveServicesEnable, noAuditTrail, noO_*policy rules, no new threat-model scenario — those are M2 / M3 / M4 / M5. - Prior-lessons coverage category does not apply (greenfield for the Operations variant).
- The
RebootOptiondiscriminated union ({ kind: "RebootIfNeeded" } | { kind: "NoReboot", hulumi_decision_comment: string }) is the single most important design decision in this milestone — it must be recorded in the lessons file as a deliberate inversion of the "Sandbox = NoReboot" trap and as the load-bearing reasonEc2PatchBaselineships at all. The threat-model rowtm-hulumi-ops-abuse-noreboot-without-decisionis the specific abuse-case row this design decision answers. - The real-AWS integration test is gated on
HULUMI_INTEGRATION=1+ a configured sandbox profile. CI in this milestone runs only mock-runtime BDD; the aws-ops suite is exercised in M5's release readiness smoke + the weekly-integration workflow extension. - Open-question Q4 from the design record (
MaintenanceWindowTarget.Targets[].TargetAccountIds— cross-account targets) is explicitly deferred to v1.2; M1 ships single-account only. Document the deferral in the lessons file with a pointer to the open question.