diff --git a/.github/workflows/check_assets-tool.yml b/.github/workflows/check_assets-tool.yml index e7362b9dc..7a554873f 100644 --- a/.github/workflows/check_assets-tool.yml +++ b/.github/workflows/check_assets-tool.yml @@ -6,11 +6,15 @@ on: - '.github/workflows/check_assets-tool.yml' - 'ansible/www-standalone/tools/promote/expected_assets/*' - 'ansible/www-standalone/tools/promote/check_assets*' + - 'ansible/www-standalone/tools/promote/check_r2_assets*' + - 'ansible/www-standalone/tools/promote/test/**' push: paths: - '.github/workflows/check_assets-tool.yml' - 'ansible/www-standalone/tools/promote/expected_assets/*' - 'ansible/www-standalone/tools/promote/check_assets*' + - 'ansible/www-standalone/tools/promote/check_r2_assets*' + - 'ansible/www-standalone/tools/promote/test/**' schedule: - cron: 0 0 * * * workflow_dispatch: @@ -35,5 +39,5 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} - name: Run tests - run: node --test + run: node --test --experimental-test-module-mocks working-directory: ansible/www-standalone/tools/promote/ diff --git a/ansible/www-standalone/tools/promote/check_r2_assets.mjs b/ansible/www-standalone/tools/promote/check_r2_assets.mjs new file mode 100755 index 000000000..a63aaadbd --- /dev/null +++ b/ansible/www-standalone/tools/promote/check_r2_assets.mjs @@ -0,0 +1,147 @@ +#!/usr/bin/env node + +import { exec } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; + +const versionRe = /^v\d+\.\d+\.\d+/ +// These are normally generated as part of the release process after the asset +// check, but may be present if a release has already been partially promoted. +const additionalAssets = new Set([ + 'SHASUMS256.txt', + 'SHASUMS256.txt.asc', + 'SHASUMS256.txt.sig' +]); + +if (process.argv[1] === import.meta.filename) { + checkArgs(process.argv).then(run(process.argv[2], process.argv[3])).catch(console.error); +} + +async function checkArgs (argv) { + let bad = false; + if (!argv || argv.length < 4) { + bad = true; + } else { + if (!versionRe.test(basename(argv[2]))) { + bad = true; + console.error(`Bad staging directory name: ${argv[2]}`); + } + if (!versionRe.test(basename(argv[3]))) { + bad = true; + console.error(`Bad dist directory name: ${argv[3]}`); + } + } + if (bad) { + console.error(`Usage: ${basename(import.meta.filename)} `); + process.exit(1); + } +} + +async function loadExpectedAssets (version, line) { + try { + const templateFile = join(import.meta.dirname, 'expected_assets', line); + let files = await readFile(templateFile, 'utf8'); + return files.replace(/{VERSION}/g, version).split(/\n/g).filter(Boolean); + } catch (e) { } + return null; +} + +async function lsRemoteDepth2 (dir) { + return new Promise((resolve, reject) => { + const command = `rclone lsjson ${dir} --no-modtime --no-mimetype -R --max-depth 2`; + exec(command, {}, (err, stdout, stderr) => { + if (err) { + return reject(err); + } + if (stderr) { + console.log('STDERR:', stderr); + } + const assets = JSON.parse(stdout).map(({ Path, IsDir }) => { + if (IsDir) { + return `${Path}/`; + } + return Path; + }) + resolve(assets); + }); + }); +} + +async function run (stagingDir, distDir) { + const version = basename(stagingDir); + const line = versionToLine(version); + const stagingAssets = new Set(await lsRemoteDepth2(stagingDir)).difference(additionalAssets); + const distAssets = new Set((await lsRemoteDepth2(distDir))).difference(additionalAssets); + const expectedAssets = new Set(await loadExpectedAssets(version, line)); + + let caution = false; + let update = false; + + // generate comparison lists + const stagingDistIntersection = stagingAssets.intersection(distAssets); + const stagingDistUnion = stagingAssets.union(distAssets); + let notInActual = expectedAssets.difference(stagingAssets); + let stagingNotInExpected = stagingAssets.difference(expectedAssets); + let distNotInExpected = distAssets.difference(expectedAssets); + + console.log('... Checking R2 assets'); + // No expected asset list available for this line + if (expectedAssets.size === 0) { + console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m No expected asset list is available for ${line}, does one need to be created?`); + console.log(` https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/${line}`); + return; + } + + console.log(`... Expecting a total of ${expectedAssets.size} assets for ${line}`); + console.log(`... ${stagingAssets.size} assets waiting in R2 staging`); + + // what might be overwritten by promotion? + if (stagingDistIntersection.size) { + caution = true; + console.log(` \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m ${stagingDistIntersection.size} assets already promoted in R2 will be overwritten, is this OK?`); + if (stagingDistIntersection.size <= 10) { + stagingDistIntersection.forEach((a) => console.log(` • ${a}`)); + } + } else { + console.log(`... ${distAssets.size} assets already promoted in R2`); + } + + if (!notInActual.size) { // perfect staging state, we have everything we need + console.log(` \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for ${line}`); + } else { // missing some assets and they're not in staging, are you impatient? + caution = true; + console.log(` \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m The following assets are expected for ${line} but are currently missing from R2 staging:`); + notInActual.forEach((a) => console.log(` • ${a}`)); + } + + // bogus unexpected files found in staging, not good + if (stagingNotInExpected.size) { + caution = true; + update = true; + console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m The following assets were found in R2 staging but are not expected for ${line}:`); + stagingNotInExpected.forEach((a) => console.log(` • ${a}`)); + } + + // bogus unexpected files found in dist, not good + if (distNotInExpected.size) { + caution = true; + update = true; + console.log(` \u001b[31m\u001b[1m✖\u001b[22m\u001b[39m The following assets were already promoted in R2 but are not expected for ${line}:`); + distNotInExpected.forEach((a) => console.log(` • ${a}`)); + } + + // do we need to provide final notices? + if (update) { + console.log(` Does the expected assets list for ${line} need to be updated?`); + console.log(` https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/${line}`); + } + if (caution) { + console.log(' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m'); + } +} + +function versionToLine (version) { + return version.replace(/^(v\d+)\.[\d.]+.*/g, '$1.x'); +} + +export { checkArgs, run }; diff --git a/ansible/www-standalone/tools/promote/promote_release.sh b/ansible/www-standalone/tools/promote/promote_release.sh index f720985f9..f897a6a6a 100755 --- a/ansible/www-standalone/tools/promote/promote_release.sh +++ b/ansible/www-standalone/tools/promote/promote_release.sh @@ -24,6 +24,11 @@ dirmatch=$release_dirmatch node --no-warnings /home/staging/tools/promote/check_assets.js $srcdir/$2 $dstdir/$2 +relative_srcdir=${srcdir/$staging_rootdir/"$site/"} +relative_dstdir=${dstdir/$dist_rootdir/"$site/"} + +node --no-warnings /home/staging/tools/promote/check_r2_assets.mjs $staging_bucket/$relative_srcdir/$2 $prod_bucket/$relative_dstdir/$2 + while true; do echo -n "Are you sure you want to promote the $2 assets? [y/n] " yorn="" diff --git a/ansible/www-standalone/tools/promote/test/check_r2_assets.mjs b/ansible/www-standalone/tools/promote/test/check_r2_assets.mjs new file mode 100644 index 000000000..05a2c438c --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/check_r2_assets.mjs @@ -0,0 +1,261 @@ +import assert from 'node:assert'; +import { randomUUID } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; +import { before, beforeEach, describe, it, mock } from 'node:test'; +import { exit } from 'node:process'; + +const testcases = [ + { + name: 'No rclone', + version: 'v22.12.0', + expectedStdout: '', + setup: async function setup (context) { + const command = `rclone lsjson ${context.r2StagingDir} --no-modtime --no-mimetype -R --max-depth 2`; + context.rcloneErr = new Error(`Command failed: ${command}\n/bin/sh: line 1: rclone: command not found`); + context.rcloneErr.code = 127; + context.rcloneErr.killed = false; + context.rcloneErr.signal = null; + context.rcloneErr.cmd = command; + } + }, + { + name: 'Missing asset file', + version: 'v9.0.0', // No asset file for v9.x. + expectedStdout: + '... Checking R2 assets\n' + + ' \x1B[31m\x1B[1m✖\x1B[22m\x1B[39m No expected asset list is available for v9.x, does one need to be created?\n' + + ' https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/v9.x\n', + }, + { + name: 'Everything is in staging, nothing in dist, good to go', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + '... 0 assets already promoted in R2\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('all-present-v22.12.0.json'); + } + }, + { + name: 'Not quite everything is in staging, missing two assets, nothing in dist', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 45 assets waiting in R2 staging\n' + + '... 0 assets already promoted in R2\n' + + ' \x1B[33m\x1B[1m⚠\x1B[22m\x1B[39m The following assets are expected for v22.x but are currently missing from R2 staging:\n' + + ' • node-v22.12.0-linux-armv7l.tar.gz\n' + + ' • node-v22.12.0-linux-armv7l.tar.xz\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('partial-v22.12.0.json'); + } + }, + { + name: 'Everything is in staging and everything in dist', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + ' \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m 47 assets already promoted in R2 will be overwritten, is this OK?\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('all-present-v22.12.0.json'); + context.rcloneLs[context.r2DistDir] = await fixture('all-present-v22.12.0.json'); + } + }, + { + name: 'Everything is in dist except for the armv7l files, but they are in staging', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + ' \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m 45 assets already promoted in R2 will be overwritten, is this OK?\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('all-present-v22.12.0.json'); + context.rcloneLs[context.r2DistDir] = await fixture('partial-v22.12.0.json'); + } + }, + { + name: 'Everything is in dist except for the armv7l files, but they are in staging. Ignores SHASUMS in staging.', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + ' \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m 45 assets already promoted in R2 will be overwritten, is this OK?\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('with-shasums-v22.12.0.json'); + context.rcloneLs[context.r2DistDir] = await fixture('partial-v22.12.0.json'); + } + }, + { + name: 'Everything is in dist except for the armv7l files, but they are in staging. Ignores SHASUMS in dist.', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + ' \u001b[33m\u001b[1m⚠\u001b[22m\u001b[39m 45 assets already promoted in R2 will be overwritten, is this OK?\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('with-shasums-v22.12.0.json'); + context.rcloneLs[context.r2DistDir] = await fixture('partial-with-shasums-v22.12.0.json'); + } + }, + { + name: 'Unexpected files in dist', + version: 'v22.12.0', + expectedStdout: + '... Checking R2 assets\n' + + '... Expecting a total of 47 assets for v22.x\n' + + '... 47 assets waiting in R2 staging\n' + + '... 2 assets already promoted in R2\n' + + ' \u001b[32m\u001b[1m✓\u001b[22m\u001b[39m Complete set of expected assets in place for v22.x\n' + + ' \x1B[31m\x1B[1m✖\x1B[22m\x1B[39m The following assets were already promoted in R2 but are not expected for v22.x:\n' + + ' • foo.tar.gz\n' + + ' • bar.tar.xz\n' + + ' Does the expected assets list for v22.x need to be updated?\n' + + ' https://github.com/nodejs/build/tree/main/ansible/www-standalone/tools/promote/expected_assets/v22.x\n' + + ' \u001b[33mPromote if you are certain this is the the correct course of action\u001b[39m\n', + setup: async function setup (context) { + context.rcloneLs[context.r2StagingDir] = await fixture('all-present-v22.12.0.json'); + context.rcloneLs[context.r2DistDir] = await fixture('unexpected-files.json'); + } + }, +]; + +async function fixture (name) { + return readFile(join(import.meta.dirname, 'fixtures', name)); +}; + +describe(`${basename(import.meta.filename, '.mjs')} tests`, async () => { + let execMockFn = mock.fn(); + let check_r2_assets; + let consoleLogFn; + let consoleErrorFn; + const captureConsole = (context) => { + context.stderr = ''; + context.stdout = ''; + consoleErrorFn = mock.method(console, 'error', (text) => { context.stderr += `${text}\n` }); + consoleLogFn = mock.method(console, 'log', (text) => { context.stdout += `${text}\n` }); + }; + const restoreConsole = () => { + consoleErrorFn?.mock.restore(); + consoleLogFn?.mock.restore(); + }; + before(async () => { + mock.module('node:child_process', { + namedExports: { exec: execMockFn } + }); + // Dynamic import so that mocks are set up before the import starts. + check_r2_assets = await import('../check_r2_assets.mjs'); + }); + beforeEach(async (context) => { + context.r2StagingBucket = `r2:${randomUUID()}`; + context.r2ProdBucket = `r2:${randomUUID()}`; + context.rcloneLs = {}; + context.rcloneErr = undefined; + }); + it('No arguments', async (context) => { + const expectedStderr = + 'Usage: check_r2_assets.mjs \n'; + captureConsole(context); + const exitFunc = context.mock.fn(); + process.exit = exitFunc; + await check_r2_assets.checkArgs(); + process.exit = exit; + restoreConsole(); + assert.strictEqual(exitFunc.mock.callCount(), 1); + assert.strictEqual(context.stderr, expectedStderr); + }); + it('Insufficient number of arguments', async (context) => { + const expectedStderr = + 'Usage: check_r2_assets.mjs \n'; + captureConsole(context); + const exitFunc = context.mock.fn(); + process.exit = exitFunc; + await check_r2_assets.checkArgs([ process.execPath, 'check_r2_assets.mjs' ]); + process.exit = exit; + restoreConsole(); + assert.strictEqual(exitFunc.mock.callCount(), 1); + assert.strictEqual(context.stderr, expectedStderr); + }); + it('Bad staging directory', async (context) => { + const expectedStderr = + 'Bad staging directory name: foo\n' + + 'Usage: check_r2_assets.mjs \n'; + captureConsole(context); + const exitFunc = context.mock.fn(); + process.exit = exitFunc; + await check_r2_assets.checkArgs([ process.execPath, 'check_r2_assets.mjs', 'foo', 'v22.12.0' ]); + process.exit = exit; + restoreConsole(); + assert.strictEqual(exitFunc.mock.callCount(), 1); + assert.strictEqual(context.stderr, expectedStderr); + }); + it('Bad dist directory', async (context) => { + const expectedStderr = + 'Bad dist directory name: bar\n' + + 'Usage: check_r2_assets.mjs \n'; + captureConsole(context); + const exitFunc = context.mock.fn(); + process.exit = exitFunc; + await check_r2_assets.checkArgs([ process.execPath, 'check_r2_assets.mjs', 'v22.12.0', 'bar' ]); + process.exit = exit; + restoreConsole(); + assert.strictEqual(exitFunc.mock.callCount(), 1); + assert.strictEqual(context.stderr, expectedStderr); + }); + it('Bad staging and dist directories', async (context) => { + const expectedStderr = + 'Bad staging directory name: foo\n' + + 'Bad dist directory name: bar\n' + + 'Usage: check_r2_assets.mjs \n'; + captureConsole(context); + const exitFunc = context.mock.fn(); + process.exit = exitFunc; + await check_r2_assets.checkArgs([ process.execPath, 'check_r2_assets.mjs', 'foo', 'bar' ]); + process.exit = exit; + restoreConsole(); + assert.strictEqual(exitFunc.mock.callCount(), 1); + assert.strictEqual(context.stderr, expectedStderr); + }); + for (const { name, version, expectedStdout, setup } of testcases) { + it(name, async (context) => { + context.version = version; + context.r2DistDir = join(context.r2ProdBucket, 'nodejs', 'release', version); + context.r2StagingDir = join(context.r2StagingBucket, 'nodejs', 'release', version); + await setup?.(context); + execMockFn.mock.mockImplementation(async (command, opts, cb) => { + if (context.rcloneErr) { + return cb(context.rcloneErr); + } + const [ _, stdout ] = Object.entries(context.rcloneLs)?.find(([ key, _ ]) => command.includes(key)) || [ , '[\n]\n' ]; + return cb(undefined, stdout); + }); + captureConsole(context); + try { + await check_r2_assets.run(context.r2StagingDir, context.r2DistDir); + } catch (err) { + assert.strictEqual(err, context.rcloneErr); + } finally { + restoreConsole(); + } + assert.strictEqual(context.stdout, expectedStdout); + }); + } +}); diff --git a/ansible/www-standalone/tools/promote/test/fixtures/all-present-v22.12.0.json b/ansible/www-standalone/tools/promote/test/fixtures/all-present-v22.12.0.json new file mode 100644 index 000000000..d3bddcfd3 --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/fixtures/all-present-v22.12.0.json @@ -0,0 +1,49 @@ +[ + {"Path":"docs","Name":"docs","Size":0,"ModTime":"","IsDir":true}, + {"Path":"node-v22.12.0-aix-ppc64.tar.gz","Name":"node-v22.12.0-aix-ppc64.tar.gz","Size":69853847,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-arm64.msi","Name":"node-v22.12.0-arm64.msi","Size":26984448,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.gz","Name":"node-v22.12.0-darwin-arm64.tar.gz","Size":48568612,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.xz","Name":"node-v22.12.0-darwin-arm64.tar.xz","Size":25100836,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.gz","Name":"node-v22.12.0-darwin-x64.tar.gz","Size":49317806,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.xz","Name":"node-v22.12.0-darwin-x64.tar.xz","Size":26764560,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.gz","Name":"node-v22.12.0-headers.tar.gz","Size":8778770,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.xz","Name":"node-v22.12.0-headers.tar.xz","Size":532904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.gz","Name":"node-v22.12.0-linux-arm64.tar.gz","Size":53504891,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.xz","Name":"node-v22.12.0-linux-arm64.tar.xz","Size":28591324,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-armv7l.tar.gz","Name":"node-v22.12.0-linux-armv7l.tar.gz","Size":49095271,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-armv7l.tar.xz","Name":"node-v22.12.0-linux-armv7l.tar.xz","Size":25375440,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.gz","Name":"node-v22.12.0-linux-ppc64le.tar.gz","Size":56745592,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.xz","Name":"node-v22.12.0-linux-ppc64le.tar.xz","Size":30244600,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.gz","Name":"node-v22.12.0-linux-s390x.tar.gz","Size":54242893,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.xz","Name":"node-v22.12.0-linux-s390x.tar.xz","Size":28320236,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.gz","Name":"node-v22.12.0-linux-x64.tar.gz","Size":53958425,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.xz","Name":"node-v22.12.0-linux-x64.tar.xz","Size":29734248,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.7z","Name":"node-v22.12.0-win-arm64.7z","Size":19480710,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.zip","Name":"node-v22.12.0-win-arm64.zip","Size":30538928,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.7z","Name":"node-v22.12.0-win-x64.7z","Size":21728036,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.zip","Name":"node-v22.12.0-win-x64.zip","Size":34872043,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.7z","Name":"node-v22.12.0-win-x86.7z","Size":19991434,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.zip","Name":"node-v22.12.0-win-x86.zip","Size":32316847,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x64.msi","Name":"node-v22.12.0-x64.msi","Size":30818304,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x86.msi","Name":"node-v22.12.0-x86.msi","Size":28155904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.pkg","Name":"node-v22.12.0.pkg","Size":87448749,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.gz","Name":"node-v22.12.0.tar.gz","Size":97731119,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.xz","Name":"node-v22.12.0.tar.xz","Size":47625776,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64","Name":"win-arm64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x64","Name":"win-x64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86","Name":"win-x86","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86/node.exe","Name":"node.exe","Size":73998992,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.lib","Name":"node.lib","Size":2678702,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.7z","Name":"node_pdb.7z","Size":51000521,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.zip","Name":"node_pdb.zip","Size":88377615,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.exe","Name":"node.exe","Size":74058904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.lib","Name":"node.lib","Size":2675724,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.7z","Name":"node_pdb.7z","Size":48719554,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.zip","Name":"node_pdb.zip","Size":84899182,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.exe","Name":"node.exe","Size":82818704,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.lib","Name":"node.lib","Size":2676072,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.7z","Name":"node_pdb.7z","Size":52271726,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.zip","Name":"node_pdb.zip","Size":89666140,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs/api","Name":"api","Size":0,"ModTime":"","IsDir":true}, + {"Path":"docs/apilinks.json","Name":"apilinks.json","Size":64481,"ModTime":"","IsDir":false,"Tier":"STANDARD"} +] diff --git a/ansible/www-standalone/tools/promote/test/fixtures/partial-v22.12.0.json b/ansible/www-standalone/tools/promote/test/fixtures/partial-v22.12.0.json new file mode 100644 index 000000000..b13b5d710 --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/fixtures/partial-v22.12.0.json @@ -0,0 +1,47 @@ +[ + {"Path":"docs","Name":"docs","Size":0,"ModTime":"","IsDir":true}, + {"Path":"node-v22.12.0-aix-ppc64.tar.gz","Name":"node-v22.12.0-aix-ppc64.tar.gz","Size":69856808,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-arm64.msi","Name":"node-v22.12.0-arm64.msi","Size":27000832,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.gz","Name":"node-v22.12.0-darwin-arm64.tar.gz","Size":48567076,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.xz","Name":"node-v22.12.0-darwin-arm64.tar.xz","Size":25111488,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.gz","Name":"node-v22.12.0-darwin-x64.tar.gz","Size":49318566,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.xz","Name":"node-v22.12.0-darwin-x64.tar.xz","Size":26765944,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.gz","Name":"node-v22.12.0-headers.tar.gz","Size":8770488,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.xz","Name":"node-v22.12.0-headers.tar.xz","Size":538384,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.gz","Name":"node-v22.12.0-linux-arm64.tar.gz","Size":53503539,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.xz","Name":"node-v22.12.0-linux-arm64.tar.xz","Size":28592580,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.gz","Name":"node-v22.12.0-linux-ppc64le.tar.gz","Size":56738589,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.xz","Name":"node-v22.12.0-linux-ppc64le.tar.xz","Size":30247396,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.gz","Name":"node-v22.12.0-linux-s390x.tar.gz","Size":54243198,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.xz","Name":"node-v22.12.0-linux-s390x.tar.xz","Size":28322596,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.gz","Name":"node-v22.12.0-linux-x64.tar.gz","Size":53956881,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.xz","Name":"node-v22.12.0-linux-x64.tar.xz","Size":29734420,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.7z","Name":"node-v22.12.0-win-arm64.7z","Size":19483917,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.zip","Name":"node-v22.12.0-win-arm64.zip","Size":30538863,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.7z","Name":"node-v22.12.0-win-x64.7z","Size":21723006,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.zip","Name":"node-v22.12.0-win-x64.zip","Size":34872040,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.7z","Name":"node-v22.12.0-win-x86.7z","Size":19991878,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.zip","Name":"node-v22.12.0-win-x86.zip","Size":32316845,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x64.msi","Name":"node-v22.12.0-x64.msi","Size":30814208,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x86.msi","Name":"node-v22.12.0-x86.msi","Size":28143616,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.pkg","Name":"node-v22.12.0.pkg","Size":87448300,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.gz","Name":"node-v22.12.0.tar.gz","Size":98924688,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.xz","Name":"node-v22.12.0.tar.xz","Size":47713468,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64","Name":"win-arm64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x64","Name":"win-x64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86","Name":"win-x86","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-arm64/node.exe","Name":"node.exe","Size":74058904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.lib","Name":"node.lib","Size":2675724,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.7z","Name":"node_pdb.7z","Size":48698398,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.zip","Name":"node_pdb.zip","Size":85105807,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs/api","Name":"api","Size":0,"ModTime":"","IsDir":true}, + {"Path":"docs/apilinks.json","Name":"apilinks.json","Size":64457,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.exe","Name":"node.exe","Size":82818704,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.lib","Name":"node.lib","Size":2676072,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.7z","Name":"node_pdb.7z","Size":52189903,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.zip","Name":"node_pdb.zip","Size":90064942,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.exe","Name":"node.exe","Size":73998992,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.lib","Name":"node.lib","Size":2678702,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.7z","Name":"node_pdb.7z","Size":51464274,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.zip","Name":"node_pdb.zip","Size":88907290,"ModTime":"","IsDir":false,"Tier":"STANDARD"} +] diff --git a/ansible/www-standalone/tools/promote/test/fixtures/partial-with-shasums-v22.12.0.json b/ansible/www-standalone/tools/promote/test/fixtures/partial-with-shasums-v22.12.0.json new file mode 100644 index 000000000..7b166cbf0 --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/fixtures/partial-with-shasums-v22.12.0.json @@ -0,0 +1,50 @@ +[ + {"Path":"SHASUMS256.txt","Name":"SHASUMS256.txt","Size":3777,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"SHASUMS256.txt.asc","Name":"SHASUMS256.txt.asc","Size":4659,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"SHASUMS256.txt.sig","Name":"SHASUMS256.txt.sig","Size":566,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs","Name":"docs","Size":0,"ModTime":"","IsDir":true}, + {"Path":"node-v22.12.0-aix-ppc64.tar.gz","Name":"node-v22.12.0-aix-ppc64.tar.gz","Size":69856808,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-arm64.msi","Name":"node-v22.12.0-arm64.msi","Size":27000832,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.gz","Name":"node-v22.12.0-darwin-arm64.tar.gz","Size":48567076,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.xz","Name":"node-v22.12.0-darwin-arm64.tar.xz","Size":25111488,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.gz","Name":"node-v22.12.0-darwin-x64.tar.gz","Size":49318566,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.xz","Name":"node-v22.12.0-darwin-x64.tar.xz","Size":26765944,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.gz","Name":"node-v22.12.0-headers.tar.gz","Size":8770488,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.xz","Name":"node-v22.12.0-headers.tar.xz","Size":538384,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.gz","Name":"node-v22.12.0-linux-arm64.tar.gz","Size":53503539,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.xz","Name":"node-v22.12.0-linux-arm64.tar.xz","Size":28592580,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.gz","Name":"node-v22.12.0-linux-ppc64le.tar.gz","Size":56738589,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.xz","Name":"node-v22.12.0-linux-ppc64le.tar.xz","Size":30247396,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.gz","Name":"node-v22.12.0-linux-s390x.tar.gz","Size":54243198,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.xz","Name":"node-v22.12.0-linux-s390x.tar.xz","Size":28322596,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.gz","Name":"node-v22.12.0-linux-x64.tar.gz","Size":53956881,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.xz","Name":"node-v22.12.0-linux-x64.tar.xz","Size":29734420,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.7z","Name":"node-v22.12.0-win-arm64.7z","Size":19483917,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.zip","Name":"node-v22.12.0-win-arm64.zip","Size":30538863,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.7z","Name":"node-v22.12.0-win-x64.7z","Size":21723006,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.zip","Name":"node-v22.12.0-win-x64.zip","Size":34872040,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.7z","Name":"node-v22.12.0-win-x86.7z","Size":19991878,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.zip","Name":"node-v22.12.0-win-x86.zip","Size":32316845,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x64.msi","Name":"node-v22.12.0-x64.msi","Size":30814208,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x86.msi","Name":"node-v22.12.0-x86.msi","Size":28143616,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.pkg","Name":"node-v22.12.0.pkg","Size":87448300,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.gz","Name":"node-v22.12.0.tar.gz","Size":98924688,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.xz","Name":"node-v22.12.0.tar.xz","Size":47713468,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64","Name":"win-arm64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x64","Name":"win-x64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86","Name":"win-x86","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-arm64/node.exe","Name":"node.exe","Size":74058904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.lib","Name":"node.lib","Size":2675724,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.7z","Name":"node_pdb.7z","Size":48698398,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.zip","Name":"node_pdb.zip","Size":85105807,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs/api","Name":"api","Size":0,"ModTime":"","IsDir":true}, + {"Path":"docs/apilinks.json","Name":"apilinks.json","Size":64457,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.exe","Name":"node.exe","Size":82818704,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.lib","Name":"node.lib","Size":2676072,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.7z","Name":"node_pdb.7z","Size":52189903,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.zip","Name":"node_pdb.zip","Size":90064942,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.exe","Name":"node.exe","Size":73998992,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.lib","Name":"node.lib","Size":2678702,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.7z","Name":"node_pdb.7z","Size":51464274,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.zip","Name":"node_pdb.zip","Size":88907290,"ModTime":"","IsDir":false,"Tier":"STANDARD"} +] diff --git a/ansible/www-standalone/tools/promote/test/fixtures/unexpected-files.json b/ansible/www-standalone/tools/promote/test/fixtures/unexpected-files.json new file mode 100644 index 000000000..e1a370c95 --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/fixtures/unexpected-files.json @@ -0,0 +1,4 @@ +[ + {"Path":"foo.tar.gz","Name":"foo.tar.gz","Size":69853847,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"bar.tar.xz","Name":"foo.tar.xz","Size":89666140,"ModTime":"","IsDir":false,"Tier":"STANDARD"} +] diff --git a/ansible/www-standalone/tools/promote/test/fixtures/with-shasums-v22.12.0.json b/ansible/www-standalone/tools/promote/test/fixtures/with-shasums-v22.12.0.json new file mode 100644 index 000000000..ad2112c6c --- /dev/null +++ b/ansible/www-standalone/tools/promote/test/fixtures/with-shasums-v22.12.0.json @@ -0,0 +1,50 @@ +[ + {"Path":"SHASUMS256.txt","Name":"SHASUMS256.txt","Size":3777,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs","Name":"docs","Size":0,"ModTime":"","IsDir":true}, + {"Path":"node-v22.12.0-aix-ppc64.tar.gz","Name":"node-v22.12.0-aix-ppc64.tar.gz","Size":69853847,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-arm64.msi","Name":"node-v22.12.0-arm64.msi","Size":26984448,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.gz","Name":"node-v22.12.0-darwin-arm64.tar.gz","Size":48568612,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-arm64.tar.xz","Name":"node-v22.12.0-darwin-arm64.tar.xz","Size":25100836,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.gz","Name":"node-v22.12.0-darwin-x64.tar.gz","Size":49317806,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-darwin-x64.tar.xz","Name":"node-v22.12.0-darwin-x64.tar.xz","Size":26764560,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.gz","Name":"node-v22.12.0-headers.tar.gz","Size":8778770,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-headers.tar.xz","Name":"node-v22.12.0-headers.tar.xz","Size":532904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.gz","Name":"node-v22.12.0-linux-arm64.tar.gz","Size":53504891,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-arm64.tar.xz","Name":"node-v22.12.0-linux-arm64.tar.xz","Size":28591324,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-armv7l.tar.gz","Name":"node-v22.12.0-linux-armv7l.tar.gz","Size":49095271,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-armv7l.tar.xz","Name":"node-v22.12.0-linux-armv7l.tar.xz","Size":25375440,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.gz","Name":"node-v22.12.0-linux-ppc64le.tar.gz","Size":56745592,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-ppc64le.tar.xz","Name":"node-v22.12.0-linux-ppc64le.tar.xz","Size":30244600,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.gz","Name":"node-v22.12.0-linux-s390x.tar.gz","Size":54242893,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-s390x.tar.xz","Name":"node-v22.12.0-linux-s390x.tar.xz","Size":28320236,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.gz","Name":"node-v22.12.0-linux-x64.tar.gz","Size":53958425,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-linux-x64.tar.xz","Name":"node-v22.12.0-linux-x64.tar.xz","Size":29734248,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.7z","Name":"node-v22.12.0-win-arm64.7z","Size":19480710,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-arm64.zip","Name":"node-v22.12.0-win-arm64.zip","Size":30538928,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.7z","Name":"node-v22.12.0-win-x64.7z","Size":21728036,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x64.zip","Name":"node-v22.12.0-win-x64.zip","Size":34872043,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.7z","Name":"node-v22.12.0-win-x86.7z","Size":19991434,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-win-x86.zip","Name":"node-v22.12.0-win-x86.zip","Size":32316847,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x64.msi","Name":"node-v22.12.0-x64.msi","Size":30818304,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0-x86.msi","Name":"node-v22.12.0-x86.msi","Size":28155904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.pkg","Name":"node-v22.12.0.pkg","Size":87448749,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.gz","Name":"node-v22.12.0.tar.gz","Size":97731119,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"node-v22.12.0.tar.xz","Name":"node-v22.12.0.tar.xz","Size":47625776,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64","Name":"win-arm64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x64","Name":"win-x64","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86","Name":"win-x86","Size":0,"ModTime":"","IsDir":true}, + {"Path":"win-x86/node.exe","Name":"node.exe","Size":73998992,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node.lib","Name":"node.lib","Size":2678702,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.7z","Name":"node_pdb.7z","Size":51000521,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x86/node_pdb.zip","Name":"node_pdb.zip","Size":88377615,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.exe","Name":"node.exe","Size":74058904,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node.lib","Name":"node.lib","Size":2675724,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.7z","Name":"node_pdb.7z","Size":48719554,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-arm64/node_pdb.zip","Name":"node_pdb.zip","Size":84899182,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.exe","Name":"node.exe","Size":82818704,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node.lib","Name":"node.lib","Size":2676072,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.7z","Name":"node_pdb.7z","Size":52271726,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"win-x64/node_pdb.zip","Name":"node_pdb.zip","Size":89666140,"ModTime":"","IsDir":false,"Tier":"STANDARD"}, + {"Path":"docs/api","Name":"api","Size":0,"ModTime":"","IsDir":true}, + {"Path":"docs/apilinks.json","Name":"apilinks.json","Size":64481,"ModTime":"","IsDir":false,"Tier":"STANDARD"} +]