Skip to content

Commit

Permalink
Ignore HTTP 409 when trying to fast-forward main to upstream
Browse files Browse the repository at this point in the history
Even if the main branch wasn't fast-forwarded, we completed the primary
objective of making the objects from upstream available to fork.

Fixes #24
  • Loading branch information
mislav committed Dec 23, 2021
1 parent e1f3c48 commit 0104f12
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 5 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
149 changes: 149 additions & 0 deletions 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<Response> {
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)
})
17 changes: 13 additions & 4 deletions 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<T>(
Expand Down Expand Up @@ -67,10 +68,18 @@ export default async function (params: Options): Promise<string> {
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({
Expand Down

0 comments on commit 0104f12

Please sign in to comment.