Skip to content

Commit b88f0d8

Browse files
release: automatically discover artifact
If the artifact-name input is not provided, use github API to retrieve the job runs associated with the commit being released to build the artifact name. Issue: ZENKO-5046
1 parent fb42b7e commit b88f0d8

File tree

6 files changed

+483
-8
lines changed

6 files changed

+483
-8
lines changed

.github/scripts/get-build-artifact.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Auto-derive artifacts name from successful staging build
3+
*
4+
* This script is used in GitHub Actions to automatically find and construct
5+
* the artifacts name from a successful staging workflow run that contains
6+
* the build-iso-and-end2end-test job.
7+
*
8+
* @param {Object} github - GitHub Actions toolkit github object
9+
* @param {Object} context - GitHub Actions context object
10+
* @param {Object} core - GitHub Actions core object for logging/errors
11+
* @returns {Promise<string>} The constructed artifacts name
12+
*/
13+
async function getBuildArtifact(github, context, core) {
14+
const workflow_id = 'build-iso-and-end2end-test'; // The workflow ID for the build job
15+
16+
// Get the commit SHA for the tag
17+
const tagCommit = context.sha;
18+
core.info(`Looking for successful builds for commit: ${tagCommit}`);
19+
20+
// Get workflow runs for this repository``
21+
const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({
22+
owner: context.repo.owner,
23+
repo: context.repo.repo,
24+
workflow_id,
25+
head_sha: tagCommit,
26+
status: 'completed',
27+
conclusion: 'success'
28+
});
29+
30+
// Find the first successful staging workflow run
31+
const run = workflowRuns.workflow_runs[0];
32+
if (!run) {
33+
throw new Error(`No successful end2end workflow run found for commit ${tagCommit}`);
34+
}
35+
36+
core.info(`Found staging run: ${run.id} with conclusion: ${run.conclusion}`);
37+
38+
// Construct artifacts name
39+
const commitHash = tagCommit.substring(0, 7); // Use first 7 chars of commit hash
40+
const buildNumber = run.run_number;
41+
const artifactsName = `github:${context.repo.owner}:${context.repo.repo}:staging-${commitHash}.${workflow_id}.${buildNumber}`;
42+
43+
core.info(`Auto-derived artifacts name: ${artifactsName}`);
44+
return artifactsName;
45+
}
46+
47+
module.exports = { getBuildArtifact };
48+
49+
// For TypeScript imports
50+
if (typeof exports !== 'undefined') {
51+
exports.getBuildArtifact = getBuildArtifact;
52+
}

.github/workflows/release.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ on:
99
description: Tag
1010
required: true
1111
artifacts-name:
12-
description: Artifacts name to promote
13-
required: true
12+
description: Artifacts name to promote (optional - will be auto-detected if not provided)
13+
required: false
1414

1515
jobs:
1616
verify-release:
1717
name: Verify if tag is valid
1818
runs-on: ubuntu-24.04
19+
outputs:
20+
artifacts-name: ${{ steps.get-artifacts-name.outputs.artifacts-name }}
1921
steps:
2022
- name: Checkout
2123
uses: actions/checkout@v4
@@ -42,6 +44,18 @@ jobs:
4244
run: |
4345
! git show-ref --tags ${{ github.event.inputs.tag }} --quiet
4446
47+
- name: Auto-derive artifacts name if not provided
48+
uses: actions/github-script@v7
49+
id: get-artifacts-name
50+
with:
51+
script: |
52+
const { getBuildArtifact } = require('./.github/scripts/get-build-artifact');
53+
const artifactsName = process.env.ARTIFACTS_NAME
54+
|| await getBuildArtifact(github, context, core);
55+
core.setOutput('artifacts-name', artifactsName);
56+
env:
57+
ARTIFACTS_NAME: ${{ github.event.inputs.artifacts-name }}
58+
4559
release:
4660
name: Release
4761
runs-on: ubuntu-24.04
@@ -114,7 +128,7 @@ jobs:
114128
uses: scality/action-artifacts@v4
115129
with:
116130
method: promote
117-
name: ${{ github.event.inputs.artifacts-name }}
131+
name: ${{ needs.verify-release.outputs.artifacts-name }}
118132
tag: ${{ github.event.inputs.tag }}
119133
url: https://artifacts.scality.net
120134
user: ${{ secrets.ARTIFACTS_USER }}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// eslint-disable-next-line @typescript-eslint/no-var-requires
2+
const { getBuildArtifact } = require('../../.github/scripts/get-build-artifact');
3+
import * as sinon from 'sinon';
4+
5+
describe('getBuildArtifact', () => {
6+
let mockGithub: any;
7+
let mockContext: any;
8+
let mockCore: any;
9+
let sandbox: sinon.SinonSandbox;
10+
11+
beforeEach(() => {
12+
sandbox = sinon.createSandbox();
13+
14+
// Create mock GitHub API client
15+
mockGithub = {
16+
rest: {
17+
actions: {
18+
listWorkflowRuns: sandbox.stub()
19+
}
20+
}
21+
};
22+
23+
mockContext = {
24+
repo: {
25+
owner: 'scality',
26+
repo: 'zenko',
27+
},
28+
sha: 'abcd1234567890abcdef1234567890abcdefabcd',
29+
};
30+
31+
mockCore = {
32+
info: sandbox.stub(),
33+
exportVariable: sandbox.stub(),
34+
};
35+
});
36+
37+
afterEach(() => {
38+
sandbox.restore();
39+
});
40+
41+
it('should construct correct artifacts name from successful run', async () => {
42+
mockGithub.rest.actions.listWorkflowRuns.resolves({
43+
data: {
44+
total_count: 1,
45+
workflow_runs: [
46+
{
47+
id: 12345,
48+
name: 'build-iso-and-end2end-test',
49+
conclusion: 'success',
50+
run_number: 678,
51+
status: 'completed',
52+
head_sha: 'abcd1234567890abcdef1234567890abcdefabcd',
53+
workflow_id: 1,
54+
url: 'https://api.github.com/repos/scality/zenko/actions/runs/12345',
55+
html_url: 'https://github.com/scality/zenko/actions/runs/12345',
56+
created_at: '2023-01-01T00:00:00Z',
57+
updated_at: '2023-01-01T00:00:00Z',
58+
run_started_at: '2023-01-01T00:00:00Z',
59+
},
60+
],
61+
},
62+
});
63+
64+
const result = await getBuildArtifact(mockGithub, mockContext, mockCore);
65+
expect(result).toBe('github:scality:zenko:staging-abcd123.build-iso-and-end2end-test.678');
66+
67+
// Verify the API call was made with correct parameters
68+
expect(mockGithub.rest.actions.listWorkflowRuns.calledOnce).toBe(true);
69+
expect(mockGithub.rest.actions.listWorkflowRuns.calledWith({
70+
owner: 'scality',
71+
repo: 'zenko',
72+
workflow_id: 'build-iso-and-end2end-test',
73+
head_sha: 'abcd1234567890abcdef1234567890abcdefabcd',
74+
status: 'completed',
75+
conclusion: 'success'
76+
})).toBe(true);
77+
78+
// Verify logging calls
79+
expect(mockCore.info.calledWith('Looking for successful builds for commit: abcd1234567890abcdef1234567890abcdefabcd')).toBe(true);
80+
expect(mockCore.info.calledWith('Found staging run: 12345 with conclusion: success')).toBe(true);
81+
expect(mockCore.info.calledWith('Auto-derived artifacts name: github:scality:zenko:staging-abcd123.build-iso-and-end2end-test.678')).toBe(true);
82+
});
83+
84+
it('should fail when no staging workflow run is found', async () => {
85+
mockGithub.rest.actions.listWorkflowRuns.resolves({
86+
data: {
87+
total_count: 0,
88+
workflow_runs: [],
89+
},
90+
});
91+
92+
await expect(getBuildArtifact(mockGithub, mockContext, mockCore))
93+
.rejects
94+
.toThrow('No successful end2end workflow run found for commit abcd1234567890abcdef1234567890abcdefabcd');
95+
96+
// Verify the API call was made with correct parameters
97+
expect(mockGithub.rest.actions.listWorkflowRuns.calledOnce).toBe(true);
98+
expect(mockGithub.rest.actions.listWorkflowRuns.calledWith({
99+
owner: 'scality',
100+
repo: 'zenko',
101+
workflow_id: 'build-iso-and-end2end-test',
102+
head_sha: 'abcd1234567890abcdef1234567890abcdefabcd',
103+
status: 'completed',
104+
conclusion: 'success'
105+
})).toBe(true);
106+
});
107+
108+
it('should handle workflow run with multiple workflows', async () => {
109+
mockGithub.rest.actions.listWorkflowRuns.resolves({
110+
data: {
111+
total_count: 2,
112+
workflow_runs: [
113+
{
114+
id: 56789,
115+
name: 'build-iso-and-end2end-test',
116+
conclusion: 'success',
117+
run_number: 999,
118+
status: 'completed',
119+
head_sha: 'abcd1234567890abcdef1234567890abcdefabcd',
120+
url: 'https://api.github.com/repos/scality/zenko/actions/runs/56789',
121+
html_url: 'https://github.com/scality/zenko/actions/runs/56789',
122+
created_at: '2024-01-01T00:00:00Z',
123+
updated_at: '2024-01-01T00:00:00Z',
124+
run_started_at: '2024-01-01T00:00:00Z',
125+
},
126+
{
127+
id: 12345,
128+
name: 'build-iso-and-end2end-test',
129+
conclusion: 'success',
130+
run_number: 678,
131+
status: 'completed',
132+
head_sha: 'abcd1234567890abcdef1234567890abcdefabcd',
133+
workflow_id: 1,
134+
url: 'https://api.github.com/repos/scality/zenko/actions/runs/12345',
135+
html_url: 'https://github.com/scality/zenko/actions/runs/12345',
136+
created_at: '2023-01-01T00:00:00Z',
137+
updated_at: '2023-01-01T00:00:00Z',
138+
run_started_at: '2023-01-01T00:00:00Z',
139+
},
140+
],
141+
},
142+
});
143+
144+
const result = await getBuildArtifact(mockGithub, mockContext, mockCore);
145+
146+
expect(result).toBe('github:scality:zenko:staging-abcd123.build-iso-and-end2end-test.999');
147+
expect(mockCore.info.calledWith('Found staging run: 56789 with conclusion: success')).toBe(true);
148+
});
149+
});

tests/workflows/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
"devDependencies": {
1111
"@kie/act-js": "^2.6.1",
1212
"@kie/mock-github": "^2.0.1",
13+
"@octokit/rest": "^19.0.4",
1314
"@types/jest": "^29.1.2",
1415
"@types/node": "^18.8.5",
16+
"@types/sinon": "^10.0.0",
1517
"jest": "^29.1.2",
18+
"sinon": "^15.0.0",
1619
"ts-jest": "^29.0.3",
1720
"ts-node": "^10.9.1",
1821
"tsc": "^2.0.4",

tests/workflows/release.spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ function withGitTag(tag: string, repo: string = 'zenko') {
2727

2828
function withArtifact(artifactsName: string) {
2929
const f = () => act.setInput("artifacts-name", artifactsName);
30-
f.toString = () => " and artifact " + artifactsName
30+
f.toString = () => " and artifact " + artifactsName;
31+
return f;
32+
}
33+
34+
function withoutArtifact() {
35+
const f = () => act.deleteInput("artifacts-name");
36+
f.toString = () => " and no artifact";
3137
return f;
3238
}
3339

@@ -138,6 +144,7 @@ test.each([
138144
['Promote artifacts', Fail, '2.3.7-rc.1', withArtifact('github:scality:Zenko:staging-ac5768a8c6.build-iso-and-end2end-test.3454')],
139145
['Promote artifacts', Pass, '2.3.7-rc.1', ''],
140146
['Promote artifacts', Pass, '2.3.7', withVersionFile("VERSION-2.3.7")],
147+
['Promote artifacts', Pass, '2.3.7-rc.1', withoutArtifact()],
141148
])("%s should %s when version is %s%s", async (stepName, status, tag, ...configs) => {
142149

143150
for(var c of configs.filter(c => !!c)) {
@@ -162,6 +169,25 @@ test.each([
162169
.setIndex()
163170
.reply({ status: 200, data: "PASSED\n", repeat: 2 }),
164171

172+
// Mock automatic artifact discovery
173+
moctokit.rest.actions
174+
.listWorkflowRuns()
175+
.reply({
176+
status: 200,
177+
data: {
178+
total_count: 1,
179+
workflow_runs: [{
180+
id: 1234,
181+
conclusion: "success",
182+
head_branch: "development/2.3",
183+
head_sha: await getCommitHash(),
184+
name: "build-iso-and-end2end-test",
185+
run_number: 3454,
186+
status: "completed",
187+
}],
188+
}
189+
}),
190+
165191
// Mock release notes generation
166192
moctokit.rest.repos
167193
.listReleases()

0 commit comments

Comments
 (0)