@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 +5 -1
- package/dist/.build-fingerprint.json +1 -1
- package/dist/alchemy/index.d.ts +35 -4
- package/dist/alchemy/index.js +516 -392
- package/dist/index.d.ts +5 -4
- package/dist/index.js +24 -8
- package/package.json +1 -1
- package/src/alchemy/first-deploy-authorization.ts +49 -7
- package/src/alchemy/plan-effects.ts +128 -7
- package/src/merge-freeze-job.ts +24 -8
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
|
|
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
|
|
package/dist/alchemy/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
508
|
-
*
|
|
509
|
-
*
|
|
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;
|