@patronage/factory-ci 1.0.0-alpha.18 → 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
  }
@@ -904,6 +906,45 @@ interface FactoryLifecycleContractLane {
904
906
  */
905
907
  declare const factoryLifecycleContractLane: (declaration?: FactoryLifecycleContractLaneOptions) => FactoryLifecycleContractLane | undefined;
906
908
  //#endregion
909
+ //#region src/pr-status-hud-workflow.d.ts
910
+ declare const FACTORY_PR_STATUS_HUD_JOB_ID = "status-hud";
911
+ declare const FACTORY_PR_STATUS_HUD_JOB_NAME = "Present PR status HUD";
912
+ declare const FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID = "factory-app-token";
913
+ declare const FACTORY_PR_STATUS_HUD_PLAN_PATH = ".factory-memory/preview-plan.json";
914
+ /**
915
+ * Presentation is independent of deploy demand. Callers who `needs` the jobs
916
+ * that settle HUD sources (verify, deploy) must keep this `always()` so a
917
+ * skipped or withdrawn deploy still publishes Skipped rows.
918
+ */
919
+ declare const FACTORY_PR_STATUS_HUD_IF = "always() && github.event_name == 'pull_request' && github.event.pull_request.draft != true";
920
+ interface FactoryPrStatusHudWorkflowOptions {
921
+ readonly cli?: string;
922
+ readonly createGithubAppToken: PinnedAction;
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
+ }>;
934
+ declare const FACTORY_PR_STATUS_HUD_CONCURRENCY: Readonly<{
935
+ cancelInProgress: false;
936
+ group: "status-hud-${{ github.repository }}-${{ github.event.pull_request.number }}";
937
+ }>;
938
+ interface FactoryPrStatusHudWorkflow {
939
+ readonly concurrency: typeof FACTORY_PR_STATUS_HUD_CONCURRENCY;
940
+ readonly if: typeof FACTORY_PR_STATUS_HUD_IF;
941
+ readonly jobId: typeof FACTORY_PR_STATUS_HUD_JOB_ID;
942
+ readonly jobName: typeof FACTORY_PR_STATUS_HUD_JOB_NAME;
943
+ readonly permissions: typeof FACTORY_PR_STATUS_HUD_PERMISSIONS;
944
+ readonly presentSteps: readonly WorkflowStep[];
945
+ }
946
+ declare const factoryPrStatusHudWorkflow: (options: FactoryPrStatusHudWorkflowOptions) => FactoryPrStatusHudWorkflow;
947
+ //#endregion
907
948
  //#region src/preview-cleanup-topology.d.ts
908
949
  declare const FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID = "stage-residue-audit";
909
950
  declare const FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME = "Stage residue audit";
@@ -1301,4 +1342,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
1301
1342
  readonly source: string;
1302
1343
  }) => void;
1303
1344
  //#endregion
1304
- 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_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, 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, 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);
@@ -2006,6 +2019,66 @@ const factoryLifecycleContractLane = (declaration) => {
2006
2019
  });
2007
2020
  };
2008
2021
  //#endregion
2022
+ //#region src/pr-status-hud-workflow.ts
2023
+ const FACTORY_PR_STATUS_HUD_JOB_ID = "status-hud";
2024
+ const FACTORY_PR_STATUS_HUD_JOB_NAME = "Present PR status HUD";
2025
+ const FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID = "factory-app-token";
2026
+ const FACTORY_PR_STATUS_HUD_PLAN_PATH = ".factory-memory/preview-plan.json";
2027
+ /**
2028
+ * Presentation is independent of deploy demand. Callers who `needs` the jobs
2029
+ * that settle HUD sources (verify, deploy) must keep this `always()` so a
2030
+ * skipped or withdrawn deploy still publishes Skipped rows.
2031
+ */
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
+ });
2043
+ const FACTORY_PR_STATUS_HUD_CONCURRENCY = Object.freeze({
2044
+ cancelInProgress: false,
2045
+ group: `status-hud-\${{ github.repository }}-\${{ github.event.pull_request.number }}`
2046
+ });
2047
+ const defaultCli = "psf";
2048
+ const factoryPrStatusHudWorkflow = (options) => {
2049
+ assertPinnedAction("createGithubAppToken", options.createGithubAppToken, "actions/create-github-app-token");
2050
+ const cli = options.cli || defaultCli;
2051
+ const mintStep = Object.freeze({
2052
+ id: FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID,
2053
+ name: "Mint the factory App token",
2054
+ uses: options.createGithubAppToken.uses,
2055
+ with: Object.freeze({
2056
+ "client-id": FACTORY_PROOF_GATE_APP_ID,
2057
+ "private-key": `\${{ secrets.FACTORY_GITHUB_APP_PRIVATE_KEY }}`
2058
+ })
2059
+ });
2060
+ const presentStep = Object.freeze({
2061
+ env: Object.freeze({
2062
+ FACTORY_GITHUB_APP_TOKEN: `\${{ steps.${FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID}.outputs.token }}`,
2063
+ FACTORY_HEAD_SHA: `\${{ github.event.pull_request.head.sha }}`,
2064
+ FACTORY_OWNER: `\${{ github.repository_owner }}`,
2065
+ FACTORY_PR: `\${{ github.event.pull_request.number }}`,
2066
+ FACTORY_REPO: `\${{ github.event.repository.name }}`,
2067
+ GITHUB_TOKEN: `\${{ github.token }}`
2068
+ }),
2069
+ name: "Present the PR status HUD",
2070
+ run: `${cli} pr:status-hud --head "$FACTORY_HEAD_SHA" --owner "$FACTORY_OWNER" --repo "$FACTORY_REPO" --pr "$FACTORY_PR"`
2071
+ });
2072
+ return Object.freeze({
2073
+ concurrency: FACTORY_PR_STATUS_HUD_CONCURRENCY,
2074
+ if: FACTORY_PR_STATUS_HUD_IF,
2075
+ jobId: FACTORY_PR_STATUS_HUD_JOB_ID,
2076
+ jobName: FACTORY_PR_STATUS_HUD_JOB_NAME,
2077
+ permissions: FACTORY_PR_STATUS_HUD_PERMISSIONS,
2078
+ presentSteps: Object.freeze([mintStep, presentStep])
2079
+ });
2080
+ };
2081
+ //#endregion
2009
2082
  //#region src/preview-cleanup-topology.ts
2010
2083
  const FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID = "stage-residue-audit";
2011
2084
  const FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME = "Stage residue audit";
@@ -3126,4 +3199,4 @@ const assertWorkflowShellParses = (yaml, options) => {
3126
3199
  throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
3127
3200
  };
3128
3201
  //#endregion
3129
- 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_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, 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.18",
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
@@ -124,6 +124,18 @@ export {
124
124
  type FactoryLifecycleContractLaneOptions,
125
125
  factoryLifecycleContractLane,
126
126
  } from "./lifecycle-contract-lane.ts";
127
+ export {
128
+ FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID,
129
+ FACTORY_PR_STATUS_HUD_CONCURRENCY,
130
+ FACTORY_PR_STATUS_HUD_IF,
131
+ FACTORY_PR_STATUS_HUD_JOB_ID,
132
+ FACTORY_PR_STATUS_HUD_JOB_NAME,
133
+ FACTORY_PR_STATUS_HUD_PERMISSIONS,
134
+ FACTORY_PR_STATUS_HUD_PLAN_PATH,
135
+ type FactoryPrStatusHudWorkflow,
136
+ type FactoryPrStatusHudWorkflowOptions,
137
+ factoryPrStatusHudWorkflow,
138
+ } from "./pr-status-hud-workflow.ts";
127
139
  export {
128
140
  FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID,
129
141
  FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME,
@@ -0,0 +1,96 @@
1
+ import type { PinnedAction } from "./actions.ts";
2
+ import type { WorkflowStep } from "./factory-workflow.ts";
3
+ import { assertPinnedAction } from "./pinned-action.ts";
4
+ import { FACTORY_PROOF_GATE_APP_ID } from "./proof-reuse-gate.ts";
5
+
6
+ export const FACTORY_PR_STATUS_HUD_JOB_ID = "status-hud";
7
+ export const FACTORY_PR_STATUS_HUD_JOB_NAME = "Present PR status HUD";
8
+ export const FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID = "factory-app-token";
9
+ export const FACTORY_PR_STATUS_HUD_PLAN_PATH =
10
+ ".factory-memory/preview-plan.json";
11
+
12
+ /**
13
+ * Presentation is independent of deploy demand. Callers who `needs` the jobs
14
+ * that settle HUD sources (verify, deploy) must keep this `always()` so a
15
+ * skipped or withdrawn deploy still publishes Skipped rows.
16
+ */
17
+ export const FACTORY_PR_STATUS_HUD_IF =
18
+ "always() && github.event_name == 'pull_request' && github.event.pull_request.draft != true";
19
+
20
+ export interface FactoryPrStatusHudWorkflowOptions {
21
+ readonly cli?: string;
22
+ readonly createGithubAppToken: PinnedAction;
23
+ }
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
+
36
+ export const FACTORY_PR_STATUS_HUD_CONCURRENCY = Object.freeze({
37
+ cancelInProgress: false,
38
+ group: `status-hud-\${{ github.repository }}-\${{ github.event.pull_request.number }}`,
39
+ });
40
+
41
+ export interface FactoryPrStatusHudWorkflow {
42
+ readonly concurrency: typeof FACTORY_PR_STATUS_HUD_CONCURRENCY;
43
+ readonly if: typeof FACTORY_PR_STATUS_HUD_IF;
44
+ readonly jobId: typeof FACTORY_PR_STATUS_HUD_JOB_ID;
45
+ readonly jobName: typeof FACTORY_PR_STATUS_HUD_JOB_NAME;
46
+ readonly permissions: typeof FACTORY_PR_STATUS_HUD_PERMISSIONS;
47
+ readonly presentSteps: readonly WorkflowStep[];
48
+ }
49
+
50
+ const defaultCli = "psf";
51
+
52
+ export const factoryPrStatusHudWorkflow = (
53
+ options: FactoryPrStatusHudWorkflowOptions
54
+ ): FactoryPrStatusHudWorkflow => {
55
+ assertPinnedAction(
56
+ "createGithubAppToken",
57
+ options.createGithubAppToken,
58
+ "actions/create-github-app-token"
59
+ );
60
+ const cli = options.cli || defaultCli;
61
+ // Same unscoped mint as the freeze job. A subset (checks/issues/PRs)
62
+ // 422s: those permissions are not granted to this installation.
63
+ const mintStep: WorkflowStep = Object.freeze({
64
+ id: FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID,
65
+ name: "Mint the factory App token",
66
+ uses: options.createGithubAppToken.uses,
67
+ with: Object.freeze({
68
+ "client-id": FACTORY_PROOF_GATE_APP_ID,
69
+ "private-key": `\${{ secrets.FACTORY_GITHUB_APP_PRIVATE_KEY }}`,
70
+ }),
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).
75
+ const presentStep: WorkflowStep = Object.freeze({
76
+ env: Object.freeze({
77
+ FACTORY_GITHUB_APP_TOKEN: `\${{ steps.${FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID}.outputs.token }}`,
78
+ FACTORY_HEAD_SHA: `\${{ github.event.pull_request.head.sha }}`,
79
+ FACTORY_OWNER: `\${{ github.repository_owner }}`,
80
+ FACTORY_PR: `\${{ github.event.pull_request.number }}`,
81
+ FACTORY_REPO: `\${{ github.event.repository.name }}`,
82
+ GITHUB_TOKEN: `\${{ github.token }}`,
83
+ }),
84
+ name: "Present the PR status HUD",
85
+ run: `${cli} pr:status-hud --head "$FACTORY_HEAD_SHA" --owner "$FACTORY_OWNER" --repo "$FACTORY_REPO" --pr "$FACTORY_PR"`,
86
+ });
87
+
88
+ return Object.freeze({
89
+ concurrency: FACTORY_PR_STATUS_HUD_CONCURRENCY,
90
+ if: FACTORY_PR_STATUS_HUD_IF,
91
+ jobId: FACTORY_PR_STATUS_HUD_JOB_ID,
92
+ jobName: FACTORY_PR_STATUS_HUD_JOB_NAME,
93
+ permissions: FACTORY_PR_STATUS_HUD_PERMISSIONS,
94
+ presentSteps: Object.freeze([mintStep, presentStep]),
95
+ });
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);