vouchington-tooling 0.13.2 → 0.13.4

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.
Files changed (36) hide show
  1. package/README.md +5 -0
  2. package/ast-grep/rules/no-three-sequential-awaits-tsx.yml +178 -0
  3. package/ast-grep/rules/no-three-sequential-awaits.yml +169 -0
  4. package/dist/ast-grep-examples/companion-parity.d.mts +19 -0
  5. package/dist/ast-grep-examples/companion-parity.mjs +156 -0
  6. package/dist/ast-grep-examples/index.d.mts +2 -1
  7. package/dist/ast-grep-examples/index.mjs +1 -0
  8. package/dist/coverage-transport/outcome.d.mts +6 -5
  9. package/dist/coverage-transport/outcome.mjs +6 -8
  10. package/dist/gha-check-run/cli.d.mts +2 -0
  11. package/dist/gha-check-run/cli.mjs +81 -0
  12. package/dist/gha-check-run/github.d.mts +30 -0
  13. package/dist/gha-check-run/github.mjs +63 -0
  14. package/dist/gha-check-run/index.d.mts +2 -0
  15. package/dist/gha-check-run/index.mjs +2 -0
  16. package/dist/gha-post-review/cli.mjs +1 -1
  17. package/dist/gha-post-review/github.d.mts +6 -22
  18. package/dist/gha-post-review/github.mjs +8 -92
  19. package/dist/gha-post-review/post.d.mts +7 -1
  20. package/dist/gha-post-review/post.mjs +11 -3
  21. package/dist/gha-post-review/pull-io.d.mts +13 -0
  22. package/dist/gha-post-review/pull-io.mjs +88 -0
  23. package/dist/index.d.mts +2 -1
  24. package/dist/index.mjs +1 -1
  25. package/dist/scc-complexity/baseline.mjs +5 -2
  26. package/dist/scc-complexity/constants.d.mts +1 -0
  27. package/dist/scc-complexity/constants.mjs +1 -0
  28. package/dist/scc-complexity/index.d.mts +1 -1
  29. package/dist/scc-complexity/index.mjs +24 -36
  30. package/dist/scc-complexity/paths.d.mts +2 -1
  31. package/dist/scc-complexity/paths.mjs +35 -2
  32. package/dist/scc-complexity/scope-config.d.mts +3 -0
  33. package/dist/scc-complexity/scope-config.mjs +35 -0
  34. package/dist/scc-complexity/workflow-command.d.mts +2 -0
  35. package/dist/scc-complexity/workflow-command.mjs +6 -0
  36. package/package.json +6 -1
@@ -4,11 +4,14 @@ import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { baselineEntryByScopeAndFile, normalizeSccComplexityBaseline, validateSccComplexityBaseline, } from './baseline.mjs';
7
- import { canonicalRepoPath, isInsideRepo } from './paths.mjs';
7
+ import { SCC_COMPLEXITY_LIMIT } from './constants.mjs';
8
+ import { canonicalRepoPath } from './paths.mjs';
8
9
  import { parseSccComplexityValues } from './parser.mjs';
10
+ import { normalizeScopes, scopesFor } from './scope-config.mjs';
11
+ import { escapeWorkflowCommandMessage, escapeWorkflowCommandProperty } from './workflow-command.mjs';
9
12
  const execFileAsync = promisify(execFile);
10
13
  export { parseSccComplexityBaseline, SCC_COMPLEXITY_BASELINE_VERSION } from './baseline.mjs';
11
- export const SCC_COMPLEXITY_LIMIT = 50;
14
+ export { SCC_COMPLEXITY_LIMIT } from './constants.mjs';
12
15
  const DEFAULT_INCLUDE_EXT = 'js,mts,jsx,ts,tsx';
13
16
  const DEFAULT_EXCLUDE_DIR = '.git,fixtures,__tests__,test-helpers';
14
17
  const DEFAULT_NOT_MATCH = String.raw `\.(test|spec)\.`;
@@ -31,10 +34,14 @@ export function buildSccArgs(options = {}) {
31
34
  }
32
35
  export async function checkSccComplexity(ctx, options = {}, runScc) {
33
36
  if (!ctx.isInsideGitRepo)
34
- return { errors: [`::error::${ctx.repoRoot} is not inside a git repository`] };
37
+ return {
38
+ errors: [
39
+ `::error::${escapeWorkflowCommandMessage(ctx.repoRoot)} is not inside a git repository`,
40
+ ],
41
+ };
35
42
  const dir = await mkdtemp(join(tmpdir(), options.tmpdirPrefix ?? DEFAULT_TMPDIR_PREFIX));
36
43
  try {
37
- const scopes = normalizeScopes(scopesFor(options), ctx.repoRoot);
44
+ const scopes = await normalizeScopes(scopesFor(options), ctx.repoRoot, options.limit);
38
45
  const trackedFileSet = new Set([...ctx.trackedFileSet].map((file) => canonicalRepoPath(ctx.repoRoot, file)));
39
46
  const baseline = options.baseline && normalizeSccComplexityBaseline(options.baseline, ctx.repoRoot);
40
47
  const results = await runScopes(ctx, options, scopes, dir, trackedFileSet, runScc);
@@ -44,7 +51,7 @@ export async function checkSccComplexity(ctx, options = {}, runScc) {
44
51
  }
45
52
  catch (error) {
46
53
  const message = error instanceof Error ? error.message : String(error);
47
- return { errors: [`::error::scc-complexity failed: ${message}`] };
54
+ return { errors: [`::error::scc-complexity failed: ${escapeWorkflowCommandMessage(message)}`] };
48
55
  }
49
56
  finally {
50
57
  await rm(dir, { force: true, recursive: true });
@@ -55,41 +62,16 @@ export function parseSccComplexityViolations(json, trackedFileSet, limit = SCC_C
55
62
  .filter((value) => value.complexity > limit)
56
63
  .toSorted(compare);
57
64
  }
58
- function scopesFor(options) {
59
- if (!options.scopes) {
60
- if (options.baseline)
61
- throw new Error('baseline requires named scopes');
62
- return [{ includePaths: [], name: '', ...options }];
63
- }
64
- if (options.scopes.length === 0)
65
- throw new Error('scopes must not be empty');
66
- const names = new Set();
67
- for (const scope of options.scopes) {
68
- if (!scope.name || names.has(scope.name))
69
- throw new Error(`scope name ${scope.name || '(empty)'} is invalid`);
70
- if (scope.includePaths.length === 0 || scope.includePaths.some((path) => !path))
71
- throw new Error(`scope ${scope.name} must include at least one path`);
72
- names.add(scope.name);
73
- }
74
- return options.scopes;
75
- }
76
- function normalizeScopes(scopes, repoRoot) {
77
- return scopes.map((scope) => {
78
- const includePaths = scope.includePaths.map((path) => canonicalRepoPath(repoRoot, path));
79
- if (includePaths.some((path) => !isInsideRepo(path)))
80
- throw new Error(`scope ${scope.name} includes a path outside the repository`);
81
- return { ...scope, includePaths };
82
- });
83
- }
84
65
  async function runScopes(ctx, options, scopes, dir, trackedFileSet, runScc) {
85
66
  const results = [];
86
67
  for (const [index, scope] of scopes.entries()) {
87
68
  const outputPath = join(dir, `scope-${index}.json`);
88
69
  const resolve = runScc ?? ((path) => runSccJson(ctx.repoRoot, path, options, scope));
89
- results.push({
90
- scope,
91
- values: parseSccComplexityValues(await resolve(outputPath, scope), trackedFileSet, ctx.repoRoot),
92
- });
70
+ const values = parseSccComplexityValues(await resolve(outputPath, scope), trackedFileSet, ctx.repoRoot);
71
+ const outside = values.find((value) => !isInScopeResult(scope, value.file));
72
+ if (outside)
73
+ throw new Error(`scope ${scope.name} received result outside its paths: ${outside.file}`);
74
+ results.push({ scope, values });
93
75
  }
94
76
  return results;
95
77
  }
@@ -106,9 +88,15 @@ function formatViolations(results, baseline) {
106
88
  const detail = entry
107
89
  ? `scc complexity ${value.complexity} exceeds baseline ceiling ${entry.complexity}`
108
90
  : `scc complexity ${value.complexity} exceeds ${limit}; simplify or split this file`;
109
- return [`::error file=${value.file}::${prefix}${value.file}: ${detail}`];
91
+ return [
92
+ `::error file=${escapeWorkflowCommandProperty(value.file)}::${escapeWorkflowCommandMessage(`${prefix}${value.file}: ${detail}`)}`,
93
+ ];
110
94
  }));
111
95
  }
96
+ function isInScopeResult(scope, file) {
97
+ return (scope.includePaths.length === 0 ||
98
+ scope.includePaths.some((path) => file === path || file.startsWith(`${path}/`) || path === '.'));
99
+ }
112
100
  async function runSccJson(repoRoot, outputPath, options, scope) {
113
101
  try {
114
102
  await execFileAsync(options.command ?? 'scc', [...buildSccArgs({ ...options, ...scope }), ...scope.includePaths, '--output', outputPath], { cwd: repoRoot, maxBuffer: 1024 * 1024 });
@@ -1,3 +1,4 @@
1
1
  export declare function canonicalRepoPath(repoRoot: string, path: string): string;
2
- export declare function isInsideRepo(path: string): boolean;
3
2
  export declare function isInScope(file: string, includePaths: readonly string[]): boolean;
3
+ export declare function canonicalScopePath(repoRoot: string, path: string): Promise<string>;
4
+ export declare function assertWorkflowCommandData(value: string, label: string): void;
@@ -1,11 +1,44 @@
1
- import { relative, resolve } from 'node:path';
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, relative, resolve, win32 } from 'node:path';
2
3
  export function canonicalRepoPath(repoRoot, path) {
3
4
  const relativePath = relative(repoRoot, resolve(repoRoot, path.replaceAll('\\', '/'))).replaceAll('\\', '/');
4
5
  return relativePath || '.';
5
6
  }
6
- export function isInsideRepo(path) {
7
+ function isInsideRepo(path) {
7
8
  return path !== '..' && !path.startsWith('../');
8
9
  }
9
10
  export function isInScope(file, includePaths) {
10
11
  return includePaths.some((includePath) => includePath === '.' || file === includePath || file.startsWith(`${includePath}/`));
11
12
  }
13
+ export async function canonicalScopePath(repoRoot, path) {
14
+ if (isAbsolute(path) || win32.parse(path).root)
15
+ throw new Error('path is outside the repository');
16
+ const physicalRoot = await realpath(repoRoot);
17
+ const candidatePath = resolve(repoRoot, path.replaceAll('\\', '/'));
18
+ if (!isInsideRepo(canonicalRepoPath(repoRoot, candidatePath)))
19
+ throw new Error('path is outside the repository');
20
+ const physicalPath = await physicalPathFor(candidatePath, repoRoot, physicalRoot);
21
+ const canonicalPath = canonicalRepoPath(physicalRoot, physicalPath);
22
+ if (!isInsideRepo(canonicalPath))
23
+ throw new Error('path is outside the repository');
24
+ return canonicalPath;
25
+ }
26
+ export function assertWorkflowCommandData(value, label) {
27
+ if (/[\r\n]/.test(value))
28
+ throw new Error(`${label} contains a workflow command control character`);
29
+ }
30
+ async function physicalPathFor(path, repoRoot, physicalRoot) {
31
+ let existingPath = path;
32
+ while (existingPath !== repoRoot) {
33
+ try {
34
+ const physicalPath = await realpath(existingPath);
35
+ return resolve(physicalPath, relative(existingPath, path));
36
+ }
37
+ catch (error) {
38
+ if ((await lstat(existingPath).catch(() => undefined))?.isSymbolicLink())
39
+ throw error;
40
+ existingPath = dirname(existingPath);
41
+ }
42
+ }
43
+ return resolve(physicalRoot, relative(repoRoot, path));
44
+ }
@@ -0,0 +1,3 @@
1
+ import type { SccComplexityOptions, SccComplexityScope } from './types.mts';
2
+ export declare function scopesFor(options: SccComplexityOptions): readonly SccComplexityScope[];
3
+ export declare function normalizeScopes(scopes: readonly SccComplexityScope[], repoRoot: string, topLevelLimit: number | undefined): Promise<readonly SccComplexityScope[]>;
@@ -0,0 +1,35 @@
1
+ import { assertWorkflowCommandData, canonicalScopePath } from './paths.mjs';
2
+ export function scopesFor(options) {
3
+ if (!options.scopes) {
4
+ if (options.baseline)
5
+ throw new Error('baseline requires named scopes');
6
+ return [{ includePaths: [], name: '', ...options }];
7
+ }
8
+ if (options.scopes.length === 0)
9
+ throw new Error('scopes must not be empty');
10
+ const names = new Set();
11
+ for (const scope of options.scopes) {
12
+ if (!scope.name || names.has(scope.name))
13
+ throw new Error(`scope name ${scope.name || '(empty)'} is invalid`);
14
+ assertWorkflowCommandData(scope.name, 'scope name');
15
+ if (scope.includePaths.length === 0 || scope.includePaths.some((path) => !path))
16
+ throw new Error(`scope ${scope.name} must include at least one path`);
17
+ for (const path of scope.includePaths)
18
+ assertWorkflowCommandData(path, `scope ${scope.name} path`);
19
+ names.add(scope.name);
20
+ }
21
+ return options.scopes;
22
+ }
23
+ export async function normalizeScopes(scopes, repoRoot, topLevelLimit) {
24
+ return Promise.all(scopes.map(async (scope) => {
25
+ let includePaths;
26
+ try {
27
+ includePaths = await Promise.all(scope.includePaths.map((path) => canonicalScopePath(repoRoot, path)));
28
+ }
29
+ catch {
30
+ throw new Error(`scope ${scope.name} includes a path outside the repository`);
31
+ }
32
+ const limit = scope.limit ?? topLevelLimit;
33
+ return limit === undefined ? { ...scope, includePaths } : { ...scope, includePaths, limit };
34
+ }));
35
+ }
@@ -0,0 +1,2 @@
1
+ export declare function escapeWorkflowCommandMessage(value: string): string;
2
+ export declare function escapeWorkflowCommandProperty(value: string): string;
@@ -0,0 +1,6 @@
1
+ export function escapeWorkflowCommandMessage(value) {
2
+ return value.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A');
3
+ }
4
+ export function escapeWorkflowCommandProperty(value) {
5
+ return escapeWorkflowCommandMessage(value).replaceAll(':', '%3A').replaceAll(',', '%2C');
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -208,6 +208,11 @@
208
208
  "import": "./dist/gha-claude-post-review/index.mjs",
209
209
  "default": "./dist/gha-claude-post-review/index.mjs"
210
210
  },
211
+ "./gha-check-run": {
212
+ "types": "./dist/gha-check-run/index.d.mts",
213
+ "import": "./dist/gha-check-run/index.mjs",
214
+ "default": "./dist/gha-check-run/index.mjs"
215
+ },
211
216
  "./http-link-pagination": {
212
217
  "types": "./dist/http-link-pagination/index.d.mts",
213
218
  "import": "./dist/http-link-pagination/index.mjs",