|
| 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 }; |
0 commit comments