diff --git a/README.md b/README.md index c6693f1..adf0c81 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/action.yml b/action.yml index fc2fa3d..49d3778 100644 --- a/action.yml +++ b/action.yml @@ -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' diff --git a/dist/index.js b/dist/index.js index bbe5a37..f8e2c0b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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; }; @@ -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))); @@ -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; @@ -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 @@ -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)) } }); @@ -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; @@ -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); + } } } } diff --git a/src/__tests__/getRequestsFromIssue.test.ts b/src/__tests__/getRequestsFromIssue.test.ts index c5a1fd3..449d10d 100644 --- a/src/__tests__/getRequestsFromIssue.test.ts +++ b/src/__tests__/getRequestsFromIssue.test.ts @@ -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 diff --git a/src/_main.ts b/src/_main.ts index 56d003c..6630416 100644 --- a/src/_main.ts +++ b/src/_main.ts @@ -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 @@ -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) + } } } } diff --git a/src/getContext.ts b/src/getContext.ts index 2b202d3..9e771f9 100644 --- a/src/getContext.ts +++ b/src/getContext.ts @@ -1,6 +1,16 @@ import {getInput} from '@actions/core' -export type Context = ReturnType +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! @@ -8,20 +18,22 @@ export const getContext = () => { 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 diff --git a/src/getIssues.ts b/src/getIssues.ts index 05abe7e..f20ce14 100644 --- a/src/getIssues.ts +++ b/src/getIssues.ts @@ -33,8 +33,8 @@ export async function getIssue(context: Context, issue: number): Promise export async function getIssues(context: Context): Promise { 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) diff --git a/src/gitBisectTypeScript.ts b/src/gitBisectTypeScript.ts index 073cf3d..15e8692 100644 --- a/src/gitBisectTypeScript.ts +++ b/src/gitBisectTypeScript.ts @@ -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 @@ -85,10 +86,9 @@ 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 @@ -96,7 +96,7 @@ function getRevisionsFromPreviousRun( } } -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, @@ -104,24 +104,36 @@ function getRevisionsFromComment( ): 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 diff --git a/src/updatesIssue.ts b/src/updatesIssue.ts index c5ff3eb..55c6dcb 100644 --- a/src/updatesIssue.ts +++ b/src/updatesIssue.ts @@ -10,13 +10,16 @@ import {API} from './utils/api' import {getTypeScriptNightlyVersion} from './utils/getTypeScriptNightlyVersion' import {TwoslashRequest} from './getRequestsFromIssue' import {BisectResult} from './gitBisectTypeScript' +import {Context} from './getContext' -export async function fixOrDeleteOldComments(issue: Issue, api: API): Promise { +export async function fixOrDeleteOldComments(issue: Issue, api: API, context: Context): Promise { const outdatedComments = 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 {