@patronage/factory-ci 1.0.0-alpha.19 → 1.0.0-alpha.20

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/dist/index.d.ts CHANGED
@@ -307,9 +307,11 @@ interface PreviewProofInventoryTransport {
307
307
  /** Per-request timeout; defaults to five seconds. */
308
308
  timeoutMs?: number;
309
309
  /**
310
- * Bearer token. `list` accepts a workflow `GITHUB_TOKEN` (read). `persist`
311
- * and `recordCleanup` must use a Factory App installation token: GitHub
312
- * refuses GITHUB_TOKEN writes to App-owned check-runs.
310
+ * Bearer token. `list` accepts a workflow `GITHUB_TOKEN` (read) and needs
311
+ * commit (contents) read for the force-push timeline; the contents-less
312
+ * Factory App token cannot serve it (#825). `persist` and `recordCleanup`
313
+ * must use a Factory App installation token: GitHub refuses GITHUB_TOKEN
314
+ * writes to App-owned check-runs.
313
315
  */
314
316
  token: string;
315
317
  }
@@ -919,6 +921,16 @@ interface FactoryPrStatusHudWorkflowOptions {
919
921
  readonly cli?: string;
920
922
  readonly createGithubAppToken: PinnedAction;
921
923
  }
924
+ /**
925
+ * Least privilege for the job that runs the present step. A `permissions`
926
+ * block zeroes every unlisted GITHUB_TOKEN scope, and the inventory list
927
+ * reads pull requests, commits, and check-runs with that token (#825).
928
+ */
929
+ declare const FACTORY_PR_STATUS_HUD_PERMISSIONS: Readonly<{
930
+ readonly checks: "read";
931
+ readonly contents: "read";
932
+ readonly "pull-requests": "read";
933
+ }>;
922
934
  declare const FACTORY_PR_STATUS_HUD_CONCURRENCY: Readonly<{
923
935
  cancelInProgress: false;
924
936
  group: "status-hud-${{ github.repository }}-${{ github.event.pull_request.number }}";
@@ -928,6 +940,7 @@ interface FactoryPrStatusHudWorkflow {
928
940
  readonly if: typeof FACTORY_PR_STATUS_HUD_IF;
929
941
  readonly jobId: typeof FACTORY_PR_STATUS_HUD_JOB_ID;
930
942
  readonly jobName: typeof FACTORY_PR_STATUS_HUD_JOB_NAME;
943
+ readonly permissions: typeof FACTORY_PR_STATUS_HUD_PERMISSIONS;
931
944
  readonly presentSteps: readonly WorkflowStep[];
932
945
  }
933
946
  declare const factoryPrStatusHudWorkflow: (options: FactoryPrStatusHudWorkflowOptions) => FactoryPrStatusHudWorkflow;
@@ -1329,4 +1342,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
1329
1342
  readonly source: string;
1330
1343
  }) => void;
1331
1344
  //#endregion
1332
- export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, type FactoryPrStatusHudWorkflow, type FactoryPrStatusHudWorkflowOptions, type FactoryPreviewCleanupAuditJob, type FactoryPreviewCleanupDestroyJob, type FactoryPreviewCleanupTopology, type FactoryPreviewCleanupTopologyOptions, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryPushIdentityConsumer, type FactoryPushIdentityConsumerOptions, type FactoryPushIdentityDisposition, type FactoryPushIdentityEnvelope, type FactoryPushIdentityProducer, type FactoryPushIdentityProducerOptions, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
1345
+ export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, type FactoryPrStatusHudWorkflow, type FactoryPrStatusHudWorkflowOptions, type FactoryPreviewCleanupAuditJob, type FactoryPreviewCleanupDestroyJob, type FactoryPreviewCleanupTopology, type FactoryPreviewCleanupTopologyOptions, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryPushIdentityConsumer, type FactoryPushIdentityConsumerOptions, type FactoryPushIdentityDisposition, type FactoryPushIdentityEnvelope, type FactoryPushIdentityProducer, type FactoryPushIdentityProducerOptions, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
package/dist/index.js CHANGED
@@ -1333,7 +1333,10 @@ const githubGraphql = async (request, token, timeoutMs, query, variables) => {
1333
1333
  method: "POST"
1334
1334
  }, timeoutMs);
1335
1335
  if (!isRecord$1(served)) throw new TypeError("GitHub GraphQL response was not an object");
1336
- if (served.errors !== void 0) throw new Error("Preview proof inventory GraphQL query failed.");
1336
+ if (served.errors !== void 0) {
1337
+ const details = Array.isArray(served.errors) ? served.errors.flatMap((entry) => isRecord$1(entry) ? [[entry.type, entry.message].filter((part) => typeof part === "string").join(": ")] : []).filter((detail) => detail.length > 0).join("; ") : "";
1338
+ throw new Error(details.length > 0 ? `Preview proof inventory GraphQL query failed: ${details}` : "Preview proof inventory GraphQL query failed.");
1339
+ }
1337
1340
  return served.data;
1338
1341
  };
1339
1342
  const repoApi = (owner, repo) => `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
@@ -1447,27 +1450,37 @@ const listPullRequestCommitShas = (request, token, timeoutMs, owner, repo, pr) =
1447
1450
  if (!Array.isArray(commits)) throw new TypeError("GitHub pull request commits response was not an array");
1448
1451
  return commits.flatMap((commit) => isRecord$1(commit) && typeof commit.sha === "string" ? [commit.sha] : []);
1449
1452
  }, COMMITS_PER_PAGE, MAX_COMMIT_PAGES, "Preview proof inventory commit listing exhausted pagination before GitHub returned a final page.");
1453
+ const readPullBaseSha = (pull) => {
1454
+ if (!isRecord$1(pull.base) || typeof pull.base.sha !== "string") throw new TypeError("GitHub pull request response omitted base.sha");
1455
+ return pull.base.sha;
1456
+ };
1450
1457
  /**
1451
- * Current head, replaced heads from force-push, and PR commits. The
1452
- * pull-commits endpoint silently stops at 250 and never lists a rebased-away
1453
- * head, so those two cases use compare pagination and the force-push timeline.
1458
+ * Current head, replaced heads from force-push, the commits each replaced head
1459
+ * carried, and PR commits. The pull-commits endpoint silently stops at 250 and
1460
+ * never lists a rebased-away head, so those two cases use compare pagination
1461
+ * and the force-push timeline. A persist, then a fast-forward, then a rebase
1462
+ * leaves the persist SHA behind the replaced head, so each replaced head is
1463
+ * also walked back to the base (#796).
1454
1464
  */
1455
1465
  const listCommitShasForPull = async (request, token, timeoutMs, owner, repo, pr) => {
1456
1466
  const pull = await githubJson(request, `${repoApi(owner, repo)}/pulls/${pr}`, token, { method: "GET" }, timeoutMs);
1457
1467
  if (!isRecord$1(pull) || !isRecord$1(pull.head) || typeof pull.head.sha !== "string") throw new TypeError("GitHub pull request response omitted head.sha");
1458
1468
  const pullCommits = await listPullRequestCommitShas(request, token, timeoutMs, owner, repo, pr);
1459
1469
  let older = pullCommits;
1460
- if (pullCommits.length >= GITHUB_PULL_COMMITS_CAP) {
1461
- if (!isRecord$1(pull.base) || typeof pull.base.sha !== "string") throw new TypeError("GitHub pull request response omitted base.sha");
1462
- older = await listCompareCommitShas(request, token, timeoutMs, owner, repo, pull.base.sha, pull.head.sha);
1463
- }
1470
+ if (pullCommits.length >= GITHUB_PULL_COMMITS_CAP) older = await listCompareCommitShas(request, token, timeoutMs, owner, repo, readPullBaseSha(pull), pull.head.sha);
1464
1471
  const previousHeads = await listForcePushBeforeShas(request, token, timeoutMs, owner, repo, pr);
1472
+ let replaced = [];
1473
+ if (previousHeads.length > 0) {
1474
+ const baseSha = readPullBaseSha(pull);
1475
+ replaced = (await mapPool(previousHeads, 8, (previousHead) => listCompareCommitShas(request, token, timeoutMs, owner, repo, baseSha, previousHead))).flatMap((history) => [...history].toReversed());
1476
+ }
1465
1477
  const shas = [];
1466
1478
  const seen = /* @__PURE__ */ new Set();
1467
1479
  for (const sha of [
1468
1480
  pull.head.sha,
1469
1481
  ...previousHeads,
1470
- ...older.toReversed()
1482
+ ...older.toReversed(),
1483
+ ...replaced
1471
1484
  ]) if (!seen.has(sha)) {
1472
1485
  seen.add(sha);
1473
1486
  shas.push(sha);
@@ -2017,6 +2030,16 @@ const FACTORY_PR_STATUS_HUD_PLAN_PATH = ".factory-memory/preview-plan.json";
2017
2030
  * skipped or withdrawn deploy still publishes Skipped rows.
2018
2031
  */
2019
2032
  const FACTORY_PR_STATUS_HUD_IF = "always() && github.event_name == 'pull_request' && github.event.pull_request.draft != true";
2033
+ /**
2034
+ * Least privilege for the job that runs the present step. A `permissions`
2035
+ * block zeroes every unlisted GITHUB_TOKEN scope, and the inventory list
2036
+ * reads pull requests, commits, and check-runs with that token (#825).
2037
+ */
2038
+ const FACTORY_PR_STATUS_HUD_PERMISSIONS = Object.freeze({
2039
+ checks: "read",
2040
+ contents: "read",
2041
+ "pull-requests": "read"
2042
+ });
2020
2043
  const FACTORY_PR_STATUS_HUD_CONCURRENCY = Object.freeze({
2021
2044
  cancelInProgress: false,
2022
2045
  group: `status-hud-\${{ github.repository }}-\${{ github.event.pull_request.number }}`
@@ -2040,7 +2063,8 @@ const factoryPrStatusHudWorkflow = (options) => {
2040
2063
  FACTORY_HEAD_SHA: `\${{ github.event.pull_request.head.sha }}`,
2041
2064
  FACTORY_OWNER: `\${{ github.repository_owner }}`,
2042
2065
  FACTORY_PR: `\${{ github.event.pull_request.number }}`,
2043
- FACTORY_REPO: `\${{ github.event.repository.name }}`
2066
+ FACTORY_REPO: `\${{ github.event.repository.name }}`,
2067
+ GITHUB_TOKEN: `\${{ github.token }}`
2044
2068
  }),
2045
2069
  name: "Present the PR status HUD",
2046
2070
  run: `${cli} pr:status-hud --head "$FACTORY_HEAD_SHA" --owner "$FACTORY_OWNER" --repo "$FACTORY_REPO" --pr "$FACTORY_PR"`
@@ -2050,6 +2074,7 @@ const factoryPrStatusHudWorkflow = (options) => {
2050
2074
  if: FACTORY_PR_STATUS_HUD_IF,
2051
2075
  jobId: FACTORY_PR_STATUS_HUD_JOB_ID,
2052
2076
  jobName: FACTORY_PR_STATUS_HUD_JOB_NAME,
2077
+ permissions: FACTORY_PR_STATUS_HUD_PERMISSIONS,
2053
2078
  presentSteps: Object.freeze([mintStep, presentStep])
2054
2079
  });
2055
2080
  };
@@ -3174,4 +3199,4 @@ const assertWorkflowShellParses = (yaml, options) => {
3174
3199
  throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
3175
3200
  };
3176
3201
  //#endregion
3177
- export { EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
3202
+ export { EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/factory-ci",
3
- "version": "1.0.0-alpha.19",
3
+ "version": "1.0.0-alpha.20",
4
4
  "description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
5
5
  "keywords": [
6
6
  "alchemy",
package/src/index.ts CHANGED
@@ -130,6 +130,7 @@ export {
130
130
  FACTORY_PR_STATUS_HUD_IF,
131
131
  FACTORY_PR_STATUS_HUD_JOB_ID,
132
132
  FACTORY_PR_STATUS_HUD_JOB_NAME,
133
+ FACTORY_PR_STATUS_HUD_PERMISSIONS,
133
134
  FACTORY_PR_STATUS_HUD_PLAN_PATH,
134
135
  type FactoryPrStatusHudWorkflow,
135
136
  type FactoryPrStatusHudWorkflowOptions,
@@ -22,6 +22,17 @@ export interface FactoryPrStatusHudWorkflowOptions {
22
22
  readonly createGithubAppToken: PinnedAction;
23
23
  }
24
24
 
25
+ /**
26
+ * Least privilege for the job that runs the present step. A `permissions`
27
+ * block zeroes every unlisted GITHUB_TOKEN scope, and the inventory list
28
+ * reads pull requests, commits, and check-runs with that token (#825).
29
+ */
30
+ export const FACTORY_PR_STATUS_HUD_PERMISSIONS = Object.freeze({
31
+ checks: "read",
32
+ contents: "read",
33
+ "pull-requests": "read",
34
+ } as const);
35
+
25
36
  export const FACTORY_PR_STATUS_HUD_CONCURRENCY = Object.freeze({
26
37
  cancelInProgress: false,
27
38
  group: `status-hud-\${{ github.repository }}-\${{ github.event.pull_request.number }}`,
@@ -32,6 +43,7 @@ export interface FactoryPrStatusHudWorkflow {
32
43
  readonly if: typeof FACTORY_PR_STATUS_HUD_IF;
33
44
  readonly jobId: typeof FACTORY_PR_STATUS_HUD_JOB_ID;
34
45
  readonly jobName: typeof FACTORY_PR_STATUS_HUD_JOB_NAME;
46
+ readonly permissions: typeof FACTORY_PR_STATUS_HUD_PERMISSIONS;
35
47
  readonly presentSteps: readonly WorkflowStep[];
36
48
  }
37
49
 
@@ -57,6 +69,9 @@ export const factoryPrStatusHudWorkflow = (
57
69
  "private-key": `\${{ secrets.FACTORY_GITHUB_APP_PRIVATE_KEY }}`,
58
70
  }),
59
71
  });
72
+ // The App token owns the HUD comment but has no contents permission, so
73
+ // the force-push timeline read (#819) is FORBIDDEN for it. The workflow
74
+ // token carries the job's `contents: read` and serves that read (#825).
60
75
  const presentStep: WorkflowStep = Object.freeze({
61
76
  env: Object.freeze({
62
77
  FACTORY_GITHUB_APP_TOKEN: `\${{ steps.${FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID}.outputs.token }}`,
@@ -64,6 +79,7 @@ export const factoryPrStatusHudWorkflow = (
64
79
  FACTORY_OWNER: `\${{ github.repository_owner }}`,
65
80
  FACTORY_PR: `\${{ github.event.pull_request.number }}`,
66
81
  FACTORY_REPO: `\${{ github.event.repository.name }}`,
82
+ GITHUB_TOKEN: `\${{ github.token }}`,
67
83
  }),
68
84
  name: "Present the PR status HUD",
69
85
  run: `${cli} pr:status-hud --head "$FACTORY_HEAD_SHA" --owner "$FACTORY_OWNER" --repo "$FACTORY_REPO" --pr "$FACTORY_PR"`,
@@ -74,6 +90,7 @@ export const factoryPrStatusHudWorkflow = (
74
90
  if: FACTORY_PR_STATUS_HUD_IF,
75
91
  jobId: FACTORY_PR_STATUS_HUD_JOB_ID,
76
92
  jobName: FACTORY_PR_STATUS_HUD_JOB_NAME,
93
+ permissions: FACTORY_PR_STATUS_HUD_PERMISSIONS,
77
94
  presentSteps: Object.freeze([mintStep, presentStep]),
78
95
  });
79
96
  };
@@ -74,9 +74,11 @@ export interface PreviewProofInventoryTransport {
74
74
  /** Per-request timeout; defaults to five seconds. */
75
75
  timeoutMs?: number;
76
76
  /**
77
- * Bearer token. `list` accepts a workflow `GITHUB_TOKEN` (read). `persist`
78
- * and `recordCleanup` must use a Factory App installation token: GitHub
79
- * refuses GITHUB_TOKEN writes to App-owned check-runs.
77
+ * Bearer token. `list` accepts a workflow `GITHUB_TOKEN` (read) and needs
78
+ * commit (contents) read for the force-push timeline; the contents-less
79
+ * Factory App token cannot serve it (#825). `persist` and `recordCleanup`
80
+ * must use a Factory App installation token: GitHub refuses GITHUB_TOKEN
81
+ * writes to App-owned check-runs.
80
82
  */
81
83
  token: string;
82
84
  }
@@ -361,7 +363,25 @@ const githubGraphql = async (
361
363
  throw new TypeError("GitHub GraphQL response was not an object");
362
364
  }
363
365
  if (served.errors !== undefined) {
364
- throw new Error("Preview proof inventory GraphQL query failed.");
366
+ const details = Array.isArray(served.errors)
367
+ ? served.errors
368
+ .flatMap((entry) =>
369
+ isRecord(entry)
370
+ ? [
371
+ [entry.type, entry.message]
372
+ .filter((part) => typeof part === "string")
373
+ .join(": "),
374
+ ]
375
+ : []
376
+ )
377
+ .filter((detail) => detail.length > 0)
378
+ .join("; ")
379
+ : "";
380
+ throw new Error(
381
+ details.length > 0
382
+ ? `Preview proof inventory GraphQL query failed: ${details}`
383
+ : "Preview proof inventory GraphQL query failed."
384
+ );
365
385
  }
366
386
  return served.data;
367
387
  };
@@ -636,10 +656,20 @@ const listPullRequestCommitShas = (
636
656
  "Preview proof inventory commit listing exhausted pagination before GitHub returned a final page."
637
657
  );
638
658
 
659
+ const readPullBaseSha = (pull: Record<string, unknown>): string => {
660
+ if (!isRecord(pull.base) || typeof pull.base.sha !== "string") {
661
+ throw new TypeError("GitHub pull request response omitted base.sha");
662
+ }
663
+ return pull.base.sha;
664
+ };
665
+
639
666
  /**
640
- * Current head, replaced heads from force-push, and PR commits. The
641
- * pull-commits endpoint silently stops at 250 and never lists a rebased-away
642
- * head, so those two cases use compare pagination and the force-push timeline.
667
+ * Current head, replaced heads from force-push, the commits each replaced head
668
+ * carried, and PR commits. The pull-commits endpoint silently stops at 250 and
669
+ * never lists a rebased-away head, so those two cases use compare pagination
670
+ * and the force-push timeline. A persist, then a fast-forward, then a rebase
671
+ * leaves the persist SHA behind the replaced head, so each replaced head is
672
+ * also walked back to the base (#796).
643
673
  */
644
674
  const listCommitShasForPull = async (
645
675
  request: typeof fetch,
@@ -674,16 +704,13 @@ const listCommitShasForPull = async (
674
704
  );
675
705
  let older = pullCommits;
676
706
  if (pullCommits.length >= GITHUB_PULL_COMMITS_CAP) {
677
- if (!isRecord(pull.base) || typeof pull.base.sha !== "string") {
678
- throw new TypeError("GitHub pull request response omitted base.sha");
679
- }
680
707
  older = await listCompareCommitShas(
681
708
  request,
682
709
  token,
683
710
  timeoutMs,
684
711
  owner,
685
712
  repo,
686
- pull.base.sha,
713
+ readPullBaseSha(pull),
687
714
  pull.head.sha
688
715
  );
689
716
  }
@@ -697,9 +724,34 @@ const listCommitShasForPull = async (
697
724
  pr
698
725
  );
699
726
 
727
+ let replaced: readonly string[] = [];
728
+ if (previousHeads.length > 0) {
729
+ const baseSha = readPullBaseSha(pull);
730
+ const histories = await mapPool(
731
+ previousHeads,
732
+ PREVIEW_PROOF_LIST_CONCURRENCY,
733
+ (previousHead) =>
734
+ listCompareCommitShas(
735
+ request,
736
+ token,
737
+ timeoutMs,
738
+ owner,
739
+ repo,
740
+ baseSha,
741
+ previousHead
742
+ )
743
+ );
744
+ replaced = histories.flatMap((history) => [...history].toReversed());
745
+ }
746
+
700
747
  const shas: string[] = [];
701
748
  const seen = new Set<string>();
702
- for (const sha of [pull.head.sha, ...previousHeads, ...older.toReversed()]) {
749
+ for (const sha of [
750
+ pull.head.sha,
751
+ ...previousHeads,
752
+ ...older.toReversed(),
753
+ ...replaced,
754
+ ]) {
703
755
  if (!seen.has(sha)) {
704
756
  seen.add(sha);
705
757
  shas.push(sha);