@patronage/factory-ci 1.0.0-alpha.33 → 1.0.0-alpha.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -719,7 +719,11 @@ Four checks that surround an Alchemy run. Paitronage and firedup each built the
719
719
 
720
720
  `credentialPreflight({ accountId, environment, requiredPermissionGroups, resolveCredential, resourcesFor?, source })` proves the deploy holds the credential it claims to. `source: "hosted"` requires `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` in the environment and requires the account to be the one the deploy allows; `source: "trusted-local"` requires that neither is there, because a local run resolves its credential from the project's own secret manager and an ambient token means the run is not the run it says it is. `resolveCredential` is where that resolution happens — a 1Password read, a hosted environment read, whatever the project uses. The token is then verified against Cloudflare, matched to the reviewed policy document by token id, and reduced to permission-group names, which must equal `requiredPermissionGroups` with nothing missing, extra, or repeated. `resourcesFor` names the exact resource identifiers a group may be scoped to and defaults to the account resource, which is how a zone-scoped group is declared. On the hosted path the resolved token must be the environment token, compared and never named, because that is the token the deploy will use. A `CloudflareCredentialUnavailableError` means Cloudflare could not be asked, or the project's own resolver failed; a resolver failure is reported as one fixed message with no `cause`, because a secret-manager error can quote the value it was reading. Every other failure means the credential is wrong. No error and no result carries a credential value.
721
721
 
722
- `firstDeployAuthorization({ authorizedHashes, planText, sourceSha, stack, stage })` binds the one deploy no earlier state can constrain. The plan text, the target, and the reviewed source hash to one value, and that value must appear on a list the project owns. `sourceSha` is required. A full lowercase 40-character SHA binds the plan to that commit; `null` binds the plan to no commit, and that is the project's explicit choice rather than a field it forgot — an omitted field and a deliberate `null` are different decisions, so only the second one is spellable. Anything else, an empty string included, is refused. It fails closed on an empty list, a list entry that is not a sha256 hash, an empty plan, plan output that is not well-formed, an unlisted plan, and a stack, stage, or source SHA carrying surrounding whitespace — padding would mint a second hash for one target. The refusal names the hash to review, never the plan. `firstDeployPlanHash` computes the same value on its own, for the run that produces a plan for an operator to read.
722
+ `firstDeployAuthorization({ authorizedHashes, planText, sourceSha, stack, stage })` binds the one deploy no earlier state can constrain. The plan, the target, and the reviewed source hash to one value, and that value must appear on a list the project owns. `sourceSha` is required. A full lowercase 40-character SHA binds the plan to that commit; `null` binds the plan to no commit, and that is the project's explicit choice rather than a field it forgot — an omitted field and a deliberate `null` are different decisions, so only the second one is spellable. Anything else, an empty string included, is refused. It fails closed on an empty list, a list entry that is not a sha256 hash, an empty plan, plan output that is not well-formed, plan output the plan adapter cannot read, an unlisted plan, and a stack, stage, or source SHA carrying surrounding whitespace — padding would mint a second hash for one target. The refusal names the hash to review, never the plan. `firstDeployPlanHash` computes the same value on its own, for the run that produces a plan for an operator to read.
723
+
724
+ What the hash binds is the plan, not the run that printed it (#1232). `planText` is read by the same `parsePlanEffects` adapter above and reduced to a canonical form first: the `Plan:` summary line, every row under it as its tag and action — resource rows, binding rows and task rows alike, `noop` rows included, sorted and with duplicates kept — and the reading of those rows the adapter arrives at, the resource list and the binding-change count. That canonical form, the stack, the stage and the source SHA are what hash. The logger prefix Alchemy puts on every line (`[HH:MM:SS.mmm] INFO (#1): `), the run's own elapsed durations (`Plan ready (2.9s)`), ANSI colour, the apply-session and progress lines after the rows, and the order the rows were printed in are all facts about one process rather than about the plan, and none of them is bound.
725
+
726
+ Row order is not bound; the reading of the rows is, and those are not the same fact. A binding row and a resource inside the sibling's namespace print alike, so `[Parent/bound] create` and `[Parent/Zed] create` under `[Parent] create` are one row multiset that creates `Parent` and `Parent/Zed` in one order and `Parent` and `Parent/bound` in the other. Order alone carried that; the canonical form states it, so sorting the rows cannot make two plans that create different resources agree. A rename, a changed action, an added or removed row, a renamed binding whose count is unchanged, a row read as a resource in one plan and a binding in the other, or a different stack, stage or source each change the hash; so does output the adapter refuses, which throws a `PlanEffectsError` rather than hashing. Before this change the verbatim output was hashed, so two graph-identical runs disagreed and a reviewed plan could never be deployed: every first-deploy hash issued earlier is invalidated, and those hashes were unreproducible in any case.
723
727
 
724
728
  `assertDestroyLeftNothing({ stack, stage, stateReader })` is the postcondition that a zero exit does not prove: after `destroy`, the state store lists nothing for the stage. The project supplies `stateReader`, because only the project knows which store it configured. A reader that returns anything other than a list of non-blank resource identifiers is a failure, not an empty stage — a check that cannot fail is not a check. A padded stack or stage is refused for the same reason: it addresses a stage the store does not know, which answers empty.
725
729
 
@@ -1,3 +1,3 @@
1
1
  {
2
- "fingerprint": "2d700828493887268efc2b9a5d8bccb2ca565d0745e175e1bb8bf86887b059d8"
2
+ "fingerprint": "460142f88d3bbc5b122a0c6235c0830ab3824b3e2501894466f73418c717af80"
3
3
  }
@@ -479,7 +479,10 @@ declare const assertDestroyLeftNothing: ({
479
479
  //#endregion
480
480
  //#region src/alchemy/first-deploy-authorization.d.ts
481
481
  interface FirstDeployPlanHashOptions {
482
- /** The plan output, verbatim. Hashed, never stored and never logged. */
482
+ /**
483
+ * The Alchemy CLI's plan output, verbatim. It is canonicalised and hashed
484
+ * here; it is never stored and never logged.
485
+ */
483
486
  readonly planText: string;
484
487
  /**
485
488
  * The reviewed source commit this plan was produced from, as a full
@@ -504,9 +507,30 @@ interface FirstDeployAuthorization {
504
507
  readonly planHash: string;
505
508
  }
506
509
  /**
507
- * The hash a first deploy is authorized by: the plan text, the target, and
508
- * the reviewed source, in one payload. The plan text is hashed before it
509
- * enters the payload so the value that is compared never embeds plan output.
510
+ * The hash a first deploy is authorized by: the plan, the target, and the
511
+ * reviewed source, in one payload.
512
+ *
513
+ * The plan enters the payload as the sha256 of `canonicalPlan`, so the value
514
+ * that is compared never embeds plan output, and two runs of one plan agree.
515
+ * Bound: the summary line, every row's tag and action (resource, binding and
516
+ * task rows alike, `noop` included), which of those rows are resources and
517
+ * which are bindings, the stack, the stage, and the source SHA. Not bound:
518
+ * logger timestamps and fiber ids, the run's elapsed durations, ANSI colour,
519
+ * progress lines, and the order the rows were printed in.
520
+ *
521
+ * Row order is not bound but the reading of the rows is, because they are
522
+ * not the same fact. Order alone used to carry which rows are a resource's
523
+ * bindings and which are resources inside its namespace; the canonical form
524
+ * states that outright, so two plans creating different resources can never
525
+ * agree merely by printing one multiset of rows two ways.
526
+ *
527
+ * `planFormat` names that shape. A hash issued by an earlier version of this
528
+ * function, which hashed the verbatim output, can never equal one issued
529
+ * now — those hashes were not reproducible in the first place, so they
530
+ * authorized nothing that could be deployed.
531
+ *
532
+ * Throws a `PlanEffectsError` when the plan adapter cannot account for the
533
+ * output: an unreadable plan is refused, never hashed.
510
534
  */
511
535
  declare const firstDeployPlanHash: ({
512
536
  planText,
@@ -712,6 +736,13 @@ declare const isAlchemyLifecycleError: (error: unknown) => error is AlchemyLifec
712
736
  * summary whose counts do not equal the lines under it, or rows the summary
713
737
  * can place in more than one way. An effect it could not account for is
714
738
  * never reported as zero.
739
+ *
740
+ * Two readings share that work. `parsePlanEffects` answers what the plan
741
+ * does, which is what the lifecycle's admission rules read. `canonicalPlan`
742
+ * answers what the plan *is*, as a stable string: the rows the renderer
743
+ * printed and the reading of them, with the per-run logger metadata it also
744
+ * prints left out. `firstDeployPlanHash` hashes that string, so a plan
745
+ * reviewed at one moment and deployed at another is the same plan (#1232).
715
746
  */
716
747
  /** The Alchemy version whose plan renderer this adapter reads. */
717
748
  declare const PLAN_OUTPUT_ALCHEMY_VERSION: string;