2023-07-27 15:24:49 +00:00
|
|
|
/**
|
|
|
|
* Verify the latest release is reproducible
|
|
|
|
*/
|
|
|
|
import { exec as nodeExec } from "node:child_process";
|
|
|
|
import crypto from "node:crypto";
|
|
|
|
import { createWriteStream } from "node:fs";
|
|
|
|
import { mkdir, readdir, readFile } from "node:fs/promises";
|
|
|
|
import path from "node:path";
|
|
|
|
import { Readable } from "node:stream";
|
|
|
|
import { finished } from "node:stream/promises";
|
|
|
|
import util from "node:util";
|
|
|
|
import { gunzip as nodeGunzip } from "node:zlib";
|
|
|
|
import { rimraf } from "rimraf";
|
|
|
|
|
|
|
|
const exec = util.promisify( nodeExec );
|
|
|
|
const gunzip = util.promisify( nodeGunzip );
|
|
|
|
|
|
|
|
const SRC_REPO = "https://github.com/jquery/jquery.git";
|
|
|
|
const CDN_URL = "https://code.jquery.com";
|
|
|
|
const REGISTRY_URL = "https://registry.npmjs.org/jquery";
|
|
|
|
|
2024-07-17 14:13:53 +00:00
|
|
|
const excludeFromCDN = [
|
|
|
|
/^package\.json$/,
|
|
|
|
/^jquery\.factory\./
|
|
|
|
];
|
|
|
|
|
|
|
|
const rjquery = /^jquery/;
|
2023-07-27 15:24:49 +00:00
|
|
|
|
2024-07-17 13:32:02 +00:00
|
|
|
async function verifyRelease( { version } = {} ) {
|
2023-07-27 15:24:49 +00:00
|
|
|
if ( !version ) {
|
|
|
|
version = process.env.VERSION || ( await getLatestVersion() );
|
|
|
|
}
|
|
|
|
const release = await buildRelease( { version } );
|
|
|
|
|
2024-07-17 13:32:02 +00:00
|
|
|
console.log( `Verifying jQuery ${ version }...` );
|
|
|
|
|
2023-07-27 15:24:49 +00:00
|
|
|
let verified = true;
|
2024-07-17 14:13:53 +00:00
|
|
|
const matchingFiles = [];
|
|
|
|
const mismatchingFiles = [];
|
2023-07-27 15:24:49 +00:00
|
|
|
|
2024-07-17 14:13:53 +00:00
|
|
|
// Check all files against the CDN
|
|
|
|
await Promise.all(
|
|
|
|
release.files
|
|
|
|
.filter( ( file ) => excludeFromCDN.every( ( re ) => !re.test( file.name ) ) )
|
|
|
|
.map( async( file ) => {
|
|
|
|
const url = new URL( file.cdnName, CDN_URL );
|
|
|
|
const response = await fetch( url );
|
|
|
|
if ( !response.ok ) {
|
|
|
|
throw new Error(
|
|
|
|
`Failed to download ${
|
|
|
|
file.cdnName
|
|
|
|
} from the CDN: ${ response.statusText }`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const cdnContents = await response.text();
|
|
|
|
if ( cdnContents !== file.cdnContents ) {
|
|
|
|
mismatchingFiles.push( url.href );
|
2023-07-27 15:24:49 +00:00
|
|
|
verified = false;
|
2024-07-17 14:13:53 +00:00
|
|
|
} else {
|
|
|
|
matchingFiles.push( url.href );
|
2023-07-27 15:24:49 +00:00
|
|
|
}
|
|
|
|
} )
|
2024-07-17 14:13:53 +00:00
|
|
|
);
|
2023-07-27 15:24:49 +00:00
|
|
|
|
2024-07-17 14:13:53 +00:00
|
|
|
// Check all files against npm.
|
2023-07-27 15:24:49 +00:00
|
|
|
// First, download npm tarball for version
|
|
|
|
const npmPackage = await fetch( REGISTRY_URL ).then( ( res ) => res.json() );
|
|
|
|
|
|
|
|
if ( !npmPackage.versions[ version ] ) {
|
|
|
|
throw new Error( `jQuery ${ version } not found on npm!` );
|
|
|
|
}
|
|
|
|
const npmTarball = npmPackage.versions[ version ].dist.tarball;
|
|
|
|
|
|
|
|
// Write npm tarball to file
|
|
|
|
const npmTarballPath = path.join( "tmp/verify", version, "npm.tgz" );
|
|
|
|
await downloadFile( npmTarball, npmTarballPath );
|
|
|
|
|
|
|
|
// Check the tarball checksum
|
|
|
|
const tgzSum = await sumTarball( npmTarballPath );
|
|
|
|
if ( tgzSum !== release.tgz.contents ) {
|
2024-07-17 14:13:53 +00:00
|
|
|
mismatchingFiles.push( `npm:${ version }.tgz` );
|
2023-07-27 15:24:49 +00:00
|
|
|
verified = false;
|
2024-07-17 14:13:53 +00:00
|
|
|
} else {
|
|
|
|
matchingFiles.push( `npm:${ version }.tgz` );
|
2023-07-27 15:24:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
release.files.map( async( file ) => {
|
|
|
|
|
|
|
|
// Get file contents from tarball
|
|
|
|
const { stdout: npmContents } = await exec(
|
|
|
|
`tar -xOf ${ npmTarballPath } package/${ file.path }/${ file.name }`
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( npmContents !== file.contents ) {
|
2024-07-17 14:13:53 +00:00
|
|
|
mismatchingFiles.push( `npm:${ file.path }/${ file.name }` );
|
2023-07-27 15:24:49 +00:00
|
|
|
verified = false;
|
2024-07-17 14:13:53 +00:00
|
|
|
} else {
|
|
|
|
matchingFiles.push( `npm:${ file.path }/${ file.name }` );
|
2023-07-27 15:24:49 +00:00
|
|
|
}
|
|
|
|
} )
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( verified ) {
|
2024-07-17 14:13:53 +00:00
|
|
|
console.log( `jQuery ${ version } is reproducible! All files match!` );
|
2023-07-27 15:24:49 +00:00
|
|
|
} else {
|
2024-07-17 14:13:53 +00:00
|
|
|
console.log();
|
|
|
|
for ( const file of matchingFiles ) {
|
|
|
|
console.log( `✅ ${ file }` );
|
|
|
|
}
|
|
|
|
console.log();
|
|
|
|
for ( const file of mismatchingFiles ) {
|
|
|
|
console.log( `❌ ${ file }` );
|
|
|
|
}
|
|
|
|
|
2023-07-27 15:24:49 +00:00
|
|
|
throw new Error( `jQuery ${ version } is NOT reproducible!` );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function buildRelease( { version } ) {
|
|
|
|
const releaseFolder = path.join( "tmp/verify", version );
|
|
|
|
|
|
|
|
// Clone the release repo
|
|
|
|
console.log( `Cloning jQuery ${ version }...` );
|
|
|
|
await rimraf( releaseFolder );
|
|
|
|
await mkdir( releaseFolder, { recursive: true } );
|
2024-07-17 14:13:53 +00:00
|
|
|
|
|
|
|
// Uses a depth of 2 so we can get the commit date of
|
|
|
|
// the commit used to build, which is the commit before the tag
|
2023-07-27 15:24:49 +00:00
|
|
|
await exec(
|
2024-07-17 14:13:53 +00:00
|
|
|
`git clone -q -b ${ version } --depth=2 ${ SRC_REPO } ${ releaseFolder }`
|
2023-07-27 15:24:49 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Install node dependencies
|
|
|
|
console.log( `Installing dependencies for jQuery ${ version }...` );
|
|
|
|
await exec( "npm ci", { cwd: releaseFolder } );
|
|
|
|
|
2024-07-17 14:13:53 +00:00
|
|
|
// Find the date of the commit just before the release,
|
|
|
|
// which was used as the date in the built files
|
|
|
|
const { stdout: date } = await exec( "git log -1 --format=%ci HEAD~1", {
|
|
|
|
cwd: releaseFolder
|
|
|
|
} );
|
|
|
|
|
2023-07-27 15:24:49 +00:00
|
|
|
// Build the release
|
|
|
|
console.log( `Building jQuery ${ version }...` );
|
|
|
|
const { stdout: buildOutput } = await exec( "npm run build:all", {
|
|
|
|
cwd: releaseFolder,
|
|
|
|
env: {
|
2024-07-17 14:13:53 +00:00
|
|
|
|
|
|
|
// Keep existing environment variables
|
|
|
|
...process.env,
|
|
|
|
RELEASE_DATE: date,
|
2023-07-27 15:24:49 +00:00
|
|
|
VERSION: version
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
console.log( buildOutput );
|
|
|
|
|
|
|
|
// Pack the npm tarball
|
|
|
|
console.log( `Packing jQuery ${ version }...` );
|
|
|
|
const { stdout: packOutput } = await exec( "npm pack", { cwd: releaseFolder } );
|
|
|
|
console.log( packOutput );
|
|
|
|
|
|
|
|
// Get all top-level /dist and /dist-module files
|
2024-07-17 14:13:53 +00:00
|
|
|
const distFiles = await readdir(
|
|
|
|
path.join( releaseFolder, "dist" ),
|
|
|
|
{ withFileTypes: true }
|
|
|
|
);
|
2023-07-27 15:24:49 +00:00
|
|
|
const distModuleFiles = await readdir(
|
|
|
|
path.join( releaseFolder, "dist-module" ),
|
2024-07-17 14:13:53 +00:00
|
|
|
{ withFileTypes: true }
|
2023-07-27 15:24:49 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const files = await Promise.all(
|
|
|
|
[ ...distFiles, ...distModuleFiles ]
|
|
|
|
.filter( ( dirent ) => dirent.isFile() )
|
2024-07-17 14:13:53 +00:00
|
|
|
.map( async( dirent ) => {
|
|
|
|
const contents = await readFile(
|
|
|
|
path.join( dirent.parentPath, dirent.name ),
|
|
|
|
"utf8"
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
name: dirent.name,
|
|
|
|
path: path.basename( dirent.parentPath ),
|
|
|
|
contents,
|
|
|
|
cdnName: dirent.name.replace( rjquery, `jquery-${ version }` ),
|
|
|
|
cdnContents: dirent.name.endsWith( ".map" ) ?
|
|
|
|
|
|
|
|
// The CDN has versioned filenames in the maps
|
|
|
|
convertMapToVersioned( contents, version ) :
|
|
|
|
contents
|
|
|
|
};
|
|
|
|
} )
|
2023-07-27 15:24:49 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Get checksum of the tarball
|
|
|
|
const tgzFilename = `jquery-${ version }.tgz`;
|
|
|
|
const sum = await sumTarball( path.join( releaseFolder, tgzFilename ) );
|
|
|
|
|
|
|
|
return {
|
|
|
|
files,
|
|
|
|
tgz: {
|
|
|
|
name: tgzFilename,
|
|
|
|
contents: sum
|
|
|
|
},
|
|
|
|
version
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function downloadFile( url, dest ) {
|
|
|
|
const response = await fetch( url );
|
|
|
|
const fileStream = createWriteStream( dest );
|
|
|
|
const stream = Readable.fromWeb( response.body ).pipe( fileStream );
|
|
|
|
return finished( stream );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getLatestVersion() {
|
|
|
|
const { stdout: sha } = await exec( "git rev-list --tags --max-count=1" );
|
|
|
|
const { stdout: tag } = await exec( `git describe --tags ${ sha.trim() }` );
|
|
|
|
return tag.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
function shasum( data ) {
|
|
|
|
const hash = crypto.createHash( "sha256" );
|
|
|
|
hash.update( data );
|
|
|
|
return hash.digest( "hex" );
|
|
|
|
}
|
|
|
|
|
|
|
|
async function sumTarball( filepath ) {
|
|
|
|
const contents = await readFile( filepath );
|
|
|
|
const unzipped = await gunzip( contents );
|
|
|
|
return shasum( unzipped );
|
|
|
|
}
|
2024-07-17 13:32:02 +00:00
|
|
|
|
2024-07-17 14:13:53 +00:00
|
|
|
function convertMapToVersioned( contents, version ) {
|
|
|
|
const map = JSON.parse( contents );
|
|
|
|
return JSON.stringify( {
|
|
|
|
...map,
|
|
|
|
file: map.file.replace( rjquery, `jquery-${ version }` ),
|
|
|
|
sources: map.sources.map( ( source ) => source.replace( rjquery, `jquery-${ version }` ) )
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2024-07-17 13:32:02 +00:00
|
|
|
verifyRelease();
|