@async/pipeline 0.9.12 → 0.9.13

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.
@@ -20,8 +20,8 @@ function defineActionRef(id, uses, sha, label) {
20
20
  ref: `${uses}@${sha} # ${label}`
21
21
  };
22
22
  }
23
- const ASYNC_ACTIONS_SHA = "c08a62380ee60fa175d5c3598d41c4485c0ead98";
24
- const ASYNC_ACTIONS_LABEL = "v0.1.12";
23
+ const ASYNC_ACTIONS_SHA = "87e033782ca1f84334d9e3a2543b0db064848fb7";
24
+ const ASYNC_ACTIONS_LABEL = "v0.1.14";
25
25
  const ASYNC_RELEASE_COMMAND = "npx --yes github:async/release#3892d94a4890600d26b812052aa58dec98b05bfb";
26
26
  const GENERATED_ACTIONS = [
27
27
  defineActionRef("async.actions.setup", "async/actions/setup", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
@@ -37,6 +37,7 @@ const GENERATED_ACTIONS = [
37
37
  defineActionRef("async.actions.source-impact", "async/actions/source-impact", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
38
38
  defineActionRef("async.actions.cache", "async/actions/cache", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
39
39
  defineActionRef("async.actions.attest", "async/actions/attest", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
40
+ defineActionRef("async.actions.contract", "async/actions/contract", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
40
41
  defineActionRef("actions.checkout", "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd", "v6.0.2"),
41
42
  defineActionRef("pnpm.setup", "pnpm/setup", "cf03a9b516e09bc5a90f041fc26fc930c9dc631b", "v1.0.0"),
42
43
  defineActionRef("deno.setup", "denoland/setup-deno", "667a34cdef165d8d2b2e98dde39547c9daac7282", "v2.0.4"),
@@ -59,6 +60,7 @@ const ASYNC_AGENT_EVIDENCE_ACTION = actionRef("async.actions.agent-evidence");
59
60
  const ASYNC_SOURCE_IMPACT_ACTION = actionRef("async.actions.source-impact");
60
61
  const ASYNC_CACHE_ACTION = actionRef("async.actions.cache");
61
62
  const ASYNC_ATTEST_ACTION = actionRef("async.actions.attest");
63
+ const ASYNC_CONTRACT_ACTION = actionRef("async.actions.contract");
62
64
  const CHECKOUT_ACTION = actionRef("actions.checkout");
63
65
  const PNPM_SETUP_ACTION = actionRef("pnpm.setup");
64
66
  const DENO_SETUP_ACTION = actionRef("deno.setup");
@@ -103,6 +105,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
103
105
  evidence: renderModel.evidence,
104
106
  sourceImpact: renderModel.sourceImpact,
105
107
  attest: renderModel.attest,
108
+ contract: renderModel.contract,
106
109
  bridge: renderModel.bridge,
107
110
  pages: renderModel.pages,
108
111
  manualDispatchJobs: renderModel.manualDispatchJobs,
@@ -133,6 +136,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
133
136
  evidence: renderModel.evidence,
134
137
  sourceImpact: renderModel.sourceImpact,
135
138
  attest: renderModel.attest,
139
+ contract: renderModel.contract,
136
140
  bridge: renderModel.bridge,
137
141
  pages: renderModel.pages,
138
142
  manualDispatchJobs: renderModel.manualDispatchJobs
@@ -458,6 +462,14 @@ function buildGeneratedJobManifests(model, rendered, event, network, selectedPip
458
462
  skipReason: selected ? "" : "event_filter"
459
463
  });
460
464
  }
465
+ if (model.contract.enabled) {
466
+ const selected = contractSelected(model.contract, event);
467
+ candidates.push({
468
+ manifest: buildContractManifest(model, rendered, event, network),
469
+ selected,
470
+ skipReason: selected ? "" : skipReasonForGeneratedJob(event, [model.contract.job])
471
+ });
472
+ }
461
473
  if (model.dependabotAutoMerge.enabled) {
462
474
  const selected = event.name === "pull_request_target";
463
475
  candidates.push({
@@ -705,13 +717,14 @@ function agentEvidenceManifestSteps(model, job) {
705
717
  }
706
718
  return steps;
707
719
  }
708
- function evidenceCollectManifestSteps(model) {
720
+ function evidenceCollectManifestSteps(model, options = {}) {
709
721
  if (!model.evidence.enabled)
710
722
  return [];
723
+ const paths = [...new Set([...model.evidence.paths, ...(options.extraPaths ?? [])])];
711
724
  return [
712
725
  actionManifestStep("collect-evidence-manifest", "Collect evidence manifest", ASYNC_EVIDENCE_ACTION, {
713
726
  mode: "collect",
714
- paths: model.evidence.paths,
727
+ paths,
715
728
  "receipt-paths": model.evidence.receiptPaths,
716
729
  "manifest-path": ".async/evidence/${{ github.job }}/manifest.json",
717
730
  "summary-path": ".async/evidence/${{ github.job }}/summary.md",
@@ -722,6 +735,32 @@ function evidenceCollectManifestSteps(model) {
722
735
  }, "evidence", { if: "${{ always() }}" })
723
736
  ];
724
737
  }
738
+ function contractActionInput(contract) {
739
+ return sortObject({
740
+ mode: contract.mode,
741
+ checks: contractChecks(contract).join(","),
742
+ "package-path": contract.packagePath,
743
+ ...(contract.schema.enabled
744
+ ? {
745
+ "schema-sources": contract.schema.sources.join("\n"),
746
+ "schema-output": contract.schema.output
747
+ }
748
+ : {}),
749
+ "evidence-dir": contract.evidenceDir,
750
+ annotations: contract.annotations,
751
+ "fail-on": contract.mode === "report" ? "advisory" : "blocking"
752
+ });
753
+ }
754
+ function contractChecks(contract) {
755
+ const checks = [];
756
+ if (contract.api)
757
+ checks.push("api");
758
+ if (contract.claims)
759
+ checks.push("claims");
760
+ if (contract.schema.enabled)
761
+ checks.push("schema");
762
+ return checks;
763
+ }
725
764
  function runActionManifestStep(id, name, command, env, contract, extraWith = {}) {
726
765
  const networked = commandLooksNetworked(command);
727
766
  return actionManifestStep(id, name, ASYNC_RUN_ACTION, {
@@ -876,6 +915,8 @@ function artifactPathForContract(contract) {
876
915
  return ".async/evidence";
877
916
  if (contract === "agent-evidence")
878
917
  return ".async/actions/agent-evidence";
918
+ if (contract === "contract")
919
+ return ".async/contract";
879
920
  if (contract === "pages")
880
921
  return ".async/pages";
881
922
  return ".async/runs";
@@ -983,6 +1024,30 @@ function buildBridgeManifest(model, rendered, event, network) {
983
1024
  network
984
1025
  });
985
1026
  }
1027
+ function buildContractManifest(model, rendered, event, network) {
1028
+ const contract = model.contract;
1029
+ const steps = [
1030
+ checkoutStep(),
1031
+ ...setupManifestSteps(model),
1032
+ ...dependencyInstallManifestSteps(model),
1033
+ ...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
1034
+ actionManifestStep("run-contract-evidence", "Run contract evidence", ASYNC_CONTRACT_ACTION, contractActionInput(contract), "contract"),
1035
+ ...evidenceCollectManifestSteps(model, { extraPaths: [contract.evidenceDir] })
1036
+ ];
1037
+ return makeJobManifest(model, rendered, event, {
1038
+ id: contract.job,
1039
+ kind: "generated",
1040
+ target: [],
1041
+ runsOn: "ubuntu-latest",
1042
+ permissions: { contents: "read" },
1043
+ environment: null,
1044
+ concurrency: null,
1045
+ if: renderContractCondition(contract),
1046
+ trigger: contract.mode === "release" ? ["release", "workflow_dispatch"] : ["pull_request", "workflow_dispatch"],
1047
+ steps,
1048
+ network
1049
+ });
1050
+ }
986
1051
  function buildEvidenceFanInManifest(model, rendered, event, network) {
987
1052
  return makeJobManifest(model, rendered, event, {
988
1053
  id: model.evidence.job,
@@ -1088,6 +1153,14 @@ function bridgeSelected(bridge, event) {
1088
1153
  return Boolean(bridge.schedule && (!event.schedule || event.schedule === bridge.schedule));
1089
1154
  return event.name === "workflow_dispatch" && event.selectedJob === bridge.job;
1090
1155
  }
1156
+ function contractSelected(contract, event) {
1157
+ if (event.name === "workflow_dispatch")
1158
+ return event.selectedJob === contract.job;
1159
+ if (contract.mode === "release") {
1160
+ return event.name === "release" && (!event.action || event.action === "published");
1161
+ }
1162
+ return event.name === "pull_request";
1163
+ }
1091
1164
  function skipReasonForJob(event, trigger) {
1092
1165
  if (event.name === "workflow_dispatch" && !event.selectedJob && trigger.some((id) => id === "manual"))
1093
1166
  return "manual_selector_missing";
@@ -1142,6 +1215,7 @@ function buildRenderModel(pipeline, options) {
1142
1215
  const evidence = resolveGitHubEvidence(pipeline);
1143
1216
  const pages = resolveGitHubPages(pipeline);
1144
1217
  const bridge = resolveGitHubBridge(pipeline);
1218
+ const contract = resolveGitHubContract(pipeline, { pages, bridge });
1145
1219
  if (pages.enabled) {
1146
1220
  if (pages.triggers.pullRequest) {
1147
1221
  addGitHubEventTrigger(triggers, "pull_request");
@@ -1153,6 +1227,14 @@ function buildRenderModel(pipeline, options) {
1153
1227
  if (bridge.actionsJob.scheduled && bridge.schedule) {
1154
1228
  addScheduleTrigger(triggers, bridge.schedule, "async-bridge");
1155
1229
  }
1230
+ if (contract.enabled) {
1231
+ if (contract.mode === "release") {
1232
+ addReleasePublishedTrigger(triggers);
1233
+ }
1234
+ else {
1235
+ addPullRequestTrigger(triggers, "pull_request");
1236
+ }
1237
+ }
1156
1238
  const manualDispatchJobs = Object.values(pipeline.jobs)
1157
1239
  .filter((job) => job.trigger.some((triggerId) => pipeline.triggers[triggerId]?.type === "manual"))
1158
1240
  .map((job) => job.id)
@@ -1165,6 +1247,10 @@ function buildRenderModel(pipeline, options) {
1165
1247
  manualDispatchJobs.push(bridge.job);
1166
1248
  manualDispatchJobs.sort((left, right) => left.localeCompare(right));
1167
1249
  }
1250
+ if (contract.enabled) {
1251
+ manualDispatchJobs.push(contract.job);
1252
+ manualDispatchJobs.sort((left, right) => left.localeCompare(right));
1253
+ }
1168
1254
  const nodeVersion = pipeline.sync.github.nodeVersion ?? DEFAULT_NODE_VERSION;
1169
1255
  const runtime = resolveRuntimeSpecs(pipeline.sync.github.runtime, options.projectKind, nodeVersion);
1170
1256
  const setup = resolveGitHubSetup(pipeline.sync.github.setup, options.packageManager, options.packageManagerVersion);
@@ -1216,6 +1302,7 @@ function buildRenderModel(pipeline, options) {
1216
1302
  evidence,
1217
1303
  sourceImpact,
1218
1304
  attest: pipeline.sync.github.attest,
1305
+ contract,
1219
1306
  bridge,
1220
1307
  pages,
1221
1308
  manualDispatchJobs
@@ -1234,6 +1321,30 @@ function resolveGitHubEvidence(pipeline) {
1234
1321
  }
1235
1322
  return config;
1236
1323
  }
1324
+ function resolveGitHubContract(pipeline, generated) {
1325
+ const config = pipeline.sync.github.contract;
1326
+ if (!config.enabled)
1327
+ return config;
1328
+ const jobId = config.job.toLowerCase();
1329
+ if (Object.keys(pipeline.jobs).some((id) => id.toLowerCase() === jobId)) {
1330
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_CONTRACT_JOB_CONFLICT", `sync.github.contract.job "${config.job}" conflicts with an existing pipeline job. Remove the explicit job or set sync.github.contract.job to a different id.`);
1331
+ }
1332
+ const generatedJobs = new Set();
1333
+ if (pipeline.sync.github.packagePreviews.enabled)
1334
+ generatedJobs.add("package-preview");
1335
+ if (pipeline.sync.github.dependabotAutoMerge.enabled)
1336
+ generatedJobs.add("dependabot-auto-merge");
1337
+ if (pipeline.sync.github.evidence.enabled)
1338
+ generatedJobs.add(pipeline.sync.github.evidence.job);
1339
+ if (generated.pages.enabled)
1340
+ generatedJobs.add(generated.pages.job);
1341
+ if (generated.bridge.actionsJob.enabled)
1342
+ generatedJobs.add(generated.bridge.job);
1343
+ if ([...generatedJobs].some((id) => id.toLowerCase() === jobId)) {
1344
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_CONTRACT_JOB_CONFLICT", `sync.github.contract.job "${config.job}" conflicts with a generated GitHub job. Set sync.github.contract.job to a different id.`);
1345
+ }
1346
+ return config;
1347
+ }
1237
1348
  function resolveGitHubSourceImpactJobs(pipeline, cwd, jobs) {
1238
1349
  const config = pipeline.sync.github.sourceImpact;
1239
1350
  if (!config.enabled)
@@ -1246,8 +1357,9 @@ function resolveGitHubSourceImpactJobs(pipeline, cwd, jobs) {
1246
1357
  "dependabot-auto-merge",
1247
1358
  "async-bridge",
1248
1359
  pipeline.sync.github.evidence.job,
1360
+ pipeline.sync.github.contract.enabled ? pipeline.sync.github.contract.job : "",
1249
1361
  pipeline.sync.github.pages.job
1250
- ].map((id) => id.toLowerCase()));
1362
+ ].filter(Boolean).map((id) => id.toLowerCase()));
1251
1363
  const existingJobIds = new Set(Object.keys(pipeline.jobs).map((id) => id.toLowerCase()));
1252
1364
  const result = [];
1253
1365
  for (const jobId of selectedJobIds) {
@@ -1344,6 +1456,16 @@ function addPullRequestTrigger(triggers, event) {
1344
1456
  types: [...new Set([...existingTypes, "opened", "reopened", "synchronize", "ready_for_review"])].sort()
1345
1457
  });
1346
1458
  }
1459
+ function addReleasePublishedTrigger(triggers) {
1460
+ const existing = triggers.release && typeof triggers.release === "object" && !Array.isArray(triggers.release)
1461
+ ? triggers.release
1462
+ : {};
1463
+ const existingTypes = Array.isArray(existing.types) ? existing.types.filter((value) => typeof value === "string") : [];
1464
+ triggers.release = sortObject({
1465
+ ...existing,
1466
+ types: [...new Set([...existingTypes, "published"])].sort()
1467
+ });
1468
+ }
1347
1469
  function addGitHubEventTrigger(triggers, event) {
1348
1470
  if (triggers[event] === undefined) {
1349
1471
  triggers[event] = {};
@@ -1468,6 +1590,9 @@ function renderWorkflow(model) {
1468
1590
  if (model.bridge.actionsJob.enabled) {
1469
1591
  renderBridgeJob(lines, model);
1470
1592
  }
1593
+ if (model.contract.enabled) {
1594
+ renderContractJob(lines, model);
1595
+ }
1471
1596
  if (model.evidence.enabled) {
1472
1597
  renderEvidenceFanInJob(lines, model);
1473
1598
  }
@@ -1651,7 +1776,8 @@ function renderEvidenceCollectStep(lines, model, options = {}) {
1651
1776
  if (!model.evidence.enabled)
1652
1777
  return;
1653
1778
  const suffix = options.matrix ? "${{ github.job }}-${{ strategy.job-index }}" : "${{ github.job }}";
1654
- lines.push("", " - name: Collect evidence manifest", " if: ${{ always() }}", ` uses: ${ASYNC_EVIDENCE_ACTION}`, " with:", " mode: collect", " paths: |", ...model.evidence.paths.map((path) => ` ${path}`), ...(model.evidence.receiptPaths.length > 0
1779
+ const paths = [...new Set([...model.evidence.paths, ...(options.extraPaths ?? [])])];
1780
+ lines.push("", " - name: Collect evidence manifest", " if: ${{ always() }}", ` uses: ${ASYNC_EVIDENCE_ACTION}`, " with:", " mode: collect", " paths: |", ...paths.map((path) => ` ${path}`), ...(model.evidence.receiptPaths.length > 0
1655
1781
  ? [
1656
1782
  " receipt-paths: |",
1657
1783
  ...model.evidence.receiptPaths.map((path) => ` ${path}`)
@@ -2058,6 +2184,32 @@ function renderBridgePullStep(lines, bridge) {
2058
2184
  ].map(shellWord).join(" ");
2059
2185
  lines.push("", " - name: Pull and apply Async bridge change sets", ` uses: ${ASYNC_RUN_ACTION}`, " with:", ` command: ${JSON.stringify(command)}`, " check-generated: false", " artifact-name: async-bridge-${{ github.run_id }}", " env:", " CI: true", ` ASYNC_PROJECT_URL: \${{ vars.${bridge.endpointVar} }}`, ` ASYNC_PROJECT_TOKEN: \${{ secrets.${bridge.tokenEnv} }}`, " GITHUB_REPOSITORY: ${{ github.repository }}", " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}");
2060
2186
  }
2187
+ function renderContractJob(lines, model) {
2188
+ const contract = model.contract;
2189
+ lines.push(` ${yamlKey(contract.job)}:`, ` name: ${contract.job}`, ` if: ${renderContractCondition(contract)}`, " runs-on: ubuntu-latest", " permissions:", " contents: read", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
2190
+ if (model.buildCommand) {
2191
+ lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
2192
+ }
2193
+ renderContractActionStep(lines, contract);
2194
+ renderEvidenceCollectStep(lines, model, { extraPaths: [contract.evidenceDir] });
2195
+ lines.push("");
2196
+ }
2197
+ function renderContractActionStep(lines, contract) {
2198
+ lines.push("", " - name: Run contract evidence", ` uses: ${ASYNC_CONTRACT_ACTION}`, " with:", ` mode: ${contract.mode}`, ` checks: ${JSON.stringify(contractChecks(contract).join(","))}`, ` package-path: ${JSON.stringify(contract.packagePath)}`, ...(contract.schema.enabled
2199
+ ? [
2200
+ " schema-sources: |",
2201
+ ...contract.schema.sources.map((source) => ` ${source}`),
2202
+ ` schema-output: ${JSON.stringify(contract.schema.output)}`
2203
+ ]
2204
+ : []), ` evidence-dir: ${JSON.stringify(contract.evidenceDir)}`, ` annotations: ${contract.annotations ? "true" : "false"}`, ` fail-on: ${contract.mode === "report" ? "advisory" : "blocking"}`);
2205
+ }
2206
+ function renderContractCondition(contract) {
2207
+ const manual = `github.event_name == 'workflow_dispatch' && github.event.inputs.job == '${escapeExpressionString(contract.job)}'`;
2208
+ if (contract.mode === "release") {
2209
+ return `(github.event_name == 'release' && github.event.action == 'published') || (${manual})`;
2210
+ }
2211
+ return `(github.event_name == 'pull_request' && github.event.pull_request.draft == false) || (${manual})`;
2212
+ }
2061
2213
  function renderEvidenceFanInJob(lines, model) {
2062
2214
  const needs = evidenceProducerJobIds(model);
2063
2215
  if (needs.length === 0)
@@ -2077,6 +2229,8 @@ function evidenceProducerJobIds(model) {
2077
2229
  ids.add("package-preview");
2078
2230
  if (model.bridge.actionsJob.enabled)
2079
2231
  ids.add(model.bridge.job);
2232
+ if (model.contract.enabled)
2233
+ ids.add(model.contract.job);
2080
2234
  ids.delete(model.evidence.job);
2081
2235
  return [...ids].sort((left, right) => left.localeCompare(right));
2082
2236
  }