diff --git a/package.json b/package.json index 1d1cc89..3a6f3ec 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@actions/github": "^5.0.0", "@octokit/core": "^3.5.1", "@octokit/plugin-request-log": "^1.0.3", - "@octokit/plugin-rest-endpoint-methods": "^5.13.0" + "@octokit/plugin-rest-endpoint-methods": "^5.13.0", + "@octokit/request-error": "^2.1.0" }, "devDependencies": { "@types/node": "^14.14.25", diff --git a/src/edit-github-blob-test.ts b/src/edit-github-blob-test.ts new file mode 100644 index 0000000..f64362a --- /dev/null +++ b/src/edit-github-blob-test.ts @@ -0,0 +1,149 @@ +import test from 'ava' +import api from './api' +import { Response } from 'node-fetch' +import editGithubBlob from './edit-github-blob' + +type fetchOptions = { + method: string + body: string | null +} + +function replyJSON(status: number, body: any): Promise { + return Promise.resolve( + new Response(JSON.stringify(body), { + status, + headers: { + 'Content-Type': 'application/json', + }, + }) + ) +} + +test('edit-github-blob direct push', async (t) => { + const stubbedFetch = function (url: string, options: fetchOptions) { + function route(method: string, path: string): boolean { + return ( + method.toUpperCase() === options.method.toUpperCase() && + `https://api.github.com/${path}` === url + ) + } + + if (route('GET', 'repos/OWNER/REPO')) { + return replyJSON(200, { + default_branch: 'main', + permissions: { push: true }, + }) + } else if (route('GET', 'repos/OWNER/REPO/branches/main')) { + return replyJSON(200, { + commit: { sha: 'COMMITSHA' }, + protected: false, + }) + } else if ( + route('GET', 'repos/OWNER/REPO/contents/formula%2Ftest.rb?ref=main') + ) { + return replyJSON(200, { + content: Buffer.from(`old content`).toString('base64'), + }) + } else if (route('PUT', 'repos/OWNER/REPO/contents/formula%2Ftest.rb')) { + const payload = JSON.parse(options.body || '') + t.is('main', payload.branch) + t.is('Update formula/test.rb', payload.message) + t.is( + 'OLD CONTENT', + Buffer.from(payload.content, 'base64').toString('utf8') + ) + return replyJSON(200, { + commit: { html_url: 'https://github.com/OWNER/REPO/commit/NEWSHA' }, + }) + } + throw `not stubbed: ${options.method} ${url}` + } + + const url = await editGithubBlob({ + apiClient: api('ATOKEN', { fetch: stubbedFetch, logRequests: false }), + owner: 'OWNER', + repo: 'REPO', + filePath: 'formula/test.rb', + replace: (oldContent) => oldContent.toUpperCase(), + }) + t.is('https://github.com/OWNER/REPO/commit/NEWSHA', url) +}) + +test('edit-github-blob via pull request', async (t) => { + var newBranchName: string + const stubbedFetch = function (url: string, options: fetchOptions) { + function route(method: string, path: string): boolean { + return ( + method.toUpperCase() === options.method.toUpperCase() && + `https://api.github.com/${path}` === url + ) + } + + if (route('GET', 'repos/OWNER/REPO')) { + return replyJSON(200, { + default_branch: 'main', + permissions: { push: false }, + }) + } else if (route('GET', 'repos/OWNER/REPO/branches/main')) { + return replyJSON(200, { + commit: { sha: 'COMMITSHA' }, + protected: false, + }) + } else if (route('POST', 'repos/OWNER/REPO/forks')) { + return replyJSON(200, {}) + } else if (route('GET', 'user')) { + return replyJSON(200, { login: 'FORKOWNER' }) + } else if (route('POST', 'repos/FORKOWNER/REPO/merge-upstream')) { + const payload = JSON.parse(options.body || '') + t.is('main', payload.branch) + return replyJSON(409, {}) + } else if (route('POST', 'repos/FORKOWNER/REPO/git/refs')) { + const payload = JSON.parse(options.body || '') + t.regex(payload.ref, /^refs\/heads\/update-test\.rb-\d+$/) + newBranchName = payload.ref.replace('refs/heads/', '') + t.is('COMMITSHA', payload.sha) + return replyJSON(201, {}) + } else if ( + route( + 'GET', + `repos/FORKOWNER/REPO/contents/formula%2Ftest.rb?ref=${newBranchName}` + ) + ) { + return replyJSON(200, { + content: Buffer.from(`old content`).toString('base64'), + }) + } else if ( + route('PUT', 'repos/FORKOWNER/REPO/contents/formula%2Ftest.rb') + ) { + const payload = JSON.parse(options.body || '') + t.is(newBranchName, payload.branch) + t.is('Update formula/test.rb', payload.message) + t.is( + 'OLD CONTENT', + Buffer.from(payload.content, 'base64').toString('utf8') + ) + return replyJSON(200, { + commit: { html_url: 'https://github.com/OWNER/REPO/commit/NEWSHA' }, + }) + } else if (route('POST', 'repos/OWNER/REPO/pulls')) { + const payload = JSON.parse(options.body || '') + t.is('main', payload.base) + t.is(`FORKOWNER:${newBranchName}`, payload.head) + t.is('Update formula/test.rb', payload.title) + t.is('', payload.body) + return replyJSON(201, { + html_url: 'https://github.com/OWNER/REPO/pull/123', + }) + } + throw `not stubbed: ${options.method} ${url}` + } + + const url = await editGithubBlob({ + apiClient: api('ATOKEN', { fetch: stubbedFetch, logRequests: false }), + owner: 'OWNER', + repo: 'REPO', + filePath: 'formula/test.rb', + replace: (oldContent) => oldContent.toUpperCase(), + }) + t.is('https://github.com/OWNER/REPO/pull/123', url) +}) diff --git a/src/edit-github-blob.ts b/src/edit-github-blob.ts index 0957c56..d330405 100644 --- a/src/edit-github-blob.ts +++ b/src/edit-github-blob.ts @@ -1,4 +1,5 @@ import type { API } from './api' +import { RequestError } from '@octokit/request-error' import { basename } from 'path' async function retry( @@ -67,10 +68,18 @@ export default async function (params: Options): Promise { const timestamp = Math.round(Date.now() / 1000) headBranch = `update-${basename(filePath)}-${timestamp}` if (needsFork) { - await api.repos.mergeUpstream({ - ...headRepo, - branch: repoRes.data.default_branch, - }) + try { + await api.repos.mergeUpstream({ + ...headRepo, + branch: repoRes.data.default_branch, + }) + } catch (err) { + if (err instanceof RequestError && err.status === 409) { + // ignore + } else { + throw err + } + } } await retry(needsFork ? 6 : 0, 5000, async () => { await api.git.createRef({