Skip to content

Commit

Permalink
Merge pull request #36 from microsoft/bisect-improvements
Browse files Browse the repository at this point in the history
Bisect improvements
  • Loading branch information
andrewbranch committed Sep 12, 2022
2 parents 5f66675 + a64896f commit e19e628
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 73 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ $ yarn test
...
```

Run or debug locally
```bash
yarn build

GITHUB_WORKSPACE=/absolute/path/to/TypeScript \
GITHUB_TOKEN=$token \ # will comment without DRY!
DRY=1 \ # do not post results comment
ISSUE=50635 \ # optional
BISECT="good 4.7.3 bad main" \ # optional
node lib/_main.js
```

## To publish

Actions are run from GitHub repos so we will check-in the packed dist folder.
Expand Down
6 changes: 5 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ inputs:
description: 'What tag should be applied to the markdown code block?'
default: 'repro'

issue:
required: false
description: 'Limits analysis to a single issue'

bisect:
required: false
description: 'Request a git bisect against an issue that already has a repro. Value is the issue number.'
description: 'Runs a git bisect on an existing repro. Requires `issue` to be set. Value can be revision labels (e.g. `good v4.7.3 bad main`) or `true` to infer bisect range.'

runs:
using: 'node16'
Expand Down
86 changes: 51 additions & 35 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,21 @@ const getContext = () => {
const workspace = process.env.GITHUB_WORKSPACE;
const label = (0, core_1.getInput)('label') || 'Has Repro';
const tag = (0, core_1.getInput)('code-tag') || 'repro';
const runIssue = (0, core_1.getInput)('issue') || process.env.ISSUE;
const bisectIssue = (0, core_1.getInput)('bisect') || process.env.BISECT_ISSUE;
const issue = (0, core_1.getInput)('issue') || process.env.ISSUE;
const bisect = (0, core_1.getInput)('bisect') || process.env.BISECT;
const owner = repo.split('/')[0];
const name = repo.split('/')[1];
const dryRun = !!process.env.DRY;
const ctx = {
token,
owner,
name,
label,
tag,
runIssue,
bisectIssue,
workspace
issue,
bisect,
workspace,
dryRun
};
return ctx;
};
Expand All @@ -120,8 +122,8 @@ async function getIssue(context, issue) {
exports.getIssue = getIssue;
async function getIssues(context) {
const octokit = (0, github_1.getOctokit)(context.token);
if (context.runIssue) {
return [await getIssue(context, parseInt(context.runIssue, 10))];
if (context.issue) {
return [await getIssue(context, parseInt(context.issue, 10))];
}
const req = issuesQuery(context.owner, context.name, context.label);
const initialIssues = (await octokit.graphql(req.query, Object.assign({}, req.vars)));
Expand Down Expand Up @@ -265,7 +267,8 @@ async function gitBisectTypeScript(context, issue) {
const requests = (0, getRequestsFromIssue_1.getRequestsFromIssue)(context)(issue);
const request = requests[requests.length - 1];
const resultComment = request && (0, getExistingComments_1.getResultCommentInfoForRequest)(issue.comments.nodes, request);
const bisectRevisions = getRevisionsFromComment(issue, request, context) ||
const bisectRevisions = getRevisionsFromContext(context, request) ||
getRevisionsFromComment(issue, request, context) ||
(resultComment && getRevisionsFromPreviousRun(resultComment, context));
if (!bisectRevisions)
return;
Expand Down Expand Up @@ -317,37 +320,43 @@ function getRevisionsFromPreviousRun(resultComment, context) {
const oldRef = `v${oldResult.label}`;
const newRef = newResult.label === 'Nightly' ? resultComment.info.typescriptSha : `v${newResult.label}`;
const oldMergeBase = (0, child_process_1.execSync)(`git merge-base ${oldRef} main`, { cwd: context.workspace, encoding: 'utf8' }).trim();
const newMergeBase = (0, child_process_1.execSync)(`git merge-base ${newRef} main`, { cwd: context.workspace, encoding: 'utf8' }).trim();
return {
oldRef: oldMergeBase,
newRef: newMergeBase,
newRef,
oldLabel: oldResult.label,
newLabel: newResult.label,
oldResult
};
}
}
const bisectCommentRegExp = /^@typescript-bot bisect (?:this )?(?:good|old) ([^\s]+) (?:bad|new) ([^\s]+)/;
const bisectCommentRegExp = /^(?:@typescript-bot bisect (?:this )?)?(?:good|old) ([^\s]+) (?:bad|new) ([^\s]+)/;
function getRevisionsFromComment(issue, request, context) {
for (let i = issue.comments.nodes.length - 1; i >= 0; i--) {
const comment = issue.comments.nodes[i];
const match = comment.body.match(bisectCommentRegExp);
if (match) {
const [, oldLabel, newLabel] = match;
const oldRef = (0, child_process_1.execSync)(`git merge-base ${oldLabel} main`, { cwd: context.workspace, encoding: 'utf8' }).trim();
const newRef = (0, child_process_1.execSync)(`git merge-base ${newLabel} main`, { cwd: context.workspace, encoding: 'utf8' }).trim();
(0, child_process_1.execSync)(`git checkout ${oldRef}`, { cwd: context.workspace });
const oldResult = buildAndRun(request, context);
return {
oldRef,
newRef,
oldLabel,
newLabel,
oldResult
};
}
const revs = tryGetRevisionsFromText(comment.body, request, context);
if (revs)
return revs;
}
}
function tryGetRevisionsFromText(text, request, context) {
const match = text.match(bisectCommentRegExp);
if (match) {
const [, oldLabel, newLabel] = match;
const oldRef = (0, child_process_1.execSync)(`git merge-base ${oldLabel} main`, { cwd: context.workspace, encoding: 'utf8' }).trim();
(0, child_process_1.execSync)(`git checkout ${oldRef}`, { cwd: context.workspace });
const oldResult = buildAndRun(request, context);
return {
oldRef,
newRef: newLabel,
oldLabel,
newLabel,
oldResult
};
}
}
function getRevisionsFromContext(context, request) {
return tryGetRevisionsFromText(context.bisect, request, context);
}
function buildAndRun(request, context) {
try {
// Try building without npm install for speed, it will work a fair amount of the time
Expand Down Expand Up @@ -474,12 +483,14 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createCommentText = exports.updateIssue = exports.postBisectComment = exports.fixOrDeleteOldComments = void 0;
const getExistingComments_1 = __nccwpck_require__(2408);
const getTypeScriptNightlyVersion_1 = __nccwpck_require__(1371);
async function fixOrDeleteOldComments(issue, api) {
async function fixOrDeleteOldComments(issue, api, context) {
const outdatedComments = (0, getExistingComments_1.getAllTypeScriptBotComments)(issue.comments.nodes)
.filter(c => c.info.version !== 1)
.map(c => c.comment);
for (const comment of outdatedComments) {
await api.deleteComment(comment.id);
if (!context.dryRun) {
for (const comment of outdatedComments) {
await api.deleteComment(comment.id);
}
}
if (outdatedComments.length) {
return Object.assign(Object.assign({}, issue), { comments: { nodes: issue.comments.nodes.filter(c => !outdatedComments.includes(c)) } });
Expand Down Expand Up @@ -20052,11 +20063,14 @@ async function run() {
const ctx = (0, getContext_1.getContext)();
const api = (0, api_1.createAPI)(ctx);
console.log(`Context: ${JSON.stringify(ctx, null, ' ')}`);
if (ctx.bisectIssue) {
let issue = await (0, getIssues_1.getIssue)(ctx, parseInt(ctx.bisectIssue, 10));
issue = await (0, updatesIssue_1.fixOrDeleteOldComments)(issue, api);
if (ctx.bisect) {
if (!ctx.issue) {
throw new Error('Must provide an issue number to bisect');
}
let issue = await (0, getIssues_1.getIssue)(ctx, parseInt(ctx.issue, 10));
issue = await (0, updatesIssue_1.fixOrDeleteOldComments)(issue, api, ctx);
const result = await (0, gitBisectTypeScript_1.gitBisectTypeScript)(ctx, issue);
if (result) {
if (result && !ctx.dryRun) {
await (0, updatesIssue_1.postBisectComment)(issue, result, api);
}
return;
Expand All @@ -20068,11 +20082,13 @@ async function run() {
process.stdout.write('.');
if (issues.indexOf(issue) % 10)
console.log('');
issue = await (0, updatesIssue_1.fixOrDeleteOldComments)(issue, api);
issue = await (0, updatesIssue_1.fixOrDeleteOldComments)(issue, api, ctx);
const requests = (0, getRequestsFromIssue_1.getRequestsFromIssue)(ctx)(issue);
for (const request of requests) {
const results = (0, runTwoslashRequests_1.runTwoslashRequests)(issue, request);
await (0, updatesIssue_1.updateIssue)(request, issue, results, api);
if (!ctx.dryRun) {
await (0, updatesIssue_1.updateIssue)(request, issue, results, api);
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/__tests__/getRequestsFromIssue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const testCtx: Context = {
token: '123456',
tag: 'repro',
workspace: '',
bisectIssue: undefined,
runIssue: undefined,
bisect: undefined,
issue: undefined,
dryRun: false,
}

const oneCodeBlock = ` comment blah
Expand Down
17 changes: 11 additions & 6 deletions src/_main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ async function run() {
const api = createAPI(ctx)
console.log(`Context: ${JSON.stringify(ctx, null, ' ')}`)

if (ctx.bisectIssue) {
let issue = await getIssue(ctx, parseInt(ctx.bisectIssue, 10))
issue = await fixOrDeleteOldComments(issue, api)
if (ctx.bisect) {
if (!ctx.issue) {
throw new Error('Must provide an issue number to bisect')
}
let issue = await getIssue(ctx, parseInt(ctx.issue, 10))
issue = await fixOrDeleteOldComments(issue, api, ctx)

const result = await gitBisectTypeScript(ctx, issue)
if (result) {
if (result && !ctx.dryRun) {
await postBisectComment(issue, result, api)
}
return
Expand All @@ -32,11 +35,13 @@ async function run() {
process.stdout.write('.')
if (issues.indexOf(issue) % 10) console.log('')

issue = await fixOrDeleteOldComments(issue, api)
issue = await fixOrDeleteOldComments(issue, api, ctx)
const requests = getRequestsFromIssue(ctx)(issue)
for (const request of requests) {
const results = runTwoslashRequests(issue, request)
await updateIssue(request, issue, results, api)
if (!ctx.dryRun) {
await updateIssue(request, issue, results, api)
}
}
}
}
Expand Down
26 changes: 19 additions & 7 deletions src/getContext.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import {getInput} from '@actions/core'

export type Context = ReturnType<typeof getContext>
export interface Context {
token: string
owner: string
name: string
label: string
tag: string
issue: string | undefined
bisect: string | undefined
workspace: string
dryRun: boolean
}

export const getContext = () => {
const token = getInput('github-token') || process.env.GITHUB_TOKEN!
const repo = getInput('repo') || process.env.GITHUB_REPOSITORY || 'microsoft/TypeScript'
const workspace = process.env.GITHUB_WORKSPACE!
const label = getInput('label') || 'Has Repro'
const tag = getInput('code-tag') || 'repro'
const runIssue = getInput('issue') || process.env.ISSUE
const bisectIssue = getInput('bisect') || (process.env.BISECT_ISSUE as string | undefined)
const issue = getInput('issue') || process.env.ISSUE
const bisect = getInput('bisect') || (process.env.BISECT as string | undefined)
const owner = repo.split('/')[0]
const name = repo.split('/')[1]
const dryRun = !!process.env.DRY

const ctx = {
const ctx: Context = {
token,
owner,
name,
label,
tag,
runIssue,
bisectIssue,
workspace
issue,
bisect,
workspace,
dryRun
}

return ctx
Expand Down
4 changes: 2 additions & 2 deletions src/getIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export async function getIssue(context: Context, issue: number): Promise<Issue>

export async function getIssues(context: Context): Promise<Issue[]> {
const octokit = getOctokit(context.token)
if (context.runIssue) {
return [await getIssue(context, parseInt(context.runIssue, 10))]
if (context.issue) {
return [await getIssue(context, parseInt(context.issue, 10))]
}

const req = issuesQuery(context.owner, context.name, context.label)
Expand Down
46 changes: 29 additions & 17 deletions src/gitBisectTypeScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export async function gitBisectTypeScript(context: Context, issue: Issue): Promi
const request = requests[requests.length - 1]
const resultComment = request && getResultCommentInfoForRequest(issue.comments.nodes, request)
const bisectRevisions =
getRevisionsFromContext(context, request) ||
getRevisionsFromComment(issue, request, context) ||
(resultComment && getRevisionsFromPreviousRun(resultComment, context))
if (!bisectRevisions) return
Expand Down Expand Up @@ -85,43 +86,54 @@ function getRevisionsFromPreviousRun(
const oldRef = `v${oldResult.label}`
const newRef = newResult.label === 'Nightly' ? resultComment.info.typescriptSha : `v${newResult.label}`
const oldMergeBase = execSync(`git merge-base ${oldRef} main`, {cwd: context.workspace, encoding: 'utf8'}).trim()
const newMergeBase = execSync(`git merge-base ${newRef} main`, {cwd: context.workspace, encoding: 'utf8'}).trim()
return {
oldRef: oldMergeBase,
newRef: newMergeBase,
newRef,
oldLabel: oldResult.label,
newLabel: newResult.label,
oldResult
}
}
}

const bisectCommentRegExp = /^@typescript-bot bisect (?:this )?(?:good|old) ([^\s]+) (?:bad|new) ([^\s]+)/
const bisectCommentRegExp = /^(?:@typescript-bot bisect (?:this )?)?(?:good|old) ([^\s]+) (?:bad|new) ([^\s]+)/
function getRevisionsFromComment(
issue: Issue,
request: TwoslashRequest,
context: Context
): BisectRevisions | undefined {
for (let i = issue.comments.nodes.length - 1; i >= 0; i--) {
const comment = issue.comments.nodes[i]
const match = comment.body.match(bisectCommentRegExp)
if (match) {
const [, oldLabel, newLabel] = match
const oldRef = execSync(`git merge-base ${oldLabel} main`, {cwd: context.workspace, encoding: 'utf8'}).trim()
const newRef = execSync(`git merge-base ${newLabel} main`, {cwd: context.workspace, encoding: 'utf8'}).trim()
execSync(`git checkout ${oldRef}`, {cwd: context.workspace})
const oldResult = buildAndRun(request, context)
return {
oldRef,
newRef,
oldLabel,
newLabel,
oldResult
}
const revs = tryGetRevisionsFromText(comment.body, request, context)
if (revs) return revs
}
}

function tryGetRevisionsFromText(
text: string,
request: TwoslashRequest,
context: Context
): BisectRevisions | undefined {
const match = text.match(bisectCommentRegExp)
if (match) {
const [, oldLabel, newLabel] = match
const oldRef = execSync(`git merge-base ${oldLabel} main`, {cwd: context.workspace, encoding: 'utf8'}).trim()
execSync(`git checkout ${oldRef}`, {cwd: context.workspace})
const oldResult = buildAndRun(request, context)
return {
oldRef,
newRef: newLabel,
oldLabel,
newLabel,
oldResult
}
}
}

function getRevisionsFromContext(context: Context, request: TwoslashRequest): BisectRevisions | undefined {
return tryGetRevisionsFromText(context.bisect!, request, context)
}

function buildAndRun(request: TwoslashRequest, context: Context) {
try {
// Try building without npm install for speed, it will work a fair amount of the time
Expand Down

0 comments on commit e19e628

Please sign in to comment.