Remove grunt, add s3 publishing tests

This commit is contained in:
Igor Savin
2026-03-19 23:56:14 +02:00
committed by Jay Linski
parent b5ad730473
commit f1421eb2ab
9 changed files with 1900 additions and 292 deletions
-9
View File
@@ -1,9 +0,0 @@
// Legacy Gruntfile — kept only for the 'metrics' and 'version' tasks.
// The main build pipeline uses rspack + swc (see rspack.config.js and .swcrc).
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
});
grunt.task.loadTasks('tasks');
};
+1564 -193
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -48,6 +48,7 @@
"test:browser": "vitest run --project browser", "test:browser": "vitest run --project browser",
"test:unit": "vitest run --project node", "test:unit": "vitest run --project node",
"test:tasks": "vitest run --project tasks", "test:tasks": "vitest run --project tasks",
"test:publish": "npm run build && vitest run --project publish",
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js", "test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
"test:serve": "npx serve -l 9999 .", "test:serve": "npx serve -l 9999 .",
"test:integration": "npm run build && ./tests/integration/run-integration-tests.sh", "test:integration": "npm run build && ./tests/integration/run-integration-tests.sh",
@@ -77,6 +78,7 @@
"concurrently": "^5.0.0", "concurrently": "^5.0.0",
"eslint": "^10.0.3", "eslint": "^10.0.3",
"eslint-plugin-compat": "^7.0.1", "eslint-plugin-compat": "^7.0.1",
"fauxqs": "^2.3.1",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"husky": "^3.1.0", "husky": "^3.1.0",
"lint-staged": "^16.3.2", "lint-staged": "^16.3.2",
+34 -19
View File
@@ -13,18 +13,7 @@ async function main() {
const commitInfo = await git.commitInfo(); const commitInfo = await git.commitInfo();
console.log('tag: ', commitInfo.tagName); console.log('tag: ', commitInfo.tagName);
const suffixes = []; const suffixes = buildSuffixes(commitInfo);
// Publish the master as "latest" and with the commit-id
if (commitInfo.isMaster) {
suffixes.push('-latest');
suffixes.push('-' + commitInfo.headSha);
}
// Publish tags by their tag-name
if (commitInfo.tagName != null && semver.valid(commitInfo.tagName)) {
suffixes.push('-' + commitInfo.tagName);
}
if (suffixes.length > 0) { if (suffixes.length > 0) {
validateS3Env(); validateS3Env();
@@ -33,6 +22,21 @@ async function main() {
} }
} }
function buildSuffixes(commitInfo) {
const suffixes = [];
if (commitInfo.isMaster) {
suffixes.push('-latest');
suffixes.push('-' + commitInfo.headSha);
}
if (commitInfo.tagName != null && semver.valid(commitInfo.tagName)) {
suffixes.push('-' + commitInfo.tagName);
}
return suffixes;
}
function validateS3Env() { function validateS3Env() {
const bucket = process.env.S3_BUCKET_NAME, const bucket = process.env.S3_BUCKET_NAME,
region = process.env.S3_REGION, region = process.env.S3_REGION,
@@ -44,12 +48,14 @@ function validateS3Env() {
} }
} }
async function publish(suffixes) { async function publish(suffixes, overrides) {
const publishPromises = suffixes.map((suffix) => publishSuffix(suffix)); const publishPromises = suffixes.map((suffix) =>
publishSuffix(suffix, overrides)
);
return Promise.all(publishPromises); return Promise.all(publishPromises);
} }
async function publishSuffix(suffix) { async function publishSuffix(suffix, overrides) {
const filenames = [ const filenames = [
'handlebars.js', 'handlebars.js',
'handlebars.min.js', 'handlebars.min.js',
@@ -59,18 +65,19 @@ async function publishSuffix(suffix) {
const publishPromises = filenames.map(async (filename) => { const publishPromises = filenames.map(async (filename) => {
const nameInBucket = getNameInBucket(filename, suffix); const nameInBucket = getNameInBucket(filename, suffix);
const localFile = getLocalFile(filename); const localFile = getLocalFile(filename);
await uploadToBucket(localFile, nameInBucket); await uploadToBucket(localFile, nameInBucket, overrides);
console.log(`Published ${localFile} to build server (${nameInBucket})`); console.log(`Published ${localFile} to build server (${nameInBucket})`);
}); });
return Promise.all(publishPromises); return Promise.all(publishPromises);
} }
async function uploadToBucket(localFile, nameInBucket) { async function uploadToBucket(localFile, nameInBucket, overrides) {
const s3 = getS3Client(); const s3 = overrides?.s3Client ?? getS3Client();
const bucket = overrides?.bucket ?? process.env.S3_BUCKET_NAME;
return s3.send( return s3.send(
new PutObjectCommand({ new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME, Bucket: bucket,
Key: nameInBucket, Key: nameInBucket,
Body: fs.readFileSync(localFile, 'utf8'), Body: fs.readFileSync(localFile, 'utf8'),
}) })
@@ -98,6 +105,14 @@ function getLocalFile(filename) {
return 'dist/' + filename; return 'dist/' + filename;
} }
module.exports = {
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
};
if (require.main === module) { if (require.main === module) {
main().catch((err) => { main().catch((err) => {
console.error(err); console.error(err);
+228
View File
@@ -0,0 +1,228 @@
const fs = require('fs');
const { S3, GetObjectCommand } = require('@aws-sdk/client-s3');
const {
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
} = require('../publish-to-aws');
const BUCKET = 'builds.handlebarsjs.com';
const PUBLISHED_FILES = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
'handlebars.runtime.min.js',
];
let startFauxqs;
let server;
let s3Client;
beforeAll(async () => {
({ startFauxqs } = await import('fauxqs'));
server = await startFauxqs({ port: 0, logger: false });
server.createBucket(BUCKET);
s3Client = new S3({
endpoint: server.address,
region: 'us-east-1',
credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
forcePathStyle: true,
});
});
afterAll(async () => {
await server?.stop();
});
beforeEach(() => {
server.reset();
server.createBucket(BUCKET);
});
async function getObjectBody(key) {
const response = await s3Client.send(
new GetObjectCommand({ Bucket: BUCKET, Key: key })
);
return response.Body.transformToString();
}
describe('buildSuffixes', () => {
it('should add -latest and commit sha for master', () => {
const result = buildSuffixes({
isMaster: true,
headSha: 'abc123',
tagName: null,
});
expect(result).toEqual(['-latest', '-abc123']);
});
it('should add tag suffix for valid semver tag', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: '4.7.0',
});
expect(result).toEqual(['-4.7.0']);
});
it('should add both master and tag suffixes when on master with a tag', () => {
const result = buildSuffixes({
isMaster: true,
headSha: 'abc123',
tagName: '4.7.0',
});
expect(result).toEqual(['-latest', '-abc123', '-4.7.0']);
});
it('should return empty array for non-master with no valid tag', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: null,
});
expect(result).toEqual([]);
});
it('should ignore invalid semver tags', () => {
const result = buildSuffixes({
isMaster: false,
headSha: 'abc123',
tagName: 'not-semver',
});
expect(result).toEqual([]);
});
});
describe('getNameInBucket', () => {
it('should insert suffix before .js extension', () => {
expect(getNameInBucket('handlebars.js', '-latest')).toBe(
'handlebars-latest.js'
);
});
it('should handle .min.js files', () => {
expect(getNameInBucket('handlebars.min.js', '-4.7.0')).toBe(
'handlebars.min-4.7.0.js'
);
});
it('should handle runtime files', () => {
expect(getNameInBucket('handlebars.runtime.js', '-abc123')).toBe(
'handlebars.runtime-abc123.js'
);
});
});
describe('getLocalFile', () => {
it('should prefix with dist/', () => {
expect(getLocalFile('handlebars.js')).toBe('dist/handlebars.js');
});
});
describe('validateS3Env', () => {
const S3_KEYS = [
'S3_BUCKET_NAME',
'S3_REGION',
'S3_ACCESS_KEY_ID',
'S3_SECRET_ACCESS_KEY',
];
const saved = {};
beforeEach(() => {
for (const key of S3_KEYS) {
saved[key] = process.env[key];
}
});
afterEach(() => {
for (const key of S3_KEYS) {
if (saved[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = saved[key];
}
}
});
it('should throw when S3 env vars are missing', () => {
for (const key of S3_KEYS) {
delete process.env[key];
}
expect(() => validateS3Env()).toThrow('Missing S3 config values');
});
it('should not throw when all S3 env vars are set', () => {
process.env.S3_BUCKET_NAME = 'test-bucket';
process.env.S3_REGION = 'us-east-1';
process.env.S3_ACCESS_KEY_ID = 'key';
process.env.S3_SECRET_ACCESS_KEY = 'secret';
expect(() => validateS3Env()).not.toThrow();
});
});
describe('publish to S3', () => {
const overrides = {};
beforeAll(() => {
overrides.s3Client = s3Client;
overrides.bucket = BUCKET;
});
it('should upload all 4 files for a single suffix', async () => {
await publish(['-latest'], overrides);
for (const filename of PUBLISHED_FILES) {
const key = getNameInBucket(filename, '-latest');
const body = await getObjectBody(key);
const localContent = fs.readFileSync(getLocalFile(filename), 'utf8');
expect(body).toBe(localContent);
}
});
it('should upload files for multiple suffixes', async () => {
await publish(['-latest', '-abc123'], overrides);
for (const suffix of ['-latest', '-abc123']) {
for (const filename of PUBLISHED_FILES) {
const key = getNameInBucket(filename, suffix);
const body = await getObjectBody(key);
expect(body).toBeTruthy();
}
}
});
it('should upload correct content from dist/', async () => {
await publish(['-v1.0.0'], overrides);
const key = getNameInBucket('handlebars.js', '-v1.0.0');
const uploaded = await getObjectBody(key);
const local = fs.readFileSync('dist/handlebars.js', 'utf8');
expect(uploaded).toBe(local);
});
it('should produce correct keys for a version tag', async () => {
await publish(['-4.7.0'], overrides);
const expectedKeys = PUBLISHED_FILES.map((f) =>
getNameInBucket(f, '-4.7.0')
);
for (const key of expectedKeys) {
const body = await getObjectBody(key);
expect(body).toBeTruthy();
}
expect(expectedKeys).toEqual([
'handlebars-4.7.0.js',
'handlebars.min-4.7.0.js',
'handlebars.runtime-4.7.0.js',
'handlebars.runtime.min-4.7.0.js',
]);
});
});
-13
View File
@@ -1,13 +0,0 @@
module.exports = { createRegisterAsyncTaskFn };
function createRegisterAsyncTaskFn(grunt) {
return function registerAsyncTask(name, asyncFunction) {
grunt.registerTask(name, function () {
asyncFunction()
.catch((error) => {
grunt.fatal(error);
})
.finally(this.async());
});
};
}
+28 -23
View File
@@ -1,23 +1,22 @@
const fs = require('fs');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const git = require('./util/git'); const git = require('./util/git');
const semver = require('semver'); const semver = require('semver');
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
module.exports = function (grunt) { async function main() {
const registerAsyncTask = createRegisterAsyncTaskFn(grunt); const version = process.argv[2];
registerAsyncTask('version', async () => {
const pkg = grunt.config('pkg');
const version = grunt.option('ver');
if (!semver.valid(version)) { if (!semver.valid(version)) {
throw new Error( throw new Error(
'Must provide a version number (Ex: --ver=1.0.0):\n\t' + 'Must provide a valid semver version as first argument (e.g.: node tasks/version.js 1.0.0):\n\t' +
version + version +
'\n\n' '\n\n'
); );
} }
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = version; pkg.version = version;
grunt.config('pkg', pkg); fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
await git.add('package.json');
const replaceSpec = [ const replaceSpec = [
{ {
@@ -43,21 +42,27 @@ module.exports = function (grunt) {
]; ];
await Promise.all( await Promise.all(
replaceSpec.map((replaceSpec) => replaceSpec.map((spec) =>
replaceAndAdd( replaceAndAdd(spec.path, spec.regex, spec.replacement)
replaceSpec.path,
replaceSpec.regex,
replaceSpec.replacement
)
) )
); );
execSync('npm run build', { stdio: 'inherit' });
});
async function replaceAndAdd(path, regex, value) { execSync('npm run build', { stdio: 'inherit' });
let content = grunt.file.read(path); }
content = content.replace(regex, value);
grunt.file.write(path, content); async function replaceAndAdd(filePath, regex, value) {
await git.add(path); let content = fs.readFileSync(filePath, 'utf8');
content = content.replace(regex, value);
fs.writeFileSync(filePath, content);
await git.add(filePath);
}
module.exports = { replaceAndAdd };
if (require.main === module) {
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});
} }
};
+1 -1
View File
@@ -8,7 +8,7 @@ Execute the following commands in the project root:
```bash ```bash
npm install npm install
npx grunt prepare npm run build
docker pull mcr.microsoft.com/playwright:focal docker pull mcr.microsoft.com/playwright:focal
docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:focal npm run test:browser docker run -it --rm --volume $(pwd):/srv/app --workdir /srv/app --ipc=host mcr.microsoft.com/playwright:focal npm run test:browser
``` ```
+9
View File
@@ -17,6 +17,7 @@ export default defineConfig({
test: { test: {
name: 'tasks', name: 'tasks',
include: ['tasks/tests/*.test.js'], include: ['tasks/tests/*.test.js'],
exclude: ['tasks/tests/publish-to-aws.test.js'],
globals: true, globals: true,
pool: 'forks', pool: 'forks',
}, },
@@ -28,6 +29,14 @@ export default defineConfig({
globals: true, globals: true,
}, },
}, },
{
test: {
name: 'publish',
include: ['tasks/tests/publish-to-aws.test.js'],
globals: true,
pool: 'forks',
},
},
{ {
test: { test: {
name: 'browser', name: 'browser',