From 11ce557d95faf1583eaba6f42456fae0ffed7cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= <git@mislav.net> Date: Sat, 27 Feb 2021 18:06:00 +0100 Subject: [PATCH] Refactor to be more testable --- package.json | 15 +++--- src/api.ts | 33 ++++++------ src/calculate-download-checksum.ts | 5 +- src/edit-github-blob.ts | 13 +++-- src/main-test.ts | 81 ++++++++++++++++++++++++++++++ src/main.ts | 53 +++++++++++++------ src/replace-formula-fields-test.ts | 22 ++++++++ src/test.ts | 61 ---------------------- src/version-test.ts | 17 +++++++ 9 files changed, 196 insertions(+), 104 deletions(-) create mode 100644 src/main-test.ts create mode 100644 src/replace-formula-fields-test.ts delete mode 100644 src/test.ts create mode 100644 src/version-test.ts diff --git a/package.json b/package.json index 925c649..bf5edaf 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,18 @@ "scripts": { "build": "rm -rf lib && ncc build src/run.ts -o lib --source-map", "lint": "eslint --ext '.js,.ts' .", - "test": "tsc && ava" + "test": "tsc --sourceMap && ava" }, "dependencies": { - "@actions/core": "^1.2.4", - "@octokit/core": "^3.1.1", - "@octokit/plugin-request-log": "^1.0.0", - "@octokit/plugin-rest-endpoint-methods": "^4.1.2" + "@actions/core": "^1.2.6", + "@actions/github": "^4.0.0", + "@octokit/core": "^3.2.5", + "@octokit/plugin-request-log": "^1.0.3", + "@octokit/plugin-rest-endpoint-methods": "^4.10.1" }, "devDependencies": { - "@types/node": "^12.7.5", + "@types/node": "^14.14.25", + "@types/node-fetch": "^2.5.8", "@typescript-eslint/eslint-plugin": "^3.7.1", "@typescript-eslint/parser": "^3.7.1", "@zeit/ncc": "^0.22.3", @@ -20,6 +22,7 @@ "eslint": "^7.5.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.4", + "node-fetch": "^2.6.1", "prettier": "^2.0.5", "typescript": "^3.9.7" }, diff --git a/src/api.ts b/src/api.ts index 1956966..51e9d0a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -9,23 +9,24 @@ const GitHub = Octokit.plugin(restEndpointMethods, requestLog).defaults({ export type API = InstanceType<typeof GitHub> -type LogMethod = (msg: any, ...params: any[]) => void - -type Logger = { - info?: LogMethod - debug?: LogMethod -} - -export default function (token: string): API { - const log: Logger = { - info: console.log, - } - if (isDebug()) { - log.debug = console.debug - } - +export default function (token: string, options?: {fetch?: any}): API { return new GitHub({ + request: {fetch: options?.fetch}, auth: `token ${token}`, - log, + log: { + info(msg: string) { + return console.info(msg) + }, + debug(msg: string) { + if (!isDebug()) return + return console.debug(msg) + }, + warn(msg: string) { + return console.warn(msg) + }, + error(msg: string) { + return console.error(msg) + }, + }, }) } diff --git a/src/calculate-download-checksum.ts b/src/calculate-download-checksum.ts index 3fcad8b..25a1402 100644 --- a/src/calculate-download-checksum.ts +++ b/src/calculate-download-checksum.ts @@ -39,10 +39,11 @@ async function resolveDownload(api: API, url: URL): Promise<URL> { ) if (archive != null) { const [, owner, repo, ref, ext] = archive - const res = await api.repos.downloadArchive({ + const res = await (ext == '.zip' + ? api.repos.downloadZipballArchive + : api.repos.downloadTarballArchive)({ owner, repo, - archive_format: ext == '.zip' ? 'zipball' : 'tarball', ref, request: { redirect: 'manual', diff --git a/src/edit-github-blob.ts b/src/edit-github-blob.ts index db153e9..572ab81 100644 --- a/src/edit-github-blob.ts +++ b/src/edit-github-blob.ts @@ -20,7 +20,7 @@ async function retry<T>( } } -type Options = { +export type Options = { owner: string repo: string filePath: string @@ -47,12 +47,15 @@ export default async function (params: Options): Promise<string> { branch: baseBranch, }) - const needsFork = !repoRes.data.permissions.push + const needsFork = !repoRes.data.permissions?.push if (needsFork) { - const forkRes = await api.repos.createFork(baseRepo) + const res = await Promise.all([ + api.repos.createFork(baseRepo), + api.users.getAuthenticated(), + ]) headRepo = { - owner: forkRes.data.owner.login, - repo: forkRes.data.name, + owner: res[1].data.login, + repo: baseRepo.repo, } } diff --git a/src/main-test.ts b/src/main-test.ts new file mode 100644 index 0000000..e58b30e --- /dev/null +++ b/src/main-test.ts @@ -0,0 +1,81 @@ +import test from 'ava' +import api from './api' +import { commitForRelease, prepareEdit } from './main' +import { Response } from 'node-fetch' + +test('commitForRelease()', (t) => { + t.is( + commitForRelease('This is a fixed commit message', { + formulaName: 'test formula', + }), + 'This is a fixed commit message' + ) + t.is( + commitForRelease('chore({{formulaName}}): version {{version}}', { + formulaName: 'test formula', + }), + 'chore(test formula): version {{version}}' + ) + t.is( + commitForRelease('chore({{formulaName}}): upgrade to version {{version}}', { + formulaName: 'test formula', + version: 'v1.2.3', + }), + 'chore(test formula): upgrade to version v1.2.3' + ) +}) + +test('prepareEdit()', async (t) => { + const ctx = { + sha: 'TAGSHA', + ref: 'refs/tags/v0.8.2', + repo: { + owner: 'OWNER', + repo: 'REPO', + }, + } + + process.env['INPUT_HOMEBREW-TAP'] = 'Homebrew/homebrew-core' + process.env['INPUT_COMMIT-MESSAGE'] = 'Upgrade {{formulaName}} to {{version}}' + + // FIXME: this tests results in a live HTTP request. Figure out how to stub the `stream()` method in + // calculate-download-checksum. + const stubbedFetch = function (url: string) { + if (url == 'https://api.github.com/repos/OWNER/REPO/tarball/v0.8.2') { + return Promise.resolve( + new Response('', { + status: 301, + headers: { + Location: + 'https://github.com/mislav/bump-homebrew-formula-action/archive/v1.9.tar.gz', + }, + }) + ) + } + throw url + } + const apiClient = api('ATOKEN', { fetch: stubbedFetch }) + + const opts = await prepareEdit(ctx, apiClient, apiClient) + t.is(opts.owner, 'Homebrew') + t.is(opts.repo, 'homebrew-core') + t.is(opts.branch, '') + t.is(opts.filePath, 'Formula/repo.rb') + t.is(opts.commitMessage, 'Upgrade repo to 0.8.2') + + const oldFormula = ` + class MyProgram < Formula + url "OLDURL" + sha256 "OLDSHA" + end + ` + t.is( + opts.replace(oldFormula), + ` + class MyProgram < Formula + url "https://github.com/OWNER/REPO/archive/v0.8.2.tar.gz" + sha256 "c036fbc44901b266f6d408d6ca36ba56f63c14cc97994a935fb9741b55edee83" + end + ` + ) +}) diff --git a/src/main.ts b/src/main.ts index b0867dc..153befa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ import { getInput } from '@actions/core' import type { API } from './api' import editGitHubBlob from './edit-github-blob' +import { Options as EditOptions } from './edit-github-blob' import { replaceFields } from './replace-formula-fields' import calculateDownloadChecksum from './calculate-download-checksum' +import { context } from '@actions/github' function tarballForRelease( owner: string, @@ -27,19 +29,34 @@ export default async function (api: (token: string) => API): Promise<void> { process.env.GITHUB_TOKEN || process.env.COMMITTER_TOKEN || '' const externalToken = process.env.COMMITTER_TOKEN || '' - const [contextOwner, contextRepoName] = (process.env - .GITHUB_REPOSITORY as string).split('/') + const options = await prepareEdit(context, api(internalToken), api(externalToken)) + const createdUrl = await editGitHubBlob(options) + console.log(createdUrl) +} + +type Context = { + ref: string + sha: string + repo: { + owner: string + repo: string + } +} + +export async function prepareEdit(ctx: Context, sameRepoClient: API, crossRepoClient: API): Promise<EditOptions> { + const tagName = getInput('tag-name') || ((ref) => { + if (!ref.startsWith('refs/tags/')) throw `invalid ref: ${ref}` + return ref.replace('refs/tags/', '') + })(ctx.ref) const [owner, repo] = getInput('homebrew-tap', { required: true }).split('/') - const formulaName = getInput('formula-name') || contextRepoName.toLowerCase() + const formulaName = getInput('formula-name') || ctx.repo.repo.toLowerCase() const branch = getInput('base-branch') const filePath = `Formula/${formulaName}.rb` - const tagName = (process.env.GITHUB_REF as string).replace('refs/tags/', '') - const tagSha = process.env.GITHUB_SHA as string - const version = getInput('tag-name') || tagName.replace(/^v(\d)/, '$1') + const version = tagName.replace(/^v(\d)/, '$1') const downloadUrl = getInput('download-url') || - tarballForRelease(contextOwner, contextRepoName, tagName) + tarballForRelease(ctx.repo.owner, ctx.repo.repo, tagName) const messageTemplate = getInput('commit-message', { required: true }) const replacements = new Map<string, string>() @@ -47,11 +64,20 @@ export default async function (api: (token: string) => API): Promise<void> { replacements.set('url', downloadUrl) if (downloadUrl.endsWith('.git')) { replacements.set('tag', tagName) - replacements.set('revision', tagSha) + replacements.set('revision', await (async () => { + if (ctx.ref == `refs/tags/${tagName}`) return ctx.sha + else { + const res = await sameRepoClient.git.getRef({ + ...ctx.repo, + ref: `tags/${tagName}` + }) + return res.data.object.sha + } + })()) } else { replacements.set( 'sha256', - await calculateDownloadChecksum(api(internalToken), downloadUrl, 'sha256') + await calculateDownloadChecksum(sameRepoClient, downloadUrl, 'sha256') ) } @@ -60,16 +86,15 @@ export default async function (api: (token: string) => API): Promise<void> { version, }) - const createdUrl = await editGitHubBlob({ - apiClient: api(externalToken), + return { + apiClient: crossRepoClient, owner, repo, branch, filePath, commitMessage, - replace(oldContent) { + replace(oldContent: string) { return replaceFields(oldContent, replacements) }, - }) - console.log(createdUrl) + } } diff --git a/src/replace-formula-fields-test.ts b/src/replace-formula-fields-test.ts new file mode 100644 index 0000000..ef615ba --- /dev/null +++ b/src/replace-formula-fields-test.ts @@ -0,0 +1,22 @@ +import test from 'ava' +import { replaceFields } from './replace-formula-fields' + +test('replaceFields()', (t) => { + const input = ` + url "https://github.com/old/url.git", + tag: 'v0.9.0', + revision => "OLDREV" +` + const expected = ` + url "https://github.com/cli/cli.git", + tag: 'v0.11.1', + revision => "NEWREV" +` + + const replacements = new Map<string, string>() + replacements.set('url', 'https://github.com/cli/cli.git') + replacements.set('tag', 'v0.11.1') + replacements.set('revision', 'NEWREV') + + t.is(replaceFields(input, replacements), expected) +}) diff --git a/src/test.ts b/src/test.ts deleted file mode 100644 index d702ae7..0000000 --- a/src/test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import test from 'ava' -import { commitForRelease } from './main' -import { fromUrl } from './version' -import { replaceFields } from './replace-formula-fields' - -test('version.fromUrl()', (t) => { - t.is( - fromUrl('https://github.com/me/myproject/archive/v1.2.3.tar.gz'), - 'v1.2.3' - ) - t.is( - fromUrl( - 'https://github.com/me/myproject/releases/download/v1.2.3/file.tgz' - ), - 'v1.2.3' - ) - t.is(fromUrl('http://myproject.net/download/v1.2.3.tgz'), 'v1.2.3') - t.is(fromUrl('https://example.com/v1.2.3.zip'), 'v1.2.3') -}) - -test('main.commitForRelease()', (t) => { - t.is( - commitForRelease('This is a fixed commit message', { - formulaName: 'test formula', - }), - 'This is a fixed commit message' - ) - t.is( - commitForRelease('chore({{formulaName}}): version {{version}}', { - formulaName: 'test formula', - }), - 'chore(test formula): version {{version}}' - ) - t.is( - commitForRelease('chore({{formulaName}}): upgrade to version {{version}}', { - formulaName: 'test formula', - version: 'v1.2.3', - }), - 'chore(test formula): upgrade to version v1.2.3' - ) -}) - -test('replace-formula-fields.replaceFields()', (t) => { - const input = ` - url "https://github.com/old/url.git", - tag: 'v0.9.0', - revision => "OLDREV" -` - const expected = ` - url "https://github.com/cli/cli.git", - tag: 'v0.11.1', - revision => "NEWREV" -` - - const replacements = new Map<string, string>() - replacements.set('url', 'https://github.com/cli/cli.git') - replacements.set('tag', 'v0.11.1') - replacements.set('revision', 'NEWREV') - - t.is(replaceFields(input, replacements), expected) -}) diff --git a/src/version-test.ts b/src/version-test.ts new file mode 100644 index 0000000..163d506 --- /dev/null +++ b/src/version-test.ts @@ -0,0 +1,17 @@ +import test from 'ava' +import { fromUrl } from './version' + +test('fromUrl()', (t) => { + t.is( + fromUrl('https://github.com/me/myproject/archive/v1.2.3.tar.gz'), + 'v1.2.3' + ) + t.is( + fromUrl( + 'https://github.com/me/myproject/releases/download/v1.2.3/file.tgz' + ), + 'v1.2.3' + ) + t.is(fromUrl('http://myproject.net/download/v1.2.3.tgz'), 'v1.2.3') + t.is(fromUrl('https://example.com/v1.2.3.zip'), 'v1.2.3') +})