@crossplatformai/dependency-graph 0.10.0 → 0.11.0-next.1

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.
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type WorkflowValidationResult } from '../index';
3
+ export declare function formatWorkflowPathDrift(validationResults: WorkflowValidationResult[]): string[];
2
4
  export declare function runPrPreview(): Promise<void>;
3
5
  //# sourceMappingURL=pr-preview.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-preview.d.ts","sourceRoot":"","sources":["../../src/cli/pr-preview.ts"],"names":[],"mappings":";AAoLA,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA6MlD"}
1
+ {"version":3,"file":"pr-preview.d.ts","sourceRoot":"","sources":["../../src/cli/pr-preview.ts"],"names":[],"mappings":";AAQA,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,UAAU,CAAC;AA0DlB,wBAAgB,uBAAuB,CACrC,iBAAiB,EAAE,wBAAwB,EAAE,GAC5C,MAAM,EAAE,CAyBV;AAsGD,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA4NlD"}
package/dist/index-cli.js CHANGED
@@ -208,12 +208,6 @@ var defaultWorkflowValidationPolicy = {
208
208
  includeDevDependenciesTransitively: false
209
209
  };
210
210
 
211
- // src/workflow/validator.ts
212
- import { existsSync as existsSync4 } from "fs";
213
- import { readFile } from "fs/promises";
214
- import { glob } from "glob";
215
- import { parse as parseYaml2 } from "yaml";
216
-
217
211
  // src/workflow/discovery.ts
218
212
  import { existsSync as existsSync2, readdirSync } from "fs";
219
213
  import { join as join3 } from "path";
@@ -333,11 +327,69 @@ function getExpectedWorkflowPaths({
333
327
  return uniqueSorted(expectedPaths);
334
328
  }
335
329
 
330
+ // src/workflow/impact.ts
331
+ function uniqueSorted2(values) {
332
+ return Array.from(new Set(values)).sort();
333
+ }
334
+ function mergeWorkflowPolicy(policyOverrides) {
335
+ return {
336
+ ...defaultWorkflowValidationPolicy,
337
+ ...policyOverrides,
338
+ allowedRootPaths: uniqueSorted2([
339
+ ...defaultWorkflowValidationPolicy.allowedRootPaths,
340
+ ...policyOverrides?.allowedRootPaths ?? []
341
+ ])
342
+ };
343
+ }
344
+ function matchesWorkflowPathFilter(changedPath, workflowPathFilter) {
345
+ if (workflowPathFilter === changedPath) {
346
+ return true;
347
+ }
348
+ if (!workflowPathFilter.endsWith("/**")) {
349
+ return false;
350
+ }
351
+ const workflowPrefix = workflowPathFilter.slice(0, -3);
352
+ return changedPath.startsWith(`${workflowPrefix}/`);
353
+ }
354
+ function getWorkflowImpacts(rootDir, packages, changedPaths, policyOverrides) {
355
+ const policy = mergeWorkflowPolicy(policyOverrides);
356
+ const workflowTargets = discoverWorkflowTargets(rootDir, packages, policy);
357
+ const { packageMap } = buildPackageMap(packages, rootDir);
358
+ return workflowTargets.map((workflowTarget) => {
359
+ const calculatedPaths = uniqueSorted2([
360
+ ...getExpectedWorkflowPaths({
361
+ workflowTarget,
362
+ packages,
363
+ packageMap,
364
+ policy
365
+ }),
366
+ ...policy.allowedRootPaths,
367
+ workflowTarget.workflowPath
368
+ ]);
369
+ const matchedPaths = calculatedPaths.filter(
370
+ (calculatedPath) => changedPaths.some(
371
+ (changedPath) => matchesWorkflowPathFilter(changedPath, calculatedPath)
372
+ )
373
+ );
374
+ return {
375
+ ...workflowTarget,
376
+ matchedPaths: uniqueSorted2(matchedPaths)
377
+ };
378
+ }).filter((workflowImpact) => workflowImpact.matchedPaths.length > 0);
379
+ }
380
+
381
+ // src/workflow/validator.ts
382
+ import { existsSync as existsSync4 } from "fs";
383
+ import { readFile } from "fs/promises";
384
+ import { join as join5 } from "path";
385
+ import { glob } from "glob";
386
+ import { parse as parseYaml2 } from "yaml";
387
+
336
388
  // src/workflow/parser.ts
337
389
  import { existsSync as existsSync3, readFileSync } from "fs";
338
390
  import { join as join4 } from "path";
339
391
  import { parse as parseYaml } from "yaml";
340
- function uniqueSorted2(values) {
392
+ function uniqueSorted3(values) {
341
393
  return Array.from(new Set(values)).sort();
342
394
  }
343
395
  function parseWorkflowFile(workflowFile, rootDir) {
@@ -350,9 +402,9 @@ function parseWorkflowFile(workflowFile, rootDir) {
350
402
  const pushPaths = workflow.on?.push?.paths ?? [];
351
403
  const pullRequestPaths = workflow.on?.pull_request?.paths ?? [];
352
404
  const parsedWorkflow = {
353
- pushPaths: uniqueSorted2(pushPaths),
354
- pullRequestPaths: uniqueSorted2(pullRequestPaths),
355
- paths: uniqueSorted2([...pushPaths, ...pullRequestPaths])
405
+ pushPaths: uniqueSorted3(pushPaths),
406
+ pullRequestPaths: uniqueSorted3(pullRequestPaths),
407
+ paths: uniqueSorted3([...pushPaths, ...pullRequestPaths])
356
408
  };
357
409
  if (workflow.name) {
358
410
  parsedWorkflow.name = workflow.name;
@@ -361,7 +413,7 @@ function parseWorkflowFile(workflowFile, rootDir) {
361
413
  }
362
414
 
363
415
  // src/workflow/validator.ts
364
- function uniqueSorted3(values) {
416
+ function uniqueSorted4(values) {
365
417
  return Array.from(new Set(values)).sort();
366
418
  }
367
419
  function buildBroadWildcards(workspaceRoots) {
@@ -372,6 +424,23 @@ function buildBroadWildcards(workspaceRoots) {
372
424
  }
373
425
  return wildcards;
374
426
  }
427
+ function hasGlobSyntax(path) {
428
+ return /[*?[\]{}]/.test(path);
429
+ }
430
+ function getStalePathCheckBase(path, allowedRootPaths, workflowPath) {
431
+ if (path === workflowPath || allowedRootPaths.includes(path) || path.startsWith("!")) {
432
+ return null;
433
+ }
434
+ const trailingGlobSuffix = path.endsWith("/**") ? "/**" : path.endsWith("/*") ? "/*" : null;
435
+ const checkBase = trailingGlobSuffix ? path.slice(0, -trailingGlobSuffix.length) : path;
436
+ if (!checkBase || hasGlobSyntax(checkBase)) {
437
+ return null;
438
+ }
439
+ if (!trailingGlobSuffix && hasGlobSyntax(path)) {
440
+ return null;
441
+ }
442
+ return checkBase;
443
+ }
375
444
  function splitActualPaths(paths, workspaceRoots, allowedRootPaths, workflowPath) {
376
445
  const workspacePaths = [];
377
446
  const ignoredPaths = [];
@@ -388,8 +457,8 @@ function splitActualPaths(paths, workspaceRoots, allowedRootPaths, workflowPath)
388
457
  ignoredPaths.push(path);
389
458
  }
390
459
  return {
391
- workspacePaths: uniqueSorted3(workspacePaths),
392
- ignoredPaths: uniqueSorted3(ignoredPaths)
460
+ workspacePaths: uniqueSorted4(workspacePaths),
461
+ ignoredPaths: uniqueSorted4(ignoredPaths)
393
462
  };
394
463
  }
395
464
  function isCoveredByExpectedPath(actualPath, expectedPaths) {
@@ -404,7 +473,7 @@ function isCoveredByExpectedPath(actualPath, expectedPaths) {
404
473
  return actualPath.startsWith(`${expectedPrefix}/`);
405
474
  });
406
475
  }
407
- function validateWorkflowResult(workflow, targetPackage, expectedPaths, actualPaths, broadWildcards) {
476
+ function validateWorkflowResult(workflow, targetPackage, expectedPaths, actualPaths, broadWildcards, additionalIssues = []) {
408
477
  const issues = [];
409
478
  const missing = expectedPaths.filter((path) => !actualPaths.includes(path));
410
479
  const unnecessary = actualPaths.filter(
@@ -433,6 +502,7 @@ function validateWorkflowResult(workflow, targetPackage, expectedPaths, actualPa
433
502
  });
434
503
  }
435
504
  }
505
+ issues.push(...additionalIssues);
436
506
  return {
437
507
  workflow,
438
508
  targetPackage,
@@ -444,6 +514,25 @@ function validateWorkflowResult(workflow, targetPackage, expectedPaths, actualPa
444
514
  issues
445
515
  };
446
516
  }
517
+ function getStalePathIssues(ignoredPaths, rootDir, allowedRootPaths, workflowPath) {
518
+ return ignoredPaths.flatMap((path) => {
519
+ const checkBase = getStalePathCheckBase(
520
+ path,
521
+ allowedRootPaths,
522
+ workflowPath
523
+ );
524
+ if (!checkBase || existsSync4(join5(rootDir, checkBase))) {
525
+ return [];
526
+ }
527
+ return [
528
+ {
529
+ kind: "stale-path",
530
+ path,
531
+ message: `Stale path '${path}' does not exist`
532
+ }
533
+ ];
534
+ });
535
+ }
447
536
  async function validateWorkflows(rootDir, policyOverrides) {
448
537
  const discoveredPackages = await discoverWorkspaces(rootDir, {
449
538
  fs: {
@@ -460,7 +549,7 @@ async function validateWorkflows(rootDir, policyOverrides) {
460
549
  const policy = {
461
550
  ...defaultWorkflowValidationPolicy,
462
551
  ...policyOverrides,
463
- allowedRootPaths: uniqueSorted3([
552
+ allowedRootPaths: uniqueSorted4([
464
553
  ...defaultWorkflowValidationPolicy.allowedRootPaths,
465
554
  ...policyOverrides?.allowedRootPaths ?? []
466
555
  ])
@@ -510,7 +599,7 @@ async function validateWorkflows(rootDir, policyOverrides) {
510
599
  issues: []
511
600
  };
512
601
  }
513
- const { workspacePaths: actualPaths } = splitActualPaths(
602
+ const { workspacePaths: actualPaths, ignoredPaths } = splitActualPaths(
514
603
  parsedWorkflow.paths,
515
604
  workspaceRoots,
516
605
  policy.allowedRootPaths,
@@ -527,7 +616,13 @@ async function validateWorkflows(rootDir, policyOverrides) {
527
616
  workflowTarget.targetPackage,
528
617
  expectedPaths,
529
618
  actualPaths,
530
- broadWildcards
619
+ broadWildcards,
620
+ getStalePathIssues(
621
+ ignoredPaths,
622
+ rootDir,
623
+ policy.allowedRootPaths,
624
+ workflowTarget.workflowPath
625
+ )
531
626
  );
532
627
  } catch (error) {
533
628
  return {
@@ -550,14 +645,6 @@ async function validateWorkflows(rootDir, policyOverrides) {
550
645
  }
551
646
 
552
647
  // src/cli/pr-preview.ts
553
- var WORKFLOW_MAPPING = {
554
- "deploy-web.yml": "web",
555
- "deploy-api-origin.yml": "api-origin",
556
- "deploy-api-edge.yml": "api-edge",
557
- "release-mobile.yml": "mobile",
558
- "release-desktop.yml": "desktop",
559
- "release-cli.yml": "cli"
560
- };
561
648
  var PLATFORM_INDICATORS = [
562
649
  {
563
650
  platform: "cloudflare-workers",
@@ -596,6 +683,28 @@ var PLATFORM_INDICATORS = [
596
683
  }
597
684
  ];
598
685
  var DEFAULT_APP_DIRECTORIES = ["apps"];
686
+ function formatWorkflowPathDrift(validationResults) {
687
+ const invalidResults = validationResults.filter((result) => !result.valid);
688
+ if (invalidResults.length === 0) {
689
+ return [];
690
+ }
691
+ const lines = [
692
+ "\u26A0\uFE0F Workflow path drift (advisory - does not affect impact above):"
693
+ ];
694
+ for (const result of invalidResults) {
695
+ lines.push(` ${result.workflow} (${result.targetPackage}):`);
696
+ for (const issue of result.issues) {
697
+ if (issue.kind === "missing") {
698
+ lines.push(` + ${issue.message}`);
699
+ } else if (issue.kind === "unnecessary") {
700
+ lines.push(` - ${issue.message}`);
701
+ } else {
702
+ lines.push(` ! ${issue.message}`);
703
+ }
704
+ }
705
+ }
706
+ return lines;
707
+ }
599
708
  function isDeployableApp(pkg) {
600
709
  const isInAppDirectory = DEFAULT_APP_DIRECTORIES.some(
601
710
  (dir) => pkg.path.includes(`/${dir}/`) || pkg.path.endsWith(`/${dir}`)
@@ -713,23 +822,32 @@ async function runPrPreview() {
713
822
  changedPackages.add(pkg.name);
714
823
  }
715
824
  }
716
- const changedWorkflows = changedFiles.filter(
717
- (f) => f.path.startsWith(".github/workflows/")
825
+ const workflowImpacts = getWorkflowImpacts(
826
+ rootDir,
827
+ packages,
828
+ changedFiles.map((file) => file.path)
829
+ );
830
+ const workflowDriftLines = formatWorkflowPathDrift(
831
+ await validateWorkflows(rootDir)
832
+ );
833
+ const workflowAffectedApps = new Set(
834
+ workflowImpacts.map(
835
+ (workflowImpact) => workflowImpact.targetPackage ?? workflowImpact.targetSlug
836
+ )
718
837
  );
719
- const workflowAffectedApps = /* @__PURE__ */ new Set();
720
- for (const workflow of changedWorkflows) {
721
- const workflowName = workflow.path.split("/").pop();
722
- if (workflowName && WORKFLOW_MAPPING[workflowName]) {
723
- workflowAffectedApps.add(WORKFLOW_MAPPING[workflowName]);
724
- }
725
- }
726
838
  console.log(
727
839
  `
728
840
  \u{1F4E6} Changed packages: ${Array.from(changedPackages).join(", ") || "none"}
729
841
  `
730
842
  );
731
- if (changedPackages.size === 0) {
732
- console.log("\n\u2728 No packages changed - no deployments needed\n");
843
+ if (changedPackages.size === 0 && workflowAffectedApps.size === 0) {
844
+ console.log(
845
+ "\n\u2728 No packages or workflow filters changed - no deployments needed\n"
846
+ );
847
+ if (workflowDriftLines.length > 0) {
848
+ console.log(workflowDriftLines.join("\n"));
849
+ console.log("");
850
+ }
733
851
  return;
734
852
  }
735
853
  const affected = findAffectedPackages(changedPackages, graph, {
@@ -826,6 +944,10 @@ async function runPrPreview() {
826
944
  console.log(`\u{1F4DD} = File changed`);
827
945
  console.log(`\u274C = File deleted
828
946
  `);
947
+ if (workflowDriftLines.length > 0) {
948
+ console.log(workflowDriftLines.join("\n"));
949
+ console.log("");
950
+ }
829
951
  } catch (error) {
830
952
  console.error(
831
953
  "Error:",
@@ -840,10 +962,10 @@ if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
840
962
 
841
963
  // src/cli/validate-workflows.ts
842
964
  import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
843
- import { join as join5, resolve as resolve3 } from "path";
965
+ import { join as join6, resolve as resolve3 } from "path";
844
966
  import { parse as parseYaml4 } from "yaml";
845
967
  function discoverPnpmWorkflows(rootDir) {
846
- const workflowsDir = join5(rootDir, ".github/workflows");
968
+ const workflowsDir = join6(rootDir, ".github/workflows");
847
969
  if (!existsSync5(workflowsDir)) {
848
970
  return [];
849
971
  }
@@ -852,7 +974,7 @@ function discoverPnpmWorkflows(rootDir) {
852
974
  (file) => file.endsWith(".yml")
853
975
  );
854
976
  for (const file of files) {
855
- const content = readFileSync3(join5(workflowsDir, file), "utf-8");
977
+ const content = readFileSync3(join6(workflowsDir, file), "utf-8");
856
978
  if (content.includes("pnpm/action-setup")) {
857
979
  workflows.push(file);
858
980
  }
@@ -865,12 +987,12 @@ function discoverPnpmDockerfiles(rootDir) {
865
987
  if (!entry.startsWith("Dockerfile")) {
866
988
  continue;
867
989
  }
868
- const content = readFileSync3(join5(rootDir, entry), "utf-8");
990
+ const content = readFileSync3(join6(rootDir, entry), "utf-8");
869
991
  if (content.includes("pnpm@")) {
870
992
  dockerfiles.push(entry);
871
993
  }
872
994
  }
873
- const appsDir = join5(rootDir, "apps");
995
+ const appsDir = join6(rootDir, "apps");
874
996
  if (!existsSync5(appsDir)) {
875
997
  return dockerfiles;
876
998
  }
@@ -878,7 +1000,7 @@ function discoverPnpmDockerfiles(rootDir) {
878
1000
  if (!entry.isDirectory()) {
879
1001
  continue;
880
1002
  }
881
- const dockerfilePath = join5(appsDir, entry.name, "Dockerfile");
1003
+ const dockerfilePath = join6(appsDir, entry.name, "Dockerfile");
882
1004
  if (!existsSync5(dockerfilePath)) {
883
1005
  continue;
884
1006
  }
@@ -891,7 +1013,7 @@ function discoverPnpmDockerfiles(rootDir) {
891
1013
  }
892
1014
  function getExpectedPnpmVersion(rootDir) {
893
1015
  const packageJson = JSON.parse(
894
- readFileSync3(join5(rootDir, "package.json"), "utf-8")
1016
+ readFileSync3(join6(rootDir, "package.json"), "utf-8")
895
1017
  );
896
1018
  const packageManager = packageJson.packageManager;
897
1019
  if (!packageManager?.startsWith("pnpm@")) {
@@ -900,7 +1022,7 @@ function getExpectedPnpmVersion(rootDir) {
900
1022
  return packageManager.replace("pnpm@", "");
901
1023
  }
902
1024
  function checkWorkflowPnpmVersion(workflowFile, rootDir) {
903
- const workflowPath = join5(rootDir, ".github/workflows", workflowFile);
1025
+ const workflowPath = join6(rootDir, ".github/workflows", workflowFile);
904
1026
  if (!existsSync5(workflowPath)) {
905
1027
  return { valid: true };
906
1028
  }
@@ -923,7 +1045,7 @@ function checkWorkflowPnpmVersion(workflowFile, rootDir) {
923
1045
  return { valid: true };
924
1046
  }
925
1047
  function checkDockerfilePnpmVersion(dockerfile, expectedVersion, rootDir) {
926
- const dockerfilePath = join5(rootDir, dockerfile);
1048
+ const dockerfilePath = join6(rootDir, dockerfile);
927
1049
  if (!existsSync5(dockerfilePath)) {
928
1050
  return { valid: true };
929
1051
  }