@elisra-devops/docgen-data-provider 1.69.0 → 1.69.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/bin/modules/PipelinesDataProvider.d.ts +104 -22
- package/bin/modules/PipelinesDataProvider.js +364 -58
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.d.ts +3 -0
- package/bin/modules/ResultDataProvider.js +49 -6
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +40 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +207 -8
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/PipelinesDataProvider.ts +404 -60
- package/src/modules/ResultDataProvider.ts +52 -6
- package/src/tests/modules/ResultDataProvider.test.ts +52 -0
- package/src/tests/modules/pipelineDataProvider.test.ts +232 -8
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { PipelineRun } from '../models/tfs-data';
|
|
1
|
+
import { PipelineRun, ResourceRepository } from '../models/tfs-data';
|
|
2
2
|
import GitDataProvider from './GitDataProvider';
|
|
3
3
|
export default class PipelinesDataProvider {
|
|
4
4
|
orgUrl: string;
|
|
5
5
|
token: string;
|
|
6
|
+
private projectNameByIdCache;
|
|
6
7
|
constructor(orgUrl: string, token: string);
|
|
7
8
|
findPreviousPipeline(teamProject: string, pipelineId: string, toPipelineRunId: number, targetPipeline: any, searchPrevPipelineFromDifferentCommit: boolean, fromStage?: string): Promise<any>;
|
|
8
9
|
/**
|
|
@@ -32,32 +33,113 @@ export default class PipelinesDataProvider {
|
|
|
32
33
|
* @returns `true` if the pipelines match based on the repository and version criteria; otherwise, `false`.
|
|
33
34
|
*/
|
|
34
35
|
private isMatchingPipeline;
|
|
36
|
+
private tryGetTeamProjectFromAzureDevOpsUrl;
|
|
35
37
|
/**
|
|
36
|
-
*
|
|
38
|
+
* Returns `true` when the input looks like an Azure DevOps GUID identifier.
|
|
39
|
+
*/
|
|
40
|
+
private isGuidLike;
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes a project identifier into a project name suitable for `{project}` URL path segments.
|
|
43
|
+
*
|
|
44
|
+
* Azure DevOps APIs often accept both project names and IDs, but some endpoints (especially on ADO Server)
|
|
45
|
+
* behave differently or return empty results when a GUID is used in the URL path. This method converts
|
|
46
|
+
* a GUID into its canonical project name via `/_apis/projects/{id}` and caches the mapping.
|
|
47
|
+
*
|
|
48
|
+
* @param projectNameOrId Project name (e.g. "Test CMMI") or project GUID.
|
|
49
|
+
* @returns The project name, or the original input when resolution fails.
|
|
50
|
+
*/
|
|
51
|
+
private normalizeProjectName;
|
|
52
|
+
/**
|
|
53
|
+
* Attempts to extract a run/build id from an Azure DevOps URL.
|
|
54
|
+
*
|
|
55
|
+
* Supports URLs shaped like:
|
|
56
|
+
* - `.../_apis/pipelines/{pipelineId}/runs/{runId}`
|
|
57
|
+
* - `.../_apis/build/builds/{buildId}`
|
|
58
|
+
*/
|
|
59
|
+
private tryParseRunIdFromUrl;
|
|
60
|
+
/**
|
|
61
|
+
* Normalizes a branch name into a `refs/...` form accepted by the Builds API.
|
|
62
|
+
*/
|
|
63
|
+
private normalizeBranchName;
|
|
64
|
+
/**
|
|
65
|
+
* Searches for a build by build number without restricting by definition id.
|
|
66
|
+
*
|
|
67
|
+
* Used as a fallback when the `resources.pipelines[alias].pipeline.id` is not a stable build definition id
|
|
68
|
+
* (some ADO instances return a run/build id or a pipeline revision-related id instead).
|
|
69
|
+
*
|
|
70
|
+
* @param projectName Team project name.
|
|
71
|
+
* @param buildNumber Build number / run name (e.g. "20251225.2", "1.0.56").
|
|
72
|
+
* @param branch Optional branch filter.
|
|
73
|
+
* @param expectedDefinitionName Optional build definition name to disambiguate results (typically YAML `source`).
|
|
74
|
+
*/
|
|
75
|
+
private findBuildByBuildNumber;
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves a build by id, with an additional fallback for ADO instances that allow the non-project-scoped route.
|
|
78
|
+
*
|
|
79
|
+
* @param projectName Optional team project name.
|
|
80
|
+
* @param buildId Build id.
|
|
81
|
+
*/
|
|
82
|
+
private tryGetBuildByIdWithFallback;
|
|
83
|
+
/**
|
|
84
|
+
* Searches for a build by definition id + build number.
|
|
85
|
+
*
|
|
86
|
+
* @param projectName Team project name.
|
|
87
|
+
* @param definitionId Build definition id (classic build definition id).
|
|
88
|
+
* @param buildNumber Build number / run name.
|
|
89
|
+
* @param branch Optional branch filter.
|
|
90
|
+
*/
|
|
91
|
+
private findBuildByDefinitionAndBuildNumber;
|
|
92
|
+
/**
|
|
93
|
+
* Resolves a pipelines API run id by comparing a desired run name against the pipeline run history.
|
|
94
|
+
*
|
|
95
|
+
* Useful when Builds API lookups don't return results even though the upstream run exists.
|
|
96
|
+
*
|
|
97
|
+
* @param projectName Team project name.
|
|
98
|
+
* @param pipelineId Pipeline id.
|
|
99
|
+
* @param runName Run "name" from ADO UI (e.g. "20251225.2").
|
|
100
|
+
*/
|
|
101
|
+
private findRunIdByPipelineRunName;
|
|
102
|
+
/**
|
|
103
|
+
* Attempts to infer a run/build id for a pipeline resource from the run payload fields.
|
|
104
|
+
*
|
|
105
|
+
* Resolution order:
|
|
106
|
+
* 1) `resource.runId` (preferred)
|
|
107
|
+
* 2) numeric `resource.version` (only if it's an integer string/number)
|
|
108
|
+
* 3) parse from `resource.pipeline.url` if it contains `/runs/{id}` or `/builds/{id}`
|
|
109
|
+
*/
|
|
110
|
+
private inferRunIdFromPipelineResource;
|
|
111
|
+
/**
|
|
112
|
+
* Resolves a pipeline resource run id from a non-numeric `version` (run name/build number).
|
|
113
|
+
*
|
|
114
|
+
* Fall back order (when `runId` isn't present):
|
|
115
|
+
* 1) Builds API: definitionId + buildNumber
|
|
116
|
+
* 2) Builds API: buildNumber-only (optionally filtered by YAML `source`)
|
|
117
|
+
* 3) Pipelines API: run history match by `name` / `#name`
|
|
118
|
+
* 4) Heuristic: treat `pipelineIdCandidate` as buildId and fetch that build
|
|
119
|
+
*/
|
|
120
|
+
private resolveRunIdFromVersion;
|
|
121
|
+
private isSupportedResourcePipelineBuild;
|
|
122
|
+
/**
|
|
123
|
+
* Extracts and resolves pipeline resources (`resources.pipelines`) from a pipeline run.
|
|
37
124
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
125
|
+
* Azure DevOps represents pipeline dependencies as "pipeline resources". Those resources do not always include
|
|
126
|
+
* a concrete `runId`, so this method resolves them into build-backed objects that can be used for recursion
|
|
127
|
+
* (e.g., release notes / SVD traversal).
|
|
40
128
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* 2. Checks if the input pipeline has any resources of type pipelines.
|
|
44
|
-
* 3. Iterates over each pipeline resource and processes it.
|
|
45
|
-
* 4. Fixes the URL of the pipeline resource to match the build API format.
|
|
46
|
-
* 5. Fetches the build details using the fixed URL.
|
|
47
|
-
* 6. If the build response is valid and matches the criteria, adds the pipeline resource to the set.
|
|
48
|
-
* 7. Returns an array of unique pipeline resources.
|
|
129
|
+
* @param inPipeline Pipeline run payload that contains `resources.pipelines`.
|
|
130
|
+
* @returns A collection of normalized pipeline resource objects (array), or an empty Set when there are no resources.
|
|
49
131
|
*
|
|
50
|
-
*
|
|
51
|
-
* - name: The alias name of the resource pipeline.
|
|
52
|
-
* - buildId: The ID of the resource pipeline.
|
|
53
|
-
* - definitionId: The ID of the build definition.
|
|
54
|
-
* - buildNumber: The build number.
|
|
55
|
-
* - teamProject: The name of the team project.
|
|
56
|
-
* - provider: The type of repository provider.
|
|
132
|
+
* Complexity: O(p) for p pipeline resources (network I/O dominates).
|
|
57
133
|
*
|
|
58
|
-
*
|
|
134
|
+
* Returned object shape:
|
|
135
|
+
* - `name`: resource alias
|
|
136
|
+
* - `buildId`: resolved build/run id
|
|
137
|
+
* - `definitionId`: build definition id (classic build definition id)
|
|
138
|
+
* - `buildNumber`: build number / run name
|
|
139
|
+
* - `teamProject`: resolved project name
|
|
140
|
+
* - `provider`: repo provider type (e.g., `TfsGit`)
|
|
59
141
|
*/
|
|
60
|
-
getPipelineResourcePipelinesFromObject(inPipeline: PipelineRun): Promise<any[] | Set<
|
|
142
|
+
getPipelineResourcePipelinesFromObject(inPipeline: PipelineRun): Promise<any[] | Set<unknown>>;
|
|
61
143
|
/**
|
|
62
144
|
* Retrieves a set of resource repositories from a given pipeline object.
|
|
63
145
|
*
|
|
@@ -65,7 +147,7 @@ export default class PipelinesDataProvider {
|
|
|
65
147
|
* @param gitDataProviderInstance - An instance of GitDataProvider to fetch repository details.
|
|
66
148
|
* @returns A promise that resolves to an array of unique resource repositories.
|
|
67
149
|
*/
|
|
68
|
-
getPipelineResourceRepositoriesFromObject(inPipeline: PipelineRun, gitDataProviderInstance: GitDataProvider): Promise<
|
|
150
|
+
getPipelineResourceRepositoriesFromObject(inPipeline: PipelineRun, gitDataProviderInstance: GitDataProvider): Promise<ResourceRepository[]>;
|
|
69
151
|
/**
|
|
70
152
|
* Retrieves the details of a specific pipeline build by its build ID.
|
|
71
153
|
*
|
|
@@ -6,10 +6,12 @@ class PipelinesDataProvider {
|
|
|
6
6
|
constructor(orgUrl, token) {
|
|
7
7
|
this.orgUrl = '';
|
|
8
8
|
this.token = '';
|
|
9
|
+
this.projectNameByIdCache = new Map();
|
|
9
10
|
this.orgUrl = orgUrl;
|
|
10
11
|
this.token = token;
|
|
11
12
|
}
|
|
12
13
|
async findPreviousPipeline(teamProject, pipelineId, toPipelineRunId, targetPipeline, searchPrevPipelineFromDifferentCommit, fromStage = '') {
|
|
14
|
+
var _a;
|
|
13
15
|
const pipelineRuns = await this.GetPipelineRunHistory(teamProject, pipelineId);
|
|
14
16
|
if (!pipelineRuns.value) {
|
|
15
17
|
return undefined;
|
|
@@ -22,7 +24,7 @@ class PipelinesDataProvider {
|
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
24
26
|
const fromPipeline = await this.getPipelineRunDetails(teamProject, Number(pipelineId), pipelineRun.id);
|
|
25
|
-
if (!fromPipeline.resources.repositories) {
|
|
27
|
+
if (!((_a = fromPipeline === null || fromPipeline === void 0 ? void 0 : fromPipeline.resources) === null || _a === void 0 ? void 0 : _a.repositories)) {
|
|
26
28
|
continue;
|
|
27
29
|
}
|
|
28
30
|
if (this.isMatchingPipeline(fromPipeline, targetPipeline, searchPrevPipelineFromDifferentCommit)) {
|
|
@@ -66,9 +68,15 @@ class PipelinesDataProvider {
|
|
|
66
68
|
* @returns `true` if the pipelines match based on the repository and version criteria; otherwise, `false`.
|
|
67
69
|
*/
|
|
68
70
|
isMatchingPipeline(fromPipeline, targetPipeline, searchPrevPipelineFromDifferentCommit) {
|
|
69
|
-
var _a, _b;
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
var _a, _b, _c, _d, _e, _f;
|
|
72
|
+
if (!((_a = fromPipeline === null || fromPipeline === void 0 ? void 0 : fromPipeline.resources) === null || _a === void 0 ? void 0 : _a.repositories) || !((_b = targetPipeline === null || targetPipeline === void 0 ? void 0 : targetPipeline.resources) === null || _b === void 0 ? void 0 : _b.repositories)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const fromRepo = ((_c = fromPipeline.resources.repositories[0]) === null || _c === void 0 ? void 0 : _c.self) || fromPipeline.resources.repositories.__designer_repo;
|
|
76
|
+
const targetRepo = ((_d = targetPipeline.resources.repositories[0]) === null || _d === void 0 ? void 0 : _d.self) || targetPipeline.resources.repositories.__designer_repo;
|
|
77
|
+
if (!((_e = fromRepo === null || fromRepo === void 0 ? void 0 : fromRepo.repository) === null || _e === void 0 ? void 0 : _e.id) || !((_f = targetRepo === null || targetRepo === void 0 ? void 0 : targetRepo.repository) === null || _f === void 0 ? void 0 : _f.id)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
72
80
|
if (fromRepo.repository.id !== targetRepo.repository.id) {
|
|
73
81
|
return false;
|
|
74
82
|
}
|
|
@@ -77,68 +85,362 @@ class PipelinesDataProvider {
|
|
|
77
85
|
}
|
|
78
86
|
return fromRepo.refName === targetRepo.refName;
|
|
79
87
|
}
|
|
88
|
+
tryGetTeamProjectFromAzureDevOpsUrl(url) {
|
|
89
|
+
if (!url)
|
|
90
|
+
return undefined;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
94
|
+
const apiIndex = parts.findIndex((p) => p === '_apis');
|
|
95
|
+
if (apiIndex <= 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
return parts[apiIndex - 1];
|
|
98
|
+
}
|
|
99
|
+
catch (_a) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns `true` when the input looks like an Azure DevOps GUID identifier.
|
|
105
|
+
*/
|
|
106
|
+
isGuidLike(value) {
|
|
107
|
+
if (!value)
|
|
108
|
+
return false;
|
|
109
|
+
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(String(value).trim());
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Normalizes a project identifier into a project name suitable for `{project}` URL path segments.
|
|
113
|
+
*
|
|
114
|
+
* Azure DevOps APIs often accept both project names and IDs, but some endpoints (especially on ADO Server)
|
|
115
|
+
* behave differently or return empty results when a GUID is used in the URL path. This method converts
|
|
116
|
+
* a GUID into its canonical project name via `/_apis/projects/{id}` and caches the mapping.
|
|
117
|
+
*
|
|
118
|
+
* @param projectNameOrId Project name (e.g. "Test CMMI") or project GUID.
|
|
119
|
+
* @returns The project name, or the original input when resolution fails.
|
|
120
|
+
*/
|
|
121
|
+
async normalizeProjectName(projectNameOrId) {
|
|
122
|
+
if (!projectNameOrId)
|
|
123
|
+
return undefined;
|
|
124
|
+
const raw = String(projectNameOrId).trim();
|
|
125
|
+
if (!raw)
|
|
126
|
+
return undefined;
|
|
127
|
+
if (!this.isGuidLike(raw))
|
|
128
|
+
return raw;
|
|
129
|
+
const cached = this.projectNameByIdCache.get(raw);
|
|
130
|
+
if (cached)
|
|
131
|
+
return cached;
|
|
132
|
+
// ADO supports querying projects at the collection/org root.
|
|
133
|
+
const url = `${this.orgUrl}_apis/projects/${encodeURIComponent(raw)}?api-version=6.0`;
|
|
134
|
+
try {
|
|
135
|
+
const project = await tfs_1.TFSServices.getItemContent(url, this.token, 'get', null, null, false);
|
|
136
|
+
const resolvedName = String((project === null || project === void 0 ? void 0 : project.name) || '').trim();
|
|
137
|
+
if (resolvedName) {
|
|
138
|
+
this.projectNameByIdCache.set(raw, resolvedName);
|
|
139
|
+
return resolvedName;
|
|
140
|
+
}
|
|
141
|
+
return raw;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return raw;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Attempts to extract a run/build id from an Azure DevOps URL.
|
|
149
|
+
*
|
|
150
|
+
* Supports URLs shaped like:
|
|
151
|
+
* - `.../_apis/pipelines/{pipelineId}/runs/{runId}`
|
|
152
|
+
* - `.../_apis/build/builds/{buildId}`
|
|
153
|
+
*/
|
|
154
|
+
tryParseRunIdFromUrl(url) {
|
|
155
|
+
if (!url)
|
|
156
|
+
return undefined;
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(url);
|
|
159
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
160
|
+
const runsIndex = parts.findIndex((p) => p === 'runs');
|
|
161
|
+
if (runsIndex >= 0 && parts[runsIndex + 1]) {
|
|
162
|
+
const runId = Number(parts[runsIndex + 1]);
|
|
163
|
+
return Number.isFinite(runId) ? runId : undefined;
|
|
164
|
+
}
|
|
165
|
+
const buildsIndex = parts.findIndex((p) => p === 'builds');
|
|
166
|
+
if (buildsIndex >= 0 && parts[buildsIndex + 1]) {
|
|
167
|
+
const buildId = Number(parts[buildsIndex + 1]);
|
|
168
|
+
return Number.isFinite(buildId) ? buildId : undefined;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
catch (_a) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Normalizes a branch name into a `refs/...` form accepted by the Builds API.
|
|
178
|
+
*/
|
|
179
|
+
normalizeBranchName(branch) {
|
|
180
|
+
if (!branch)
|
|
181
|
+
return undefined;
|
|
182
|
+
const b = String(branch).trim();
|
|
183
|
+
if (!b)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (b.startsWith('refs/'))
|
|
186
|
+
return b;
|
|
187
|
+
if (b.startsWith('heads/'))
|
|
188
|
+
return `refs/${b}`;
|
|
189
|
+
return `refs/heads/${b}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Searches for a build by build number without restricting by definition id.
|
|
193
|
+
*
|
|
194
|
+
* Used as a fallback when the `resources.pipelines[alias].pipeline.id` is not a stable build definition id
|
|
195
|
+
* (some ADO instances return a run/build id or a pipeline revision-related id instead).
|
|
196
|
+
*
|
|
197
|
+
* @param projectName Team project name.
|
|
198
|
+
* @param buildNumber Build number / run name (e.g. "20251225.2", "1.0.56").
|
|
199
|
+
* @param branch Optional branch filter.
|
|
200
|
+
* @param expectedDefinitionName Optional build definition name to disambiguate results (typically YAML `source`).
|
|
201
|
+
*/
|
|
202
|
+
async findBuildByBuildNumber(projectName, buildNumber, branch, expectedDefinitionName) {
|
|
203
|
+
var _a;
|
|
204
|
+
const bn = String(buildNumber || '').trim();
|
|
205
|
+
if (!bn)
|
|
206
|
+
return undefined;
|
|
207
|
+
const normalizedBranch = this.normalizeBranchName(branch);
|
|
208
|
+
let url = `${this.orgUrl}${projectName}/_apis/build/builds?buildNumber=${encodeURIComponent(bn)}&$top=20&queryOrder=finishTimeDescending&api-version=6.0`;
|
|
209
|
+
if (normalizedBranch) {
|
|
210
|
+
url += `&branchName=${encodeURIComponent(normalizedBranch)}`;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const res = await tfs_1.TFSServices.getItemContent(url, this.token, 'get', null, null);
|
|
214
|
+
const value = (res === null || res === void 0 ? void 0 : res.value) || [];
|
|
215
|
+
const filtered = expectedDefinitionName
|
|
216
|
+
? value.filter((b) => { var _a; return String(((_a = b === null || b === void 0 ? void 0 : b.definition) === null || _a === void 0 ? void 0 : _a.name) || '') === String(expectedDefinitionName); })
|
|
217
|
+
: value;
|
|
218
|
+
return (_a = filtered[0]) !== null && _a !== void 0 ? _a : value[0];
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
logger_1.default.error(`Error resolving build by buildNumber (no definition) (${projectName}/${bn}): ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
80
225
|
/**
|
|
81
|
-
* Retrieves a
|
|
226
|
+
* Retrieves a build by id, with an additional fallback for ADO instances that allow the non-project-scoped route.
|
|
82
227
|
*
|
|
83
|
-
* @param
|
|
84
|
-
* @
|
|
228
|
+
* @param projectName Optional team project name.
|
|
229
|
+
* @param buildId Build id.
|
|
230
|
+
*/
|
|
231
|
+
async tryGetBuildByIdWithFallback(projectName, buildId) {
|
|
232
|
+
if (projectName) {
|
|
233
|
+
try {
|
|
234
|
+
return await this.getPipelineBuildByBuildId(projectName, buildId);
|
|
235
|
+
}
|
|
236
|
+
catch (e1) {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Some ADO instances allow build lookup without the {project} path segment.
|
|
240
|
+
try {
|
|
241
|
+
const url = `${this.orgUrl}_apis/build/builds/${buildId}`;
|
|
242
|
+
return await tfs_1.TFSServices.getItemContent(url, this.token, 'get', null, null);
|
|
243
|
+
}
|
|
244
|
+
catch (e2) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Searches for a build by definition id + build number.
|
|
85
250
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
251
|
+
* @param projectName Team project name.
|
|
252
|
+
* @param definitionId Build definition id (classic build definition id).
|
|
253
|
+
* @param buildNumber Build number / run name.
|
|
254
|
+
* @param branch Optional branch filter.
|
|
255
|
+
*/
|
|
256
|
+
async findBuildByDefinitionAndBuildNumber(projectName, definitionId, buildNumber, branch) {
|
|
257
|
+
const bn = String(buildNumber || '').trim();
|
|
258
|
+
if (!bn)
|
|
259
|
+
return undefined;
|
|
260
|
+
const normalizedBranch = this.normalizeBranchName(branch);
|
|
261
|
+
let url = `${this.orgUrl}${projectName}/_apis/build/builds?definitions=${encodeURIComponent(String(definitionId))}&buildNumber=${encodeURIComponent(bn)}&$top=1&queryOrder=finishTimeDescending&api-version=6.0`;
|
|
262
|
+
if (normalizedBranch) {
|
|
263
|
+
url += `&branchName=${encodeURIComponent(normalizedBranch)}`;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const res = await tfs_1.TFSServices.getItemContent(url, this.token, 'get', null, null);
|
|
267
|
+
const value = (res === null || res === void 0 ? void 0 : res.value) || [];
|
|
268
|
+
return value[0];
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
logger_1.default.error(`Error resolving build by buildNumber for definition ${definitionId} (${projectName}/${bn}): ${err.message}`);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Resolves a pipelines API run id by comparing a desired run name against the pipeline run history.
|
|
94
277
|
*
|
|
95
|
-
*
|
|
96
|
-
* - name: The alias name of the resource pipeline.
|
|
97
|
-
* - buildId: The ID of the resource pipeline.
|
|
98
|
-
* - definitionId: The ID of the build definition.
|
|
99
|
-
* - buildNumber: The build number.
|
|
100
|
-
* - teamProject: The name of the team project.
|
|
101
|
-
* - provider: The type of repository provider.
|
|
278
|
+
* Useful when Builds API lookups don't return results even though the upstream run exists.
|
|
102
279
|
*
|
|
103
|
-
* @
|
|
280
|
+
* @param projectName Team project name.
|
|
281
|
+
* @param pipelineId Pipeline id.
|
|
282
|
+
* @param runName Run "name" from ADO UI (e.g. "20251225.2").
|
|
283
|
+
*/
|
|
284
|
+
async findRunIdByPipelineRunName(projectName, pipelineId, runName) {
|
|
285
|
+
const desired = String(runName || '').trim();
|
|
286
|
+
if (!desired)
|
|
287
|
+
return undefined;
|
|
288
|
+
try {
|
|
289
|
+
const history = await this.GetPipelineRunHistory(projectName, String(pipelineId));
|
|
290
|
+
const runs = (history === null || history === void 0 ? void 0 : history.value) || [];
|
|
291
|
+
const match = runs.find((r) => {
|
|
292
|
+
const name = String((r === null || r === void 0 ? void 0 : r.name) || '').trim();
|
|
293
|
+
const id = String((r === null || r === void 0 ? void 0 : r.id) || '').trim();
|
|
294
|
+
return name === desired || name === `#${desired}` || id === desired;
|
|
295
|
+
});
|
|
296
|
+
if (match === null || match === void 0 ? void 0 : match.id) {
|
|
297
|
+
return Number(match.id);
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
logger_1.default.error(`Error resolving runId by runName for pipeline ${pipelineId} (${projectName}/${desired}): ${(e === null || e === void 0 ? void 0 : e.message) || e}`);
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Attempts to infer a run/build id for a pipeline resource from the run payload fields.
|
|
308
|
+
*
|
|
309
|
+
* Resolution order:
|
|
310
|
+
* 1) `resource.runId` (preferred)
|
|
311
|
+
* 2) numeric `resource.version` (only if it's an integer string/number)
|
|
312
|
+
* 3) parse from `resource.pipeline.url` if it contains `/runs/{id}` or `/builds/{id}`
|
|
313
|
+
*/
|
|
314
|
+
inferRunIdFromPipelineResource(resource, pipelineUrl) {
|
|
315
|
+
const explicit = Number(resource === null || resource === void 0 ? void 0 : resource.runId);
|
|
316
|
+
if (Number.isFinite(explicit))
|
|
317
|
+
return explicit;
|
|
318
|
+
const version = resource === null || resource === void 0 ? void 0 : resource.version;
|
|
319
|
+
if (typeof version === 'number' && Number.isFinite(version))
|
|
320
|
+
return version;
|
|
321
|
+
if (typeof version === 'string' && /^\d+$/.test(version))
|
|
322
|
+
return Number(version);
|
|
323
|
+
const parsed = this.tryParseRunIdFromUrl(pipelineUrl);
|
|
324
|
+
return Number.isFinite(parsed) ? Number(parsed) : undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Resolves a pipeline resource run id from a non-numeric `version` (run name/build number).
|
|
328
|
+
*
|
|
329
|
+
* Fall back order (when `runId` isn't present):
|
|
330
|
+
* 1) Builds API: definitionId + buildNumber
|
|
331
|
+
* 2) Builds API: buildNumber-only (optionally filtered by YAML `source`)
|
|
332
|
+
* 3) Pipelines API: run history match by `name` / `#name`
|
|
333
|
+
* 4) Heuristic: treat `pipelineIdCandidate` as buildId and fetch that build
|
|
334
|
+
*/
|
|
335
|
+
async resolveRunIdFromVersion(params) {
|
|
336
|
+
const { projectName, pipelineIdCandidate, version, branch, source } = params;
|
|
337
|
+
if (!Number.isFinite(pipelineIdCandidate))
|
|
338
|
+
return undefined;
|
|
339
|
+
if (!projectName) {
|
|
340
|
+
const buildById = await this.tryGetBuildByIdWithFallback(undefined, pipelineIdCandidate);
|
|
341
|
+
return (buildById === null || buildById === void 0 ? void 0 : buildById.id) ? Number(buildById.id) : undefined;
|
|
342
|
+
}
|
|
343
|
+
if (typeof version !== 'string' || !String(version).trim())
|
|
344
|
+
return undefined;
|
|
345
|
+
const buildNumber = String(version).trim();
|
|
346
|
+
const buildByNumber = await this.findBuildByDefinitionAndBuildNumber(projectName, pipelineIdCandidate, buildNumber, branch);
|
|
347
|
+
if (buildByNumber === null || buildByNumber === void 0 ? void 0 : buildByNumber.id)
|
|
348
|
+
return Number(buildByNumber.id);
|
|
349
|
+
const buildByNumberAny = await this.findBuildByBuildNumber(projectName, buildNumber, branch, typeof source === 'string' ? source : undefined);
|
|
350
|
+
if (buildByNumberAny === null || buildByNumberAny === void 0 ? void 0 : buildByNumberAny.id)
|
|
351
|
+
return Number(buildByNumberAny.id);
|
|
352
|
+
const runIdByName = await this.findRunIdByPipelineRunName(projectName, pipelineIdCandidate, buildNumber);
|
|
353
|
+
if (runIdByName)
|
|
354
|
+
return runIdByName;
|
|
355
|
+
const buildById = await this.tryGetBuildByIdWithFallback(projectName, pipelineIdCandidate);
|
|
356
|
+
return (buildById === null || buildById === void 0 ? void 0 : buildById.id) ? Number(buildById.id) : undefined;
|
|
357
|
+
}
|
|
358
|
+
isSupportedResourcePipelineBuild(buildResponse) {
|
|
359
|
+
var _a, _b;
|
|
360
|
+
const definitionType = (_a = buildResponse === null || buildResponse === void 0 ? void 0 : buildResponse.definition) === null || _a === void 0 ? void 0 : _a.type;
|
|
361
|
+
const repoType = (_b = buildResponse === null || buildResponse === void 0 ? void 0 : buildResponse.repository) === null || _b === void 0 ? void 0 : _b.type;
|
|
362
|
+
return (!!buildResponse &&
|
|
363
|
+
definitionType === 'build' &&
|
|
364
|
+
(repoType === 'TfsGit' || repoType === 'azureReposGit'));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Extracts and resolves pipeline resources (`resources.pipelines`) from a pipeline run.
|
|
368
|
+
*
|
|
369
|
+
* Azure DevOps represents pipeline dependencies as "pipeline resources". Those resources do not always include
|
|
370
|
+
* a concrete `runId`, so this method resolves them into build-backed objects that can be used for recursion
|
|
371
|
+
* (e.g., release notes / SVD traversal).
|
|
372
|
+
*
|
|
373
|
+
* @param inPipeline Pipeline run payload that contains `resources.pipelines`.
|
|
374
|
+
* @returns A collection of normalized pipeline resource objects (array), or an empty Set when there are no resources.
|
|
375
|
+
*
|
|
376
|
+
* Complexity: O(p) for p pipeline resources (network I/O dominates).
|
|
377
|
+
*
|
|
378
|
+
* Returned object shape:
|
|
379
|
+
* - `name`: resource alias
|
|
380
|
+
* - `buildId`: resolved build/run id
|
|
381
|
+
* - `definitionId`: build definition id (classic build definition id)
|
|
382
|
+
* - `buildNumber`: build number / run name
|
|
383
|
+
* - `teamProject`: resolved project name
|
|
384
|
+
* - `provider`: repo provider type (e.g., `TfsGit`)
|
|
104
385
|
*/
|
|
105
386
|
async getPipelineResourcePipelinesFromObject(inPipeline) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
387
|
+
var _a;
|
|
388
|
+
const resourcePipelinesByKey = new Map();
|
|
389
|
+
if (!((_a = inPipeline === null || inPipeline === void 0 ? void 0 : inPipeline.resources) === null || _a === void 0 ? void 0 : _a.pipelines)) {
|
|
390
|
+
return new Set();
|
|
109
391
|
}
|
|
110
|
-
const
|
|
111
|
-
const pipelineEntries = Object.entries(pipelines);
|
|
392
|
+
const pipelineEntries = Object.entries(inPipeline.resources.pipelines);
|
|
112
393
|
await Promise.all(pipelineEntries.map(async ([resourcePipelineAlias, resource]) => {
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
394
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
395
|
+
const resourcePipelineObj = resource === null || resource === void 0 ? void 0 : resource.pipeline;
|
|
396
|
+
const pipelineIdCandidate = Number(resourcePipelineObj === null || resourcePipelineObj === void 0 ? void 0 : resourcePipelineObj.id);
|
|
397
|
+
const source = resource === null || resource === void 0 ? void 0 : resource.source;
|
|
398
|
+
const version = resource === null || resource === void 0 ? void 0 : resource.version;
|
|
399
|
+
const branch = resource === null || resource === void 0 ? void 0 : resource.branch;
|
|
400
|
+
const rawProjectName = ((_a = resource === null || resource === void 0 ? void 0 : resource.project) === null || _a === void 0 ? void 0 : _a.name) ||
|
|
401
|
+
this.tryGetTeamProjectFromAzureDevOpsUrl(resourcePipelineObj === null || resourcePipelineObj === void 0 ? void 0 : resourcePipelineObj.url) ||
|
|
402
|
+
this.tryGetTeamProjectFromAzureDevOpsUrl(inPipeline === null || inPipeline === void 0 ? void 0 : inPipeline.url);
|
|
403
|
+
const projectName = await this.normalizeProjectName(rawProjectName);
|
|
404
|
+
if (!Number.isFinite(pipelineIdCandidate)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let runId = this.inferRunIdFromPipelineResource(resource, resourcePipelineObj === null || resourcePipelineObj === void 0 ? void 0 : resourcePipelineObj.url);
|
|
408
|
+
if (typeof runId !== 'number' || !Number.isFinite(runId)) {
|
|
409
|
+
runId = await this.resolveRunIdFromVersion({
|
|
410
|
+
projectName,
|
|
411
|
+
pipelineIdCandidate,
|
|
412
|
+
version,
|
|
413
|
+
branch,
|
|
414
|
+
source,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (typeof runId !== 'number' || !Number.isFinite(runId)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
118
420
|
let buildResponse;
|
|
119
421
|
try {
|
|
120
|
-
buildResponse = await
|
|
422
|
+
buildResponse = await this.tryGetBuildByIdWithFallback(projectName, runId);
|
|
121
423
|
}
|
|
122
424
|
catch (err) {
|
|
123
|
-
logger_1.default.error(`Error fetching pipeline ${
|
|
425
|
+
logger_1.default.error(`Error fetching pipeline ${resourcePipelineAlias} run ${runId} : ${err.message}`);
|
|
124
426
|
}
|
|
125
|
-
if (buildResponse
|
|
126
|
-
buildResponse.definition
|
|
127
|
-
buildResponse.
|
|
128
|
-
|
|
129
|
-
name:
|
|
130
|
-
buildId
|
|
131
|
-
definitionId
|
|
132
|
-
buildNumber: buildResponse.buildNumber,
|
|
133
|
-
teamProject: buildResponse.project.name,
|
|
134
|
-
provider: buildResponse.repository.type,
|
|
427
|
+
if (this.isSupportedResourcePipelineBuild(buildResponse)) {
|
|
428
|
+
const definitionId = (_c = (_b = buildResponse.definition) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : pipelineIdCandidate;
|
|
429
|
+
const buildId = (_d = buildResponse.id) !== null && _d !== void 0 ? _d : runId;
|
|
430
|
+
const resourcePipelineToAdd = {
|
|
431
|
+
name: resourcePipelineAlias,
|
|
432
|
+
buildId,
|
|
433
|
+
definitionId,
|
|
434
|
+
buildNumber: (_f = (_e = buildResponse.buildNumber) !== null && _e !== void 0 ? _e : resource === null || resource === void 0 ? void 0 : resource.runName) !== null && _f !== void 0 ? _f : resource === null || resource === void 0 ? void 0 : resource.version,
|
|
435
|
+
teamProject: (_h = (_g = buildResponse.project) === null || _g === void 0 ? void 0 : _g.name) !== null && _h !== void 0 ? _h : projectName,
|
|
436
|
+
provider: (_j = buildResponse.repository) === null || _j === void 0 ? void 0 : _j.type,
|
|
135
437
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
438
|
+
const key = `${resourcePipelineToAdd.teamProject}:${resourcePipelineToAdd.definitionId}:${resourcePipelineToAdd.buildId}:${resourcePipelineToAdd.name}`;
|
|
439
|
+
if (!resourcePipelinesByKey.has(key))
|
|
440
|
+
resourcePipelinesByKey.set(key, resourcePipelineToAdd);
|
|
139
441
|
}
|
|
140
442
|
}));
|
|
141
|
-
return [...
|
|
443
|
+
return [...resourcePipelinesByKey.values()];
|
|
142
444
|
}
|
|
143
445
|
/**
|
|
144
446
|
* Retrieves a set of resource repositories from a given pipeline object.
|
|
@@ -148,28 +450,32 @@ class PipelinesDataProvider {
|
|
|
148
450
|
* @returns A promise that resolves to an array of unique resource repositories.
|
|
149
451
|
*/
|
|
150
452
|
async getPipelineResourceRepositoriesFromObject(inPipeline, gitDataProviderInstance) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
453
|
+
var _a, _b;
|
|
454
|
+
const resourceRepositoriesById = new Map();
|
|
455
|
+
const repositories = (_a = inPipeline === null || inPipeline === void 0 ? void 0 : inPipeline.resources) === null || _a === void 0 ? void 0 : _a.repositories;
|
|
456
|
+
if (!repositories) {
|
|
457
|
+
return [];
|
|
154
458
|
}
|
|
155
|
-
const repositories = inPipeline.resources.repositories;
|
|
156
459
|
for (const prop in repositories) {
|
|
157
460
|
const resourceRepo = repositories[prop];
|
|
158
|
-
if (resourceRepo.repository.type !== 'azureReposGit') {
|
|
461
|
+
if (((_b = resourceRepo === null || resourceRepo === void 0 ? void 0 : resourceRepo.repository) === null || _b === void 0 ? void 0 : _b.type) !== 'azureReposGit') {
|
|
159
462
|
continue;
|
|
160
463
|
}
|
|
161
464
|
const repoId = resourceRepo.repository.id;
|
|
465
|
+
if (!repoId)
|
|
466
|
+
continue;
|
|
162
467
|
const repo = await gitDataProviderInstance.GetGitRepoFromRepoId(repoId);
|
|
163
468
|
const resourceRepository = {
|
|
164
469
|
repoName: repo.name,
|
|
165
470
|
repoSha1: resourceRepo.version,
|
|
166
471
|
url: repo.url,
|
|
167
472
|
};
|
|
168
|
-
|
|
169
|
-
|
|
473
|
+
const key = String(repoId);
|
|
474
|
+
if (!resourceRepositoriesById.has(key)) {
|
|
475
|
+
resourceRepositoriesById.set(key, resourceRepository);
|
|
170
476
|
}
|
|
171
477
|
}
|
|
172
|
-
return [...
|
|
478
|
+
return [...resourceRepositoriesById.values()];
|
|
173
479
|
}
|
|
174
480
|
/**
|
|
175
481
|
* Retrieves the details of a specific pipeline build by its build ID.
|
|
@@ -269,7 +575,7 @@ class PipelinesDataProvider {
|
|
|
269
575
|
//Filter successful builds only
|
|
270
576
|
let { value } = res;
|
|
271
577
|
if (value) {
|
|
272
|
-
const successfulRunHistory = value.filter((run) => run.result !== 'failed'
|
|
578
|
+
const successfulRunHistory = value.filter((run) => run.result !== 'failed' && run.result !== 'canceled');
|
|
273
579
|
return { count: successfulRunHistory.length, value: successfulRunHistory };
|
|
274
580
|
}
|
|
275
581
|
return res;
|