no-mistakes 0.48.0 → 0.48.2
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/README.md +1 -1
- package/index.js +75 -2
- package/invocation-types.d.ts +2 -0
- package/package.json +2 -1
- package/planning.js +137 -2
- package/test-types.d.ts +25 -27
- package/traversal-types.d.ts +2 -0
- package/workflow-topology-index-helpers.js +28 -0
- package/workflow-topology-index-types.d.ts +7 -0
- package/workflow-topology-index.js +10 -0
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ const {
|
|
|
70
70
|
changedFiles: ["src/utils.mts"],
|
|
71
71
|
});
|
|
72
72
|
// Complete changed-file inventory, including paths that selected no tests.
|
|
73
|
-
console.log(plan.
|
|
73
|
+
console.log(plan.changedFiles);
|
|
74
74
|
const targetCommands = await testsTargets({
|
|
75
75
|
root: process.cwd(),
|
|
76
76
|
framework: "vitest",
|
package/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
|
|
6
6
|
const planning = require("./planning");
|
|
7
7
|
const { createWorkflowTopologyIndex } = require("./workflow-topology-index");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
8
10
|
|
|
9
11
|
async function callJson(fn, options) {
|
|
10
12
|
const input = Buffer.from(JSON.stringify(options || {}));
|
|
@@ -59,19 +61,90 @@ const jsonApis = createJsonApis({
|
|
|
59
61
|
symbols: "symbolsJson",
|
|
60
62
|
});
|
|
61
63
|
|
|
64
|
+
const PLAN_INPUT_REPORTS = new Set(["testsComment", "testsGraph", "testsGraphMermaid"]);
|
|
65
|
+
const CAMELIZE_REPORTS = new Set(["testsPlan", "testsImpact", "testsTargets", "testsGraph"]);
|
|
66
|
+
|
|
67
|
+
async function analyzeProject(options = {}) {
|
|
68
|
+
const request = { ...options };
|
|
69
|
+
const generatedDirs = [];
|
|
70
|
+
try {
|
|
71
|
+
if (Array.isArray(request.reports)) {
|
|
72
|
+
request.reports = await Promise.all(
|
|
73
|
+
request.reports.map(async (report) => {
|
|
74
|
+
if (report.type === "testsWhy") {
|
|
75
|
+
const prepared = await planning.prepareWhyPlan(report);
|
|
76
|
+
if (prepared.generatedDir) generatedDirs.push(prepared.generatedDir);
|
|
77
|
+
return prepared.request;
|
|
78
|
+
}
|
|
79
|
+
return PLAN_INPUT_REPORTS.has(report.type)
|
|
80
|
+
? await planning.decamelizePlanOptions(report)
|
|
81
|
+
: report;
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const result = await jsonApis.analyzeProject(request);
|
|
86
|
+
for (const report of result.reports || []) {
|
|
87
|
+
if (report.type === "testsWhy") {
|
|
88
|
+
report.result = planning.camelizeWhy(report.result);
|
|
89
|
+
} else if (CAMELIZE_REPORTS.has(report.type)) {
|
|
90
|
+
report.result = planning.camelizeValue(report.result);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
} finally {
|
|
95
|
+
await Promise.all(generatedDirs.map((dir) => planning.removeGeneratedDir(dir)));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const topologyMemo = new Map();
|
|
100
|
+
|
|
101
|
+
async function ciTopology(options) {
|
|
102
|
+
const root = path.resolve((options && options.root) || process.cwd());
|
|
103
|
+
const configPath = path.resolve(root, (options && options.config) || ".no-mistakes.yml");
|
|
104
|
+
let mtime = 0;
|
|
105
|
+
try {
|
|
106
|
+
mtime = fs.statSync(configPath).mtimeMs;
|
|
107
|
+
} catch {
|
|
108
|
+
mtime = 0;
|
|
109
|
+
}
|
|
110
|
+
const workflows = JSON.stringify(
|
|
111
|
+
[]
|
|
112
|
+
.concat((options && options.workflows) || [])
|
|
113
|
+
.map(String)
|
|
114
|
+
.sort(),
|
|
115
|
+
);
|
|
116
|
+
const identity = `${root}\0${configPath}\0`;
|
|
117
|
+
const key = `${identity}${mtime}\0${workflows}`;
|
|
118
|
+
const stale = [];
|
|
119
|
+
for (const memoKey of topologyMemo.keys()) {
|
|
120
|
+
if (!memoKey.startsWith(identity)) continue;
|
|
121
|
+
const memoMtime = memoKey.slice(identity.length).split("\0")[0];
|
|
122
|
+
if (memoMtime !== String(mtime)) stale.push(memoKey);
|
|
123
|
+
}
|
|
124
|
+
for (const memoKey of stale) topologyMemo.delete(memoKey);
|
|
125
|
+
const cached = topologyMemo.get(key);
|
|
126
|
+
if (cached) return cached.then((value) => structuredClone(value));
|
|
127
|
+
const pending = jsonApis.ciTopology({ ...options, root }).catch((error) => {
|
|
128
|
+
topologyMemo.delete(key);
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
topologyMemo.set(key, pending);
|
|
132
|
+
return pending.then((value) => structuredClone(value));
|
|
133
|
+
}
|
|
134
|
+
|
|
62
135
|
async function version() {
|
|
63
136
|
return native.version();
|
|
64
137
|
}
|
|
65
138
|
|
|
66
139
|
module.exports.createWorkflowTopologyIndex = createWorkflowTopologyIndex;
|
|
67
140
|
module.exports.version = version;
|
|
68
|
-
module.exports.analyzeProject =
|
|
141
|
+
module.exports.analyzeProject = analyzeProject;
|
|
69
142
|
module.exports.callSites = jsonApis.callSites;
|
|
70
143
|
module.exports.check = jsonApis.check;
|
|
71
144
|
module.exports.resolveConfig = jsonApis.resolveConfig;
|
|
72
145
|
module.exports.ciEnv = jsonApis.ciEnv;
|
|
73
146
|
module.exports.ciImpact = jsonApis.ciImpact;
|
|
74
|
-
module.exports.ciTopology =
|
|
147
|
+
module.exports.ciTopology = ciTopology;
|
|
75
148
|
module.exports.dataPw = jsonApis.dataPw;
|
|
76
149
|
module.exports.deadExports = jsonApis.deadExports;
|
|
77
150
|
module.exports.dependencies = jsonApis.dependencies;
|
package/invocation-types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface InvocationOptions {
|
|
|
11
11
|
* `0` uses the CPU count, matching CLI `--jobs 0`.
|
|
12
12
|
*/
|
|
13
13
|
jobs?: number | null;
|
|
14
|
+
/** `ci` sets unbounded command and lock timeouts. CLI `--profile ci` does the same. */
|
|
15
|
+
profile?: "ci";
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type WithInvocationOptions<T> = T & InvocationOptions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-mistakes",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.2",
|
|
4
4
|
"description": "Static codebase analysis tools for TS/JS dependencies, dependents, and symbols",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"index.mjs",
|
|
19
19
|
"planning.js",
|
|
20
20
|
"workflow-topology-index.js",
|
|
21
|
+
"workflow-topology-index-helpers.js",
|
|
21
22
|
"scripts/install.js",
|
|
22
23
|
"scripts/install/",
|
|
23
24
|
"README.md",
|
package/planning.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
3
6
|
const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
|
|
4
7
|
|
|
5
8
|
async function callJson(fn, options) {
|
|
@@ -16,13 +19,109 @@ function createJsonApis(descriptors) {
|
|
|
16
19
|
);
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
function camelizeKey(key) {
|
|
23
|
+
return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decamelizeKey(key) {
|
|
27
|
+
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapKeys(value, mapKey) {
|
|
31
|
+
if (Array.isArray(value)) return value.map((item) => mapKeys(item, mapKey));
|
|
32
|
+
if (value && typeof value === "object") {
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.entries(value).map(([key, nested]) => [mapKey(key), mapKeys(nested, mapKey)]),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function camelizeValue(value) {
|
|
41
|
+
return mapKeys(value, camelizeKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function decamelizeValue(value) {
|
|
45
|
+
return mapKeys(value, decamelizeKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadPlanJson(planJson) {
|
|
49
|
+
let parsed = planJson;
|
|
50
|
+
if (typeof parsed === "string") {
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(parsed);
|
|
53
|
+
} catch {
|
|
54
|
+
return planJson;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (parsed && typeof parsed === "object") {
|
|
58
|
+
return decamelizeValue(parsed);
|
|
59
|
+
}
|
|
60
|
+
return planJson;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readPlanFile(planPath) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(await fs.readFile(planPath, "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function decamelizePlanOptions(options = {}) {
|
|
72
|
+
const next = { ...options };
|
|
73
|
+
if (next.planJson != null) {
|
|
74
|
+
next.planJson = loadPlanJson(next.planJson);
|
|
75
|
+
} else if (typeof next.plan === "string") {
|
|
76
|
+
const document = await readPlanFile(next.plan);
|
|
77
|
+
if (document !== undefined) {
|
|
78
|
+
next.planJson = loadPlanJson(document);
|
|
79
|
+
delete next.plan;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function prepareWhyPlan(options = {}) {
|
|
86
|
+
const next = { ...options };
|
|
87
|
+
let document = next.planJson;
|
|
88
|
+
if (document == null && typeof next.plan === "string") {
|
|
89
|
+
document = await readPlanFile(next.plan);
|
|
90
|
+
if (document === undefined) return { request: next };
|
|
91
|
+
}
|
|
92
|
+
if (document == null) return { request: next };
|
|
93
|
+
const generatedDir = await fs.mkdtemp(path.join(os.tmpdir(), "no-mistakes-why-"));
|
|
94
|
+
await fs.writeFile(path.join(generatedDir, "plan.json"), JSON.stringify(loadPlanJson(document)));
|
|
95
|
+
next.plan = path.join(generatedDir, "plan.json");
|
|
96
|
+
delete next.planJson;
|
|
97
|
+
return { request: next, generatedDir };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function materializeWhyPlan(options = {}) {
|
|
101
|
+
return (await prepareWhyPlan(options)).request;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function removeGeneratedDir(generatedDir) {
|
|
105
|
+
if (!generatedDir) return;
|
|
106
|
+
await fs.rm(generatedDir, { recursive: true, force: true }).catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function camelizeWhy(value) {
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
111
|
+
return camelizeValue(value);
|
|
112
|
+
}
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(value).map(([key, nested]) => [key, camelizeValue(nested)]),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
19
118
|
async function testsComment(options) {
|
|
20
|
-
const input = Buffer.from(JSON.stringify(options
|
|
119
|
+
const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
|
|
21
120
|
return String(await native.testsCommentMarkdown(input));
|
|
22
121
|
}
|
|
23
122
|
|
|
24
123
|
async function testsGraphMermaid(options) {
|
|
25
|
-
const input = Buffer.from(JSON.stringify(options
|
|
124
|
+
const input = Buffer.from(JSON.stringify(await decamelizePlanOptions(options)));
|
|
26
125
|
return String(await native.testsGraphMermaid(input));
|
|
27
126
|
}
|
|
28
127
|
|
|
@@ -44,8 +143,44 @@ const jsonApis = createJsonApis({
|
|
|
44
143
|
testsWhy: "testsWhyJson",
|
|
45
144
|
});
|
|
46
145
|
|
|
146
|
+
async function testsPlan(options) {
|
|
147
|
+
return camelizeValue(await jsonApis.testsPlan(options));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function testsImpact(options) {
|
|
151
|
+
return camelizeValue(await jsonApis.testsImpact(options));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function testsTargets(options) {
|
|
155
|
+
return camelizeValue(await jsonApis.testsTargets(options));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function testsWhy(options) {
|
|
159
|
+
const { request, generatedDir } = await prepareWhyPlan(options);
|
|
160
|
+
try {
|
|
161
|
+
return camelizeWhy(await jsonApis.testsWhy(request));
|
|
162
|
+
} finally {
|
|
163
|
+
await removeGeneratedDir(generatedDir);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function testsGraph(options) {
|
|
168
|
+
return camelizeValue(await jsonApis.testsGraph(await decamelizePlanOptions(options)));
|
|
169
|
+
}
|
|
170
|
+
|
|
47
171
|
module.exports = {
|
|
172
|
+
camelizeValue,
|
|
173
|
+
camelizeWhy,
|
|
174
|
+
decamelizePlanOptions,
|
|
175
|
+
materializeWhyPlan,
|
|
176
|
+
prepareWhyPlan,
|
|
177
|
+
removeGeneratedDir,
|
|
48
178
|
testsComment,
|
|
49
179
|
testsGraphMermaid,
|
|
50
180
|
...jsonApis,
|
|
181
|
+
testsGraph,
|
|
182
|
+
testsImpact,
|
|
183
|
+
testsPlan,
|
|
184
|
+
testsTargets,
|
|
185
|
+
testsWhy,
|
|
51
186
|
};
|
package/test-types.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type TestPlanFramework =
|
|
|
13
13
|
| "php"
|
|
14
14
|
| "java"
|
|
15
15
|
| "kotlin"
|
|
16
|
+
| "elixir"
|
|
17
|
+
| "dart"
|
|
16
18
|
| "jest";
|
|
17
19
|
|
|
18
20
|
interface TestsPlanOptionsBase {
|
|
@@ -41,6 +43,8 @@ interface TestsPlanOptionsBase {
|
|
|
41
43
|
globalConfigFallback?: boolean;
|
|
42
44
|
/** Include the markdown PR comment as `comment` on the returned plan. */
|
|
43
45
|
includeComment?: boolean;
|
|
46
|
+
/** Keep only selected tests whose relative path matches one of these globs. */
|
|
47
|
+
includeGlob?: string[];
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -90,18 +94,12 @@ export interface TestsTargetsOptions {
|
|
|
90
94
|
|
|
91
95
|
export interface TestPlan {
|
|
92
96
|
/** Complete deterministic changed-file inventory, relative to the request root. */
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
changedFiles?: string[];
|
|
96
|
-
selected_tests: SelectedTest[];
|
|
97
|
-
selectedTests?: SelectedTest[];
|
|
97
|
+
changedFiles: string[];
|
|
98
|
+
selectedTests: SelectedTest[];
|
|
98
99
|
groups?: TestPlanGroup[];
|
|
99
100
|
warnings: TestPlanWarning[];
|
|
100
|
-
|
|
101
|
-
fallbackTriggered?: boolean;
|
|
102
|
-
fallback_reason?: string | null;
|
|
101
|
+
fallbackTriggered: boolean;
|
|
103
102
|
fallbackReason?: string | null;
|
|
104
|
-
execution_targets?: GroupedExecutionTarget[];
|
|
105
103
|
executionTargets?: GroupedExecutionTarget[];
|
|
106
104
|
comment?: string | null;
|
|
107
105
|
}
|
|
@@ -110,16 +108,15 @@ export interface GroupedExecutionTarget {
|
|
|
110
108
|
runner: TestPlanFramework;
|
|
111
109
|
config?: string | null;
|
|
112
110
|
project?: string | null;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
testFiles
|
|
118
|
-
test_files: string[];
|
|
111
|
+
/** Path-prefix display name, such as a Swift package root. */
|
|
112
|
+
name?: string;
|
|
113
|
+
baseCommand: string[];
|
|
114
|
+
runnerArgs: string[];
|
|
115
|
+
testFiles: string[];
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
export interface SelectedTest {
|
|
122
|
-
|
|
119
|
+
testFile: string;
|
|
123
120
|
confidence: "low" | "medium" | "high";
|
|
124
121
|
reasons: ImpactReason[];
|
|
125
122
|
targets?: TestExecutionTarget[];
|
|
@@ -131,24 +128,26 @@ export interface TestExecutionTarget {
|
|
|
131
128
|
/** True when config is a Vitest workspace/project-array source rendered with --workspace. */
|
|
132
129
|
workspace?: boolean;
|
|
133
130
|
project?: string | null;
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
/** Path-prefix display name, such as a Swift package root. */
|
|
132
|
+
name?: string;
|
|
133
|
+
baseCommand: string[];
|
|
134
|
+
runnerArgs: string[];
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
export interface ImpactReason {
|
|
139
|
-
|
|
138
|
+
changedFile: string;
|
|
140
139
|
path: string[];
|
|
141
140
|
via: string[];
|
|
142
141
|
/** When present, aligns index-for-index with `via`. */
|
|
143
|
-
|
|
142
|
+
viaDetails?: Array<ImpactEdgeDetail | null>;
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
export type ImpactEdgeDetail = ResourceImpactEdgeDetail | VitestSetupImpactEdgeDetail;
|
|
147
146
|
|
|
148
147
|
export interface ResourceImpactEdgeDetail {
|
|
149
148
|
type: "resource";
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
consumerFile: string;
|
|
150
|
+
callSites: ResourceCallSite[];
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
export interface VitestSetupImpactEdgeDetail {
|
|
@@ -157,7 +156,7 @@ export interface VitestSetupImpactEdgeDetail {
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
export interface ResourceCallSite {
|
|
160
|
-
|
|
159
|
+
callKind: ResourceCallKind;
|
|
161
160
|
line: number;
|
|
162
161
|
}
|
|
163
162
|
|
|
@@ -211,6 +210,7 @@ export interface TestsWhyOptions {
|
|
|
211
210
|
test: string;
|
|
212
211
|
changed?: string;
|
|
213
212
|
plan?: string;
|
|
213
|
+
planJson?: SavedTestPlan | string;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
export interface WhyStep {
|
|
@@ -219,10 +219,8 @@ export interface WhyStep {
|
|
|
219
219
|
detail?: ImpactEdgeDetail | null;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/** A current or pre-`
|
|
223
|
-
export type SavedTestPlan =
|
|
224
|
-
changed_files?: string[];
|
|
225
|
-
};
|
|
222
|
+
/** A current or pre-`changedFiles` plan accepted by saved-plan document APIs. */
|
|
223
|
+
export type SavedTestPlan = TestPlan;
|
|
226
224
|
|
|
227
225
|
export interface TestsPlanDocumentOptions {
|
|
228
226
|
plan?: string;
|
package/traversal-types.d.ts
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function stepMatches(selector, step) {
|
|
4
|
+
return (
|
|
5
|
+
(selector.id === undefined || selector.id === step.id) &&
|
|
6
|
+
(selector.uses === undefined || selector.uses === step.uses) &&
|
|
7
|
+
(selector.name === undefined || selector.name === step.name)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function directCallerJobIdsForUses(jobsById, uses) {
|
|
12
|
+
const ids = [];
|
|
13
|
+
for (const job of jobsById.values()) {
|
|
14
|
+
if (job.steps.some((step) => step.uses === uses)) ids.push(job.id);
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze(ids.sort());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stepOrderIndexes(job, selectors) {
|
|
20
|
+
return Object.freeze(
|
|
21
|
+
selectors.map((selector) => {
|
|
22
|
+
const step = job.steps.find((candidate) => stepMatches(selector, candidate));
|
|
23
|
+
return step == null ? -1 : step.index;
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { directCallerJobIdsForUses, stepOrderIndexes };
|
|
@@ -24,6 +24,13 @@ export interface WorkflowTopologyIndex {
|
|
|
24
24
|
directDownstreamJobIds(jobId: string): readonly string[];
|
|
25
25
|
transitiveDownstreamJobIds(jobId: string): readonly string[];
|
|
26
26
|
directCallerJobIds(workflowPath: string): readonly string[];
|
|
27
|
+
/** Job ids whose steps `uses:` this action or workflow path. */
|
|
28
|
+
directCallerJobIdsForUses(uses: string): readonly string[];
|
|
29
|
+
/** Step indexes for `selectors` in `jobId`, or `-1` when a selector is missing. */
|
|
30
|
+
stepOrderIndexes(
|
|
31
|
+
jobId: string,
|
|
32
|
+
selectors: ReadonlyArray<{ id?: string; uses?: string; name?: string }>,
|
|
33
|
+
): readonly number[];
|
|
27
34
|
directCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
28
35
|
transitiveCallerWorkflowPaths(workflowPath: string): readonly string[];
|
|
29
36
|
directCalleeWorkflowPaths(workflowPath: string): readonly string[];
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
directCallerJobIdsForUses,
|
|
5
|
+
stepOrderIndexes,
|
|
6
|
+
} = require("./workflow-topology-index-helpers");
|
|
7
|
+
|
|
3
8
|
// A pure-JS query index rebuilt from the `ciTopology()` JSON, ported from
|
|
4
9
|
// the original engine's `topology-index.mts` + `frozen-topology.mts`. This
|
|
5
10
|
// stays JS-only by design: it returns closures over frozen `Map`s, which
|
|
@@ -214,6 +219,11 @@ function createWorkflowTopologyIndex(topology) {
|
|
|
214
219
|
transitiveWorkflowRunSubscriberPaths: workflowQuery(workflowRunSubscribers, true),
|
|
215
220
|
artifactProducersForConsumerJob: artifactEdgeQuery(artifactProducers),
|
|
216
221
|
artifactConsumersForProducerJob: artifactEdgeQuery(artifactConsumers),
|
|
222
|
+
directCallerJobIdsForUses: (uses) => directCallerJobIdsForUses(jobsById, uses),
|
|
223
|
+
stepOrderIndexes: (jobId, selectors) => {
|
|
224
|
+
assertKnown(jobsById, jobId, "workflow job");
|
|
225
|
+
return stepOrderIndexes(jobsById.get(jobId), selectors);
|
|
226
|
+
},
|
|
217
227
|
});
|
|
218
228
|
}
|
|
219
229
|
|