diff --git a/RELEASE.md b/RELEASE.md index 9f3e94323..8e5920b94 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,90 +15,69 @@ QUnit aims for its releases to be reproducible. Recent releases are automaticall > > System prerequisites: > -> * Node.js 12, or later. +> * Node.js 14, or later. > * Git 2.11, or later. -1. Ensure that all changes for this release have been merged into the main branch. For patch releases, try landing any other bug fixes; for minor releases, ensure new features have been documented and tested. Major releases likely have their own checklist. +Ensure that all changes for this release have been merged into the main branch. For patch releases, try landing any other bug fixes; for minor releases, ensure new features have been documented and tested. Major releases likely have their own checklist. -2. Create a local `release` branch, and ensure it is up-to-date: - * Verify that the canonical repository is cloned (not a fork): +1. Create a local `release` branch, and ensure it is up-to-date: + Verify that the canonical repository is cloned (not a fork): ``` git remote -v # … # origin git@github.com:qunitjs/qunit.git ``` - * Create or reset the `release` branch: + Create or reset the `release` branch: ``` git remote update && git checkout -B release -t origin/main ``` -3. Install dev dependencies and run the tests: +2. Install dev dependencies: ``` - npm ci && npm test + npm ci ``` - Run the tests in various real browsers, either locally or via [BrowserStack](https://www.browserstack.com/): - ``` - python3 -m http.server 4000 - # or: - # php -S localhost:4000 - open http://localhost:4000/test/ +3. Create the release preparation commit: + ``` + node build/prep-release.js @VERSION ``` -4. Create and push the release preparation commit: + * Use `git add -p` to review the changes. + * In `AUTHORS.txt`, if you see duplicate entries, then use the `.mailmap` file to normalize them to a canonical name and e-mail address, and then re-run the above command. + * Edit `History.md` to remove changes not relevant to end-users (e.g. changes relating to tests, build, internal refactoring, doc fixes, etc.). + + Commit your changes with the following message (replace `@VERSION` with the release version): + ``` + Build: Prepare @VERSION release + ``` - 1. Update the package.json and AUTHORS.txt files, by running the below command (replace `@VERSION` with the release version): - ``` - node build/prep-release.js @VERSION - ``` - * Use `git add -p` to review the changes. - * In `AUTHORS.txt`, if you see duplicate entries, then use the `.mailmap` file to normalize them to a canonical name and e-mail address, and then re-run the above command. - * Edit `History.md` to remove change not relevant to end-users (e.g. changes relating to tests, build, internal refactoring, doc fixes, etc.). - 2. Commit the above changes with the following message (replace `@VERSION` with the release version): - ``` - Build: Prepare @VERSION release - ``` - 3. Push the `release` branch to GitHub. - 4. Create a pull request, and merge it once CI is passing. + Push the `release` branch to GitHub. + Once CI is passing, push again, this time to the (protected) `main` branch. ## Performing the release -5. Create a local `release` branch, and ensure it is up-to-date: - * Run `git remote -v` and verify the following: - ``` - origin git@github.com:qunitjs/qunit.git - ``` - * Create or reset the `release` branch: - ``` - git remote update && git checkout -B release -t origin/main - ``` - * Verify that the latest commit is your release preparation commit: - ``` - git show - # Build: Prepare x.y.z release - # … - ``` +Verify that your local repo is at the release preparation commit: -6. Make changes for the release commit: - * Set the release version for npm and Bower metadata (replace `@VERSION` with the release version): - ``` - node build/set-release.js @VERSION - ``` - This script will edit `package.json` and `bower.json`. It does not need any credentials or permissions, apart from read-write in the project directory. +``` +git show +# Build: Prepare x.y.z release +# … +``` - * Generate the release artifacts: +4. Build the release: ``` - npm run build + node build/build-release.js @VERSION ``` + This script does not need any credentials or permissions, and may be run in a container that can only read-write the current directory. It will edit `package.json` and `bower.json`, and then execute `npm run build` to generate the release artifacts. - * Review the changes to the package and library files, compared to the previous release. + Review the changes to the package, compared to the previous release. ``` node build/review-package.js @LAST_VERSION # … reviews package.json, qunit.js, and qunit.css ``` -7. Commit and publish the release to GitHub.
⚠️ Do not push to the main branch! +5. Publish to GitHub.
⚠️ Do not push to the main branch! ``` git add -f package.json bower.json qunit/ git commit -m "Release @VERSION" @@ -106,15 +85,14 @@ QUnit aims for its releases to be reproducible. Recent releases are automaticall git push --tags ``` -8. Verify that Bower sees the release, by running `npx bower info qunit` and checking that the latest - version is indeed the version we just published. - -9. Publish the release to npm: - * Use `git status` to confirm once more that you have a clean working copy, apart from release artifacts in `qunit/`. - * Run `npm publish`, this will bundle the current directory and publish it to npm with the name and version specified in `package.json`. - * Verify that the release is displayed at . +6. Publish to npm: + ``` + npm publish + ```` + This will bundle the current directory and publish it to npm with the name and version specified in `package.json`. + Verify that the release is displayed at . -10. Publish the release to the jQuery CDN: +7. Publish to the jQuery CDN: * Prepare the commit locally: ``` node build/auth-cdn-commit.js real @VERSION @@ -124,7 +102,6 @@ QUnit aims for its releases to be reproducible. Recent releases are automaticall ``` cd __codeorigin git show - # … git push ``` * Verify that the release is listed at and accessible via diff --git a/build/auth-cdn-commit.js b/build/auth-cdn-commit.js deleted file mode 100644 index 44dc14662..000000000 --- a/build/auth-cdn-commit.js +++ /dev/null @@ -1,83 +0,0 @@ -// Helper to commit QUnit releases to the CDN repository. -// -// See also RELEASE.md. -// -// Inspired by . - -const fs = require('fs'); -const cp = require('child_process'); -const path = require('path'); -const remotes = { - real: 'git@github.com:jquery/codeorigin.jquery.com.git', - test: 'git@github.com:jquery/fake-cdn.git' -}; -const files = { - 'qunit/qunit.js': 'cdn/qunit/qunit-@VERSION.js', - 'qunit/qunit.css': 'cdn/qunit/qunit-@VERSION.css' -}; -const commitMessage = 'qunit: Added version @VERSION'; - -const Cdn = { - clone (remoteKey, repoPath) { - const remote = remotes[remoteKey]; - if (!remote) { - throw new Error('Unknown remote ' + remoteKey); - } - console.log('... cloning ' + remote); - if (fs.existsSync(repoPath)) { - fs.rmdirSync(repoPath, { recursive: true }); - } - cp.execFileSync('git', [ - 'clone', - '--depth=5', - '-q', - remote, - repoPath - ]); - }, - commit (remoteKey, version) { - if (!remoteKey || !version) { - throw new Error( - 'Parameters are \n\n ' + - ' One of "test" or "real".\n ' + - ' E.g. "1.2.3".\n\nExample: node auth-cdn-push.js test 2.4.0\n' - ); - } - const repoPath = path.join(__dirname, '..', '__codeorigin'); - Cdn.clone(remoteKey, repoPath); - - const toCommit = []; - for (const src in files) { - const srcPath = path.join(__dirname, '..', src); - const dest = files[src].replace('@VERSION', version); - const destPath = path.join(repoPath, dest); - console.log('... copying ' + src + ' to ' + dest); - if (remoteKey === 'test') { - const destParent = path.dirname(destPath); - fs.mkdirSync(destParent, { recursive: true }); - } - fs.copyFileSync(srcPath, destPath, fs.constants.COPYFILE_EXCL); - toCommit.push(dest); - } - - console.log('... creating commit'); - cp.execFileSync('git', [ - 'add', - ...toCommit - ], { cwd: repoPath }); - cp.execFileSync('git', [ - 'commit', - '-m', - commitMessage.replace('@VERSION', version) - ], { cwd: repoPath }); - } -}; - -const [remote, version] = process.argv.slice(2); - -try { - Cdn.commit(remote, version); -} catch (e) { - console.error(e.toString()); - process.exit(1); -} diff --git a/build/build-release.js b/build/build-release.js new file mode 100644 index 000000000..b4e973add --- /dev/null +++ b/build/build-release.js @@ -0,0 +1,144 @@ +// Helper to set the QUnit release version in various places, +// and prepare a local commit in a codeorigin.git clone. +// +// To test the local commit step, with an alternate remote +// pass "test" as the second argument. +// +// See also RELEASE.md. +// +// Inspired by . + +const cp = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const cdnRemotes = { + anonymous: 'https://github.com/jquery/codeorigin.jquery.com.git', + auth: 'git@github.com:jquery/codeorigin.jquery.com.git' +}; +const cdnFiles = { + 'qunit/qunit.js': 'cdn/qunit/qunit-@VERSION.js', + 'qunit/qunit.css': 'cdn/qunit/qunit-@VERSION.css' +}; +const cdnCommitMessage = 'qunit: Added version @VERSION'; + +const Repo = { + buildFiles (version) { + if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error('Invalid or missing version argument'); + } + { + const file = 'bower.json'; + console.log(`Updating ${file}...`); + const filePath = path.join(__dirname, '..', file); + const json = fs.readFileSync(filePath, 'utf8'); + const packageIndentation = json.match(/\n([\t\s]+)/)[1]; + const data = JSON.parse(json); + + data.version = version; + + fs.writeFileSync( + filePath, + JSON.stringify(data, null, packageIndentation) + '\n' + ); + } + { + const file = 'package.json'; + console.log(`Updating ${file}...`); + const filePath = path.join(__dirname, '..', file); + const json = fs.readFileSync(filePath, 'utf8'); + const packageIndentation = json.match(/\n([\t\s]+)/)[1]; + const data = JSON.parse(json); + + data.version = version; + data.author.url = data.author.url.replace('main', version); + + fs.writeFileSync( + filePath, + JSON.stringify(data, null, packageIndentation) + '\n' + ); + } + { + console.log('Running `npm run build`...'); + cp.execFileSync('npm', [ + 'run', + 'build' + ], { encoding: 'utf8' }); + } + }, + cdnClone (repoPath) { + const remote = cdnRemotes.anonymous; + console.log('... cloning ' + remote); + if (fs.existsSync(repoPath)) { + fs.rmdirSync(repoPath, { recursive: true }); + } + cp.execFileSync('git', [ + 'clone', + '--depth=5', + '-q', + remote, + repoPath + ]); + }, + cdnCommit (version) { + if (!version) { + throw new Error('Missing parameters'); + } + const repoPath = path.join(__dirname, '..', '__codeorigin'); + Repo.cdnClone(repoPath); + + const toCommit = []; + for (const src in cdnFiles) { + const srcPath = path.join(__dirname, '..', src); + const dest = cdnFiles[src].replace('@VERSION', version); + const destPath = path.join(repoPath, dest); + console.log('... copying ' + src + ' to ' + dest); + fs.copyFileSync(srcPath, destPath, fs.constants.COPYFILE_EXCL); + toCommit.push(dest); + } + + console.log('... creating commit'); + + const author = cp.execSync('git log -1 --format=%aN%n%aE', + { encoding: 'utf8', cwd: path.join(__dirname, '..') } + ) + .trim() + .split('\n'); + + cp.execFileSync('git', ['add', ...toCommit], + { cwd: repoPath } + ); + + cp.execFileSync('git', + ['commit', '-m', cdnCommitMessage.replace('@VERSION', version)], + { + cwd: repoPath, + env: { + GIT_AUTHOR_NAME: author[0], + GIT_AUTHOR_EMAIL: author[1], + GIT_COMMITTER_NAME: author[0], + GIT_COMMITTER_EMAIL: author[1] + } + } + ); + + // prepre for user to push from the host shell after review + cp.execFileSync('git', ['remote', 'set-url', 'origin', cdnRemotes.auth], + { cwd: repoPath } + ); + } +}; + +const [version, remote] = process.argv.slice(2); + +try { + Repo.buildFiles(version); + Repo.cdnCommit(version, remote || 'real'); +} catch (e) { + if (e.stderr) { + e.stdout = e.stdout.toString(); + e.stderr = e.stderr.toString(); + } + console.error(e); + process.exit(1); +} diff --git a/build/prep-release.js b/build/prep-release.js index 29369871d..7fb40216e 100644 --- a/build/prep-release.js +++ b/build/prep-release.js @@ -147,6 +147,6 @@ const version = process.argv[2]; (async function main () { await Repo.prep(version); }()).catch(e => { - console.error(e.toString()); + console.error(e); process.exit(1); }); diff --git a/build/set-release.js b/build/set-release.js deleted file mode 100644 index f89df3ad4..000000000 --- a/build/set-release.js +++ /dev/null @@ -1,56 +0,0 @@ -// Helper to set the QUnit release version in various places. -// -// See also RELEASE.md. -// -// Inspired by . - -const fs = require('fs'); -const path = require('path'); - -const Repo = { - setFiles (version) { - if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) { - throw new Error('Invalid or missing version argument'); - } - { - const file = 'bower.json'; - console.log(`Updating ${file}...`); - const filePath = path.join(__dirname, '..', file); - const json = fs.readFileSync(filePath, 'utf8'); - const packageIndentation = json.match(/\n([\t\s]+)/)[1]; - const data = JSON.parse(json); - - data.version = version; - - fs.writeFileSync( - filePath, - JSON.stringify(data, null, packageIndentation) + '\n' - ); - } - { - const file = 'package.json'; - console.log(`Updating ${file}...`); - const filePath = path.join(__dirname, '..', file); - const json = fs.readFileSync(filePath, 'utf8'); - const packageIndentation = json.match(/\n([\t\s]+)/)[1]; - const data = JSON.parse(json); - - data.version = version; - data.author.url = data.author.url.replace('main', version); - - fs.writeFileSync( - filePath, - JSON.stringify(data, null, packageIndentation) + '\n' - ); - } - } -}; - -const version = process.argv[2]; - -try { - Repo.setFiles(version); -} catch (e) { - console.error(e.toString()); - process.exit(1); -} diff --git a/build/utils.js b/build/utils.js index f3e4cd4ef..9dc4ce1e5 100644 --- a/build/utils.js +++ b/build/utils.js @@ -54,7 +54,7 @@ async function downloadFile (url, dest) { function cleanDir (dirPath) { if (fs.existsSync(dirPath)) { - fs.rmdirSync(dirPath, { recursive: true }); + fs.rmSync(dirPath, { force: true, recursive: true }); } fs.mkdirSync(dirPath, { recursive: true }); }