Skip to content

Commit 8727a29

Browse files
committed
promote: add check_r2_assets tool
Add tool and tests for checking if assets on R2 look ready for promotion. Relies on `rclone` being available -- there's a test for the case where it is not. The new tool is based on existing `check_assets.js` but: - Written in ESM. - Uses built-in Set objects instead of emulating sets with Arrays. - Always assumes an asset file exists. `check_assets.js` has some fallback logic, but it's not straightforward and nowadays we can consider a missing asset file to be a process failure. - Reflects that `.done` files are not used for R2 (basically they don't exist so no handling logic). Also note that for R2, promotion is a copy and not a move so after a partial promotion subsequent checks will result in overwrite warnings for files previously promoted. - `SHASUMS256.txt` can end up in the staging directory. Ignore for the staging directory as `check_assets.js` does for dist. (The shasums are generated by later parts of the release process.)
1 parent 67f98e1 commit 8727a29

9 files changed

+618
-1
lines changed

.github/workflows/check_assets-tool.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ on:
66
- '.github/workflows/check_assets-tool.yml'
77
- 'ansible/www-standalone/tools/promote/expected_assets/*'
88
- 'ansible/www-standalone/tools/promote/check_assets*'
9+
- 'ansible/www-standalone/tools/promote/check_r2_assets*'
10+
- 'ansible/www-standalone/tools/promote/test/**'
911
push:
1012
paths:
1113
- '.github/workflows/check_assets-tool.yml'
1214
- 'ansible/www-standalone/tools/promote/expected_assets/*'
1315
- 'ansible/www-standalone/tools/promote/check_assets*'
16+
- 'ansible/www-standalone/tools/promote/check_r2_assets*'
17+
- 'ansible/www-standalone/tools/promote/test/**'
1418
schedule:
1519
- cron: 0 0 * * *
1620
workflow_dispatch:
@@ -35,5 +39,5 @@ jobs:
3539
with:
3640
node-version: ${{ env.NODE_VERSION }}
3741
- name: Run tests
38-
run: node --test
42+
run: node --test --experimental-test-module-mocks
3943
working-directory: ansible/www-standalone/tools/promote/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env node
2+
3+
import { exec } from 'node:child_process';
4+
import { readFile } from 'node:fs/promises';
5+
import { basename, join } from 'node:path';
6+
7+
const versionRe = /^v\d+\.\d+\.\d+/
8+
// These are normally generated as part of the release process after the asset
9+
// check, but may be present if a release has already been partially promoted.
10+
const additionalAssets = new Set([
11+
'SHASUMS256.txt',
12+
'SHASUMS256.txt.asc',
13+
'SHASUMS256.txt.sig'
14+
]);
15+
16+
if (process.argv[1] === import.meta.filename) {
17+
checkArgs(process.argv).then(run(process.argv[2], process.argv[3])).catch(console.error)
18+
}
19+
20+
async function checkArgs (argv) {
21+
let bad = false;
22+
if (!argv || argv.length < 4) {
23+
bad = true;
24+
} else {
25+
if (!versionRe.test(basename(argv[2]))) {
26+
bad = true;
27+
console.error(`Bad staging directory name: ${argv[2]}`);
28+
}
29+
if (!versionRe.test(basename(argv[3]))) {
30+
bad = true;
31+
console.error(`Bad dist directory name: ${argv[3]}`);
32+
}
33+
}
34+
if (bad) {
35+
console.error(`Usage: ${basename(import.meta.filename)} <path to staging directory> <path to dist directory>`);
36+
process.exit(1);
37+
}
38+
}
39+
40+
async function loadExpectedAssets (version, line) {
41+
try {
42+
const templateFile = join(import.meta.dirname, 'expected_assets', line);
43+
let files = await readFile(templateFile, 'utf8');
44+
return files.replace(/{VERSION}/g, version).split(/\n/g).filter(Boolean);
45+
} catch (e) { }
46+
return null;
47+
}
48+
49+
async function lsRemoteDepth2 (dir) {
50+
return new Promise((resolve, reject) => {
51+
const command = `rclone lsjson ${dir} --no-modtime --no-mimetype -R --max-depth 2`;
52+
exec(command, {}, (err, stdout, stderr) => {
53+
if (err) {
54+
return reject(err);
55+
}
56+
if (stderr) {
57+
console.log('STDERR:', stderr);
58+
}
59+
const assets = JSON.parse(stdout).map(({ Path, IsDir }) => {
60+
if (IsDir) {
61+
return `${Path}/`;
62+
}
63+
return Path;
64+
})
65+
resolve(assets);
66+
});
67+
});
68+
}
69+
70+
async function run (stagingDir, distDir) {
71+
const version = basename(stagingDir);
72+
const line = versionToLine(version);
73+
const stagingAssets = new Set(await lsRemoteDepth2(stagingDir)).difference(additionalAssets);
74+
const distAssets = new Set((await lsRemoteDepth2(distDir))).difference(additionalAssets);
75+
const expectedAssets = new Set(await loadExpectedAssets(version, line));
76+
77+
let caution = false
78+
let update = false
79+
80+
// generate comparison lists
81+
const stagingDistIntersection = stagingAssets.intersection(distAssets);
82+
const stagingDistUnion = stagingAssets.union(distAssets);
83+
let notInActual = expectedAssets.difference(stagingAssets);
84+
let stagingNotInExpected = stagingAssets.difference(expectedAssets);
85+
let distNotInExpected = distAssets.difference(expectedAssets);
86+
87+
console.log('... Checking R2 assets');
88+
// No expected asset list available for this line
89+
if (expectedAssets.size === 0) {
90+
console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m No expected asset list is available for ${line}, does one need to be created?`);
91+
console.log(` https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/${line}`);
92+
return;
93+
}
94+
95+
console.log(`... Expecting a total of ${expectedAssets.size} assets for ${line}`);
96+
console.log(`... ${stagingAssets.size} assets waiting in R2 staging`);
97+
98+
// what might be overwritten by promotion?
99+
if (stagingDistIntersection.size) {
100+
caution = true;
101+
console.log(` \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m ${stagingDistIntersection.size} assets already promoted in R2 will be overwritten, is this OK?`);
102+
if (stagingDistIntersection.size <= 10) {
103+
stagingDistIntersection.forEach((a) => console.log(` • ${a}`));
104+
}
105+
} else {
106+
console.log(`... ${distAssets.size} assets already promoted in R2`);
107+
}
108+
109+
if (!notInActual.size) { // perfect staging state, we have everything we need
110+
console.log(` \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for ${line}`);
111+
} else { // missing some assets and they're not in staging, are you impatient?
112+
caution = true;
113+
console.log(` \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m The following assets are expected for ${line} but are currently missing from R2 staging:`);
114+
notInActual.forEach((a) => console.log(` • ${a}`));
115+
}
116+
117+
// bogus unexpected files found in staging, not good
118+
if (stagingNotInExpected.size) {
119+
caution = true
120+
update = true
121+
console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m The following assets were found in R2 staging but are not expected for ${line}:`)
122+
stagingNotInExpected.forEach((a) => console.log(` • ${a}`))
123+
}
124+
125+
// bogus unexpected files found in dist, not good
126+
if (distNotInExpected.size) {
127+
caution = true
128+
update = true
129+
console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m The following assets were already promoted in R2 but are not expected for ${line}:`)
130+
distNotInExpected.forEach((a) => console.log(` • ${a}`))
131+
}
132+
133+
// do we need to provide final notices?
134+
if (update) {
135+
console.log(` Does the expected assets list for ${line} need to be updated?`)
136+
console.log(` https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/${line}`)
137+
}
138+
if (caution) {
139+
console.log(' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m')
140+
}
141+
}
142+
143+
function versionToLine (version) {
144+
return version.replace(/^(v\d+)\.[\d.]+.*/g, '$1.x')
145+
}
146+
147+
export { checkArgs, run };

ansible/www-standalone/tools/promote/promote_release.sh

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ dirmatch=$release_dirmatch
2424

2525
node --no-warnings /home/staging/tools/promote/check_assets.js $srcdir/$2 $dstdir/$2
2626

27+
relative_srcdir=${srcdir/$staging_rootdir/"$site/"}
28+
relative_dstdir=${dstdir/$dist_rootdir/"$site/"}
29+
30+
node --no-warnings /home/staging/tools/promote/check_r2_assets.js $staging_bucket/$relative_srcdir/$2 $prod_bucket/$relative_dstdir/$2
31+
2732
while true; do
2833
echo -n "Are you sure you want to promote the $2 assets? [y/n] "
2934
yorn=""

0 commit comments

Comments
 (0)