Skip to content

Commit 7c54f0f

Browse files
authored
Added status check for spec-gen-sdk (Azure#34179)
* Added status check for spec-gen-sdk * removed hardcode artifact name * fixed lint error and revert exitcode change * fixed prettier issues * added tests and abstracted common code * added tests for fetchFailedArtifact function * using core.summary to write job summary * fixed lint issue and prettier issue
1 parent 17365a7 commit 7c54f0f

File tree

11 files changed

+1483
-33
lines changed

11 files changed

+1483
-33
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: "[TEST IGNORE] spec-gen-sdk status"
2+
3+
on:
4+
check_run:
5+
types: [completed]
6+
7+
permissions:
8+
contents: read
9+
statuses: write
10+
11+
jobs:
12+
spec-gen-sdk-status:
13+
# Only run this job when the check run is from Azure Pipelines SDK Generation job in the azure-sdk/public(id: 29ec6040-b234-4e31-b139-33dc4287b756) project
14+
if: |
15+
github.event.check_run.check_suite.app.name == 'Azure Pipelines' &&
16+
contains(github.event.check_run.name, 'SDK Generation') &&
17+
endsWith(github.event.check_run.external_id, '29ec6040-b234-4e31-b139-33dc4287b756')
18+
name: "[TEST IGNORE] spec-gen-sdk - set status"
19+
runs-on: ubuntu-24.04
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
sparse-checkout: |
24+
.github
25+
26+
- name: "spec-gen-sdk - set status"
27+
id: spec-gen-sdk-status
28+
uses: actions/github-script@v7
29+
with:
30+
script: |
31+
const setStatus =
32+
(await import('${{ github.workspace }}/.github/workflows/src/spec-gen-sdk-status.js')).default;
33+
return await setStatus({ github, context, core });

.github/workflows/src/artifacts.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// @ts-check
2+
import { fetchWithRetry } from "./retries.js";
3+
4+
/**
5+
* @typedef {Object} ArtifactResource
6+
* @property {string} [downloadUrl]
7+
*/
8+
9+
/**
10+
* @typedef {Object} ArtifactValue
11+
* @property {string} name - The name of the artifact
12+
* @property {string} [id] - The ID of the artifact
13+
* @property {ArtifactResource} [resource] - The resource containing download information
14+
*/
15+
16+
/**
17+
* @typedef {Object} Artifacts
18+
* @property {ArtifactResource} [resource] - For single artifact responses
19+
*/
20+
21+
/**
22+
* @typedef {Object} ListArtifactsResponse
23+
* @property {Array<ArtifactValue>} value
24+
*/
25+
26+
/**
27+
* @param {Object} params
28+
* @param {string} params.ado_build_id
29+
* @param {string} params.ado_project_url
30+
* @param {string} params.artifactName
31+
* @param {string} params.artifactFileName
32+
* @param {typeof import("@actions/core")} params.core
33+
* @param {import('./retries.js').RetryOptions} [params.retryOptions]
34+
* @param {boolean} [params.fallbackToFailedArtifact]
35+
* @returns {Promise<{artifactData: string}>}
36+
*/
37+
export async function getAzurePipelineArtifact({
38+
ado_build_id,
39+
ado_project_url,
40+
artifactName,
41+
artifactFileName,
42+
core,
43+
retryOptions = {},
44+
fallbackToFailedArtifact = false,
45+
}) {
46+
let apiUrl = `${ado_project_url}/_apis/build/builds/${ado_build_id}/artifacts?artifactName=${artifactName}&api-version=7.0`;
47+
core.info(`Calling Azure DevOps API to get the artifact: ${apiUrl}`);
48+
49+
let artifactData = "";
50+
// Use Node.js fetch with retry to call the API
51+
let response = await fetchWithRetry(
52+
apiUrl,
53+
{
54+
method: "GET",
55+
headers: {
56+
"Content-Type": "application/json",
57+
},
58+
},
59+
retryOptions,
60+
);
61+
62+
// If the response is 404, check if we should fallback to the failed artifact
63+
if (response.status === 404) {
64+
if (!fallbackToFailedArtifact) {
65+
core.info(`Artifact '${artifactName}' not found (404)`);
66+
return { artifactData };
67+
} else {
68+
response = await fetchFailedArtifact({
69+
ado_build_id,
70+
ado_project_url,
71+
artifactName,
72+
core,
73+
retryOptions,
74+
});
75+
}
76+
}
77+
78+
if (response.ok) {
79+
// Step 1: Get the download URL for the artifact
80+
/** @type {Artifacts} */
81+
const artifacts = /** @type {Artifacts} */ (await response.json());
82+
core.info(`Artifacts found: ${JSON.stringify(artifacts)}`);
83+
if (!artifacts.resource || !artifacts.resource.downloadUrl) {
84+
throw new Error(
85+
`Download URL not found for the artifact ${artifactName}`,
86+
);
87+
}
88+
89+
let downloadUrl = artifacts.resource.downloadUrl;
90+
const index = downloadUrl.indexOf("?format=zip");
91+
if (index !== -1) {
92+
// Keep everything up to (but not including) "?format=zip"
93+
downloadUrl = downloadUrl.substring(0, index);
94+
}
95+
downloadUrl += `?format=file&subPath=/${artifactFileName}`;
96+
core.info(`Downloading artifact from: ${downloadUrl}`);
97+
98+
// Step 2: Fetch Artifact Content (as a Buffer) with retry
99+
const artifactResponse = await fetchWithRetry(
100+
downloadUrl,
101+
{},
102+
{ logger: core.info },
103+
);
104+
if (!artifactResponse.ok) {
105+
throw new Error(
106+
`Failed to fetch artifact: ${artifactResponse.statusText}`,
107+
);
108+
}
109+
110+
artifactData = await artifactResponse.text();
111+
} else {
112+
core.error(
113+
`Failed to fetch artifacts: ${response.status}, ${response.statusText}`,
114+
);
115+
const errorText = await response.text();
116+
core.error(`Error details: ${errorText}`);
117+
}
118+
return { artifactData };
119+
}
120+
121+
/**
122+
* Extracts the ADO build ID and project URL from the given build URL.
123+
* @param {string} buildUrl
124+
* @returns {{projectUrl: string, buildId: string}}
125+
* @throws {Error} If the build URL does not match the expected format.
126+
*/
127+
export function getAdoBuildInfoFromUrl(buildUrl) {
128+
// Extract the ADO build ID and project URL from the check run details URL
129+
const buildUrlRegex = /^(.*?)(?=\/_build\/).*?[?&]buildId=(\d+)/;
130+
const match = buildUrl.match(buildUrlRegex);
131+
if (!match) {
132+
throw new Error(
133+
`Could not extract build ID or project URL from the URL: ${buildUrl}`,
134+
);
135+
}
136+
return { projectUrl: match[1], buildId: match[2] };
137+
}
138+
139+
/**
140+
* @param {Object} params
141+
* @param {string} params.ado_build_id
142+
* @param {string} params.ado_project_url
143+
* @param {string} params.artifactName
144+
* @param {typeof import("@actions/core")} params.core
145+
* @param {import('./retries.js').RetryOptions} [params.retryOptions]
146+
* @returns {Promise<Response>}
147+
*/
148+
export async function fetchFailedArtifact({
149+
ado_build_id,
150+
ado_project_url,
151+
artifactName,
152+
core,
153+
retryOptions = {},
154+
}) {
155+
// fallback to fetch the failed artifact
156+
let apiUrl = `${ado_project_url}/_apis/build/builds/${ado_build_id}/artifacts?api-version=7.0`;
157+
core.info(`List the artifacts: ${apiUrl}`);
158+
let response = await fetchWithRetry(
159+
apiUrl,
160+
{
161+
method: "GET",
162+
headers: {
163+
"Content-Type": "application/json",
164+
},
165+
},
166+
retryOptions,
167+
);
168+
if (!response.ok) {
169+
throw new Error(
170+
`Failed to fetch artifacts: ${response.status}, ${response.statusText}`,
171+
);
172+
}
173+
/** @type {ListArtifactsResponse} */
174+
const listArtifactResponse = /** @type {ListArtifactsResponse} */ (
175+
await response.json()
176+
);
177+
core.info(`Artifacts found: ${JSON.stringify(listArtifactResponse)}`);
178+
// Use filter to get matching artifacts and sort them in descending alphabetical order
179+
const artifactsList = listArtifactResponse.value
180+
.filter((artifact) => artifact.name.includes(artifactName))
181+
.sort((a, b) => b.name.localeCompare(a.name)); // Descending order (Z to A)
182+
if (artifactsList.length === 0) {
183+
throw new Error(`No artifacts found with name containing ${artifactName}`);
184+
}
185+
artifactName = artifactsList[0].name;
186+
apiUrl = `${ado_project_url}/_apis/build/builds/${ado_build_id}/artifacts?artifactName=${artifactName}&api-version=7.0`;
187+
core.info(`Fetching the failed artifact: ${apiUrl}`);
188+
return await fetchWithRetry(
189+
apiUrl,
190+
{
191+
method: "GET",
192+
headers: {
193+
"Content-Type": "application/json",
194+
},
195+
},
196+
retryOptions,
197+
);
198+
}

.github/workflows/src/github.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// @ts-check
2-
32
export const PER_PAGE_MAX = 100;
43

54
/**
@@ -120,3 +119,17 @@ export const CommitStatusState = {
120119
*/
121120
SUCCESS: "success",
122121
};
122+
123+
/**
124+
* Writes content to the GitHub Actions summary
125+
* @param {string} content - Markdown content to add to the summary
126+
* @param {typeof import("@actions/core")} core - GitHub Actions core library
127+
*/
128+
export async function writeToActionsSummary(content, core) {
129+
try {
130+
await core.summary.addRaw(content).write();
131+
core.info("Successfully wrote to the GitHub Actions summary");
132+
} catch (error) {
133+
throw new Error(`Failed to write to the GitHub Actions summary: ${error}`);
134+
}
135+
}

0 commit comments

Comments
 (0)