chore: ensure release notes always come from Clerk (#23777)
* chore: ensure release notes always come from Clerk Now with tests! * chore: move sinon devDependency into `spec-main` * refactor: tweak note-spec variable for readability
This commit is contained in:
parent
c6c022dc46
commit
980e592271
25 changed files with 372 additions and 72 deletions
185
spec-main/release-notes-spec.ts
Normal file
185
spec-main/release-notes-spec.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
import { GitProcess, IGitExecutionOptions, IGitResult } from 'dugite';
|
||||
import { expect } from 'chai';
|
||||
import * as notes from '../script/release/notes/notes.js';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
/* Fake a Dugite GitProcess that only returns the specific
|
||||
commits that we want to test */
|
||||
|
||||
class Commit {
|
||||
sha1: string;
|
||||
subject: string;
|
||||
constructor (sha1: string, subject: string) {
|
||||
this.sha1 = sha1;
|
||||
this.subject = subject;
|
||||
}
|
||||
}
|
||||
|
||||
class GitFake {
|
||||
branches: {
|
||||
[key: string]: Commit[],
|
||||
};
|
||||
|
||||
constructor () {
|
||||
this.branches = {};
|
||||
}
|
||||
|
||||
setBranch (name: string, commits: Array<Commit>): void {
|
||||
this.branches[name] = commits;
|
||||
}
|
||||
|
||||
// find the newest shared commit between branches a and b
|
||||
mergeBase (a: string, b:string): string {
|
||||
for (const commit of [ ...this.branches[a].reverse() ]) {
|
||||
if (this.branches[b].map((commit: Commit) => commit.sha1).includes(commit.sha1)) {
|
||||
return commit.sha1;
|
||||
}
|
||||
}
|
||||
console.error('test error: branches not related');
|
||||
return '';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
exec (args: string[], path: string, options?: IGitExecutionOptions | undefined): Promise<IGitResult> {
|
||||
let stdout = '';
|
||||
const stderr = '';
|
||||
const exitCode = 0;
|
||||
|
||||
if (args.length === 3 && args[0] === 'merge-base') {
|
||||
// expected form: `git merge-base branchName1 branchName2`
|
||||
const a: string = args[1]!;
|
||||
const b: string = args[2]!;
|
||||
stdout = this.mergeBase(a, b);
|
||||
} else if (args.length === 3 && args[0] === 'log' && args[1] === '--format=%H') {
|
||||
// exepcted form: `git log --format=%H branchName
|
||||
const branch: string = args[2]!;
|
||||
stdout = this.branches[branch].map((commit: Commit) => commit.sha1).join('\n');
|
||||
} else if (args.length > 1 && args[0] === 'log' && args.includes('--format=%H,%s')) {
|
||||
// expected form: `git log --format=%H,%s sha1..branchName
|
||||
const [ start, branch ] = args[args.length - 1].split('..');
|
||||
const lines : string[] = [];
|
||||
let started = false;
|
||||
for (const commit of this.branches[branch]) {
|
||||
started = started || commit.sha1 === start;
|
||||
if (started) {
|
||||
lines.push(`${commit.sha1},${commit.subject}` /* %H,%s */);
|
||||
}
|
||||
}
|
||||
stdout = lines.join('\n');
|
||||
} else {
|
||||
console.error('unhandled GitProcess.exec():', args);
|
||||
}
|
||||
|
||||
return Promise.resolve({ exitCode, stdout, stderr });
|
||||
}
|
||||
}
|
||||
|
||||
describe('release notes', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const gitFake = new GitFake();
|
||||
|
||||
const oldBranch = '8-x-y';
|
||||
const newBranch = '9-x-y';
|
||||
|
||||
// commits shared by both oldBranch and newBranch
|
||||
const sharedHistory = [
|
||||
new Commit('2abea22b4bffa1626a521711bacec7cd51425818', "fix: explicitly cancel redirects when mode is 'error' (#20686)"),
|
||||
new Commit('467409458e716c68b35fa935d556050ca6bed1c4', 'build: add support for automated minor releases (#20620)') // merge-base
|
||||
];
|
||||
|
||||
// these commits came after newBranch was created
|
||||
const newBreaking = new Commit('2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98', 'refactor: use v8 serialization for ipc (#20214)');
|
||||
const newFeat = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: allow GUID parameter to avoid systray demotion on Windows (#21891)');
|
||||
const newFix = new Commit('0600420bac25439fc2067d51c6aaa4ee11770577', "fix: don't allow window to go behind menu bar on mac (#22828)");
|
||||
const oldFix = new Commit('f77bd19a70ac2d708d17ddbe4dc12745ca3a8577', 'fix: prevent menu gc during popup (#20785)');
|
||||
|
||||
// a bug that's fixed in both branches by separate PRs
|
||||
const newTropFix = new Commit('a6ff42c190cb5caf8f3e217748e49183a951491b', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22750)');
|
||||
const oldTropFix = new Commit('8751f485c5a6c8c78990bfd55a4350700f81f8cd', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22749)');
|
||||
|
||||
before(() => {
|
||||
// location of relase-notes' octokit reply cache
|
||||
const fixtureDir = path.resolve(__dirname, 'fixtures', 'release-notes');
|
||||
process.env.NOTES_CACHE_PATH = path.resolve(fixtureDir, 'cache');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const wrapper = (args: string[], path: string, options?: IGitExecutionOptions | undefined) => gitFake.exec(args, path, options);
|
||||
sandbox.replace(GitProcess, 'exec', wrapper);
|
||||
|
||||
gitFake.setBranch(oldBranch, [ ...sharedHistory, oldFix ]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('changes that exist in older branches', () => {
|
||||
// use case: this fix is NOT news because it was already fixed
|
||||
// while oldBranch was the latest stable release
|
||||
it('are skipped if the target version is a new major line (x.0.0)', async function () {
|
||||
const version = 'v9.0.0';
|
||||
gitFake.setBranch(oldBranch, [ ...sharedHistory, oldTropFix ]);
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, newTropFix ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.fix).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
// use case: this fix IS news because it's being fixed in
|
||||
// multiple stable branches at once, including newBranch.
|
||||
it('are included if the target version is a minor or patch bump', async function () {
|
||||
const version = 'v9.0.1';
|
||||
gitFake.setBranch(oldBranch, [ ...sharedHistory, oldTropFix ]);
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, newTropFix ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.fix).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
// use case: A malicious contributor could edit the text of their 'Notes:'
|
||||
// in the PR body after a PR's been merged and the maintainers have moved on.
|
||||
// So instead always use the release-clerk PR comment
|
||||
it('uses the release-clerk text', async function () {
|
||||
// realText source: ${fixtureDir}/electron-electron-issue-21891-comments
|
||||
const realText = 'Added GUID parameter to Tray API to avoid system tray icon demotion on Windows';
|
||||
const testCommit = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: lole u got troled hard (#21891)');
|
||||
const version = 'v9.0.0';
|
||||
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.feat).to.have.lengthOf(1);
|
||||
expect(results.feat[0].hash).to.equal(testCommit.sha1);
|
||||
expect(results.feat[0].note).to.equal(realText);
|
||||
});
|
||||
|
||||
// test that when you feed in different semantic commit types,
|
||||
// the parser returns them in the results' correct category
|
||||
describe('semantic commit', () => {
|
||||
const version = 'v9.0.0';
|
||||
|
||||
it("honors 'feat' type", async function () {
|
||||
const testCommit = newFeat;
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.feat).to.have.lengthOf(1);
|
||||
expect(results.feat[0].hash).to.equal(testCommit.sha1);
|
||||
});
|
||||
|
||||
it("honors 'fix' type", async function () {
|
||||
const testCommit = newFix;
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.fix).to.have.lengthOf(1);
|
||||
expect(results.fix[0].hash).to.equal(testCommit.sha1);
|
||||
});
|
||||
|
||||
it("honors 'BREAKING CHANGE' message", async function () {
|
||||
const testCommit = newBreaking;
|
||||
gitFake.setBranch(newBranch, [ ...sharedHistory, testCommit ]);
|
||||
const results: any = await notes.get(oldBranch, newBranch, version);
|
||||
expect(results.breaking).to.have.lengthOf(1);
|
||||
expect(results.breaking[0].hash).to.equal(testCommit.sha1);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue