Skip to content

Commit

Permalink
Merge pull request #1 from bflad/td-github-actions-template
Browse files Browse the repository at this point in the history
Initial jest unit testing and ncc preparation
  • Loading branch information
milescrabill authored Jun 29, 2021
2 parents 8792cad + d94aba8 commit 55efb3b
Show file tree
Hide file tree
Showing 12 changed files with 8,429 additions and 1,306 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"env": {
"browser": true,
"commonjs": true,
"es2021": true
"es2021": true,
"jest": true
},
"extends": [
"standard"
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Testing"
on:
pull_request:
push:
branches:
- main
- 'releases/*'

jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm test
112 changes: 112 additions & 0 deletions action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict'

const fs = require('fs').promises
const os = require('os')
const path = require('path')
const crypto = require('crypto')

const core = require('@actions/core')
const tc = require('@actions/tool-cache')

const octokit = require('./octokit')

const owner = 'hashicorp'
const repo = 'signore'

// adapted from setup-terraform
// arch in [arm, x32, x64...] (https://nodejs.org/api/os.html#os_os_arch)
// return value in [x86_64, 386, arm]
function mapArch(arch) {
const mappings = {
x32: '386',
x64: 'x86_64'
}
return mappings[arch] || arch
}

// adapted from setup-terraform
// os in [darwin, linux, win32...] (https://nodejs.org/api/os.html#os_os_platform)
// return value in [darwin, linux, windows]
function mapOS(os) {
const mappings = {
win32: 'windows'
}
return mappings[os] || os
}

async function run() {
try {
const clientID = core.getInput('client-id')
const clientSecret = core.getInput('client-secret')
const expectedArchiveChecksum = core.getInput('archive-checksum')
const githubToken = core.getInput('github-token')
const version = core.getInput('version')

const platform = mapOS(os.platform())
const arch = mapArch(os.arch())

// windows binaries are zipped
const archiveSuffix = platform === 'windows' ? '.zip' : '.tar.gz'

const client = await octokit(githubToken || process.env.GITHUB_TOKEN)

// if we don't have specific version, get latest release
let releaseToDownload
if (version === 'latest' || !version) {
releaseToDownload = (await client.rest.repos.getLatestRelease({ owner, repo })).data
} else {
releaseToDownload = (await client.rest.repos.getReleaseByTag({ owner, repo, tag: version })).data
}
const tag = releaseToDownload.tag_name
core.setOutput('version', tag)

// i.e. signore_0.1.2_darwin_x86_64.tar.gz
const expectedAssetName = `${repo}_${tag.replace('v', '')}_${platform}_${arch}${archiveSuffix}`
const assetToDownload = releaseToDownload.assets.find(asset => asset.name === expectedAssetName)
if (assetToDownload === undefined) {
throw new Error(`Unable to find asset matching ${expectedAssetName} in the ${tag} release`)
}

const url = assetToDownload.url
const auth = 'token ' + (githubToken || process.env.GITHUB_TOKEN)

core.debug(`Downloading ${repo} release from ${url}`)
const downloadedArchive = await tc.downloadTool(url, undefined, auth, { accept: 'application/octet-stream' })

if (expectedArchiveChecksum !== '') {
const downloadedArchiveChecksum = crypto.createHash('sha256').update(await fs.readFile(downloadedArchive)).digest('hex')
if (expectedArchiveChecksum !== downloadedArchiveChecksum) {
throw new Error(`Checksum mismatch: ${downloadedArchiveChecksum} does not match expected checksum ${expectedArchiveChecksum}`)
}
}

core.debug(`Extracting ${repo} release`)
let pathToCLI
if (archiveSuffix === '.tar.gz') {
pathToCLI = await tc.extractTar(downloadedArchive)
} else {
pathToCLI = await tc.extractZip(downloadedArchive)
}
core.debug(`${repo} CLI path is ${pathToCLI}.`)

if (!downloadedArchive || !pathToCLI) {
throw new Error(`Unable to download ${repo} from ${url}`)
}

core.addPath(pathToCLI)

if (clientID || clientSecret) {
core.debug('writing signore config file')
let configContent = ''
configContent += clientID ? `clientID: ${clientID}\n` : ''
configContent += clientSecret ? `clientSecret: ${clientSecret}\n` : ''
fs.writeFile(path.join(os.homedir(), '.signore', 'config.yaml'), configContent)
}

core.debug('success: signore has been set up!')
} catch (error) {
core.setFailed(error.message)
}
}

module.exports = run
177 changes: 177 additions & 0 deletions action.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const fs = require('fs')
const nock = require('nock')
const path = require('path')
const os = require('os')

const core = require('@actions/core')

const mockRelease = {
assets: [
{
id: 1,
name: 'signore_0.1.3_darwin_x86_64.tar.gz',
url: 'https://api.github.com/repos/hashicorp/signore/releases/assets/1'
},
{
id: 2,
name: 'signore_0.1.3_linux_x86_64.tar.gz',
url: 'https://api.github.com/repos/hashicorp/signore/releases/assets/2'
},
{
id: 3,
name: 'signore_0.1.3_windows_x86_64.zip',
url: 'https://api.github.com/repos/hashicorp/signore/releases/assets/3'
}
],
id: '1',
name: 'v0.1.3',
tag_name: 'v0.1.3'
}

beforeAll(() => {
nock.disableNetConnect()
})

beforeEach(() => {
process.env['INPUT_GITHUB-TOKEN'] = 'testtoken'
process.env.INPUT_VERSION = 'latest'
process.env['INPUT_VERSION-CHECKSUM'] = '5663389ef1a8ec48af6ca622e66bf0f54ba8f22c127f14cb8a3f429e40868582'

const spyOsArch = jest.spyOn(os, 'arch')
spyOsArch.mockReturnValue('x64')
const spyOsPlatform = jest.spyOn(os, 'platform')
spyOsPlatform.mockReturnValue('win32')
})

describe('action', () => {
test('installs latest version', (done) => {
const scope = nock('https://api.github.com')
.get('/repos/hashicorp/signore/releases/latest')
.reply(200, mockRelease)
.get('/repos/hashicorp/signore/releases/assets/3')
.replyWithFile(200, path.resolve(__dirname, 'test.zip'), { 'content-type': 'application/octet-stream' })
const spyCoreAddPath = jest.spyOn(core, 'addPath')
const spyCoreSetOutput = jest.spyOn(core, 'setOutput')

fs.mkdtemp(path.join(os.tmpdir(), 'setup-signore-'), async (err, directory) => {
if (err) throw err

process.env.RUNNER_TEMP = directory

const spyOsHomedir = jest.spyOn(os, 'homedir')
spyOsHomedir.mockReturnValue(directory)

const action = require('./action')
await expect(await action()).resolves
expect(scope.isDone()).toBeTruthy()
expect(spyCoreAddPath).toHaveBeenCalled()
expect(spyCoreSetOutput).toHaveBeenCalledWith('version', 'v0.1.3')
done()
})
})

test('installs configured version', (done) => {
const scope = nock('https://api.github.com')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(200, mockRelease)
.get('/repos/hashicorp/signore/releases/assets/3')
.replyWithFile(200, path.resolve(__dirname, 'test.zip'), { 'content-type': 'application/octet-stream' })
const spyCoreAddPath = jest.spyOn(core, 'addPath')
const spyCoreSetOutput = jest.spyOn(core, 'setOutput')

fs.mkdtemp(path.join(os.tmpdir(), 'setup-signore-'), async (err, directory) => {
if (err) throw err

process.env.INPUT_VERSION = 'v0.1.3'
process.env.RUNNER_TEMP = directory

const spyOsHomedir = jest.spyOn(os, 'homedir')
spyOsHomedir.mockReturnValue(directory)

const action = require('./action')
await expect(await action()).resolves
expect(scope.isDone()).toBeTruthy()
expect(spyCoreAddPath).toHaveBeenCalled()
expect(spyCoreSetOutput).toHaveBeenCalledWith('version', 'v0.1.3')
done()
})
})

test('retries transient errors', (done) => {
const scope = nock('https://api.github.com')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(500, 'expected transient error')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(200, mockRelease)
.get('/repos/hashicorp/signore/releases/assets/3')
.replyWithFile(200, path.resolve(__dirname, 'test.zip'), { 'content-type': 'application/octet-stream' })

fs.mkdtemp(path.join(os.tmpdir(), 'setup-signore-'), async (err, directory) => {
if (err) throw err

process.env.INPUT_VERSION = 'v0.1.3'
process.env.RUNNER_TEMP = directory

const spyOsHomedir = jest.spyOn(os, 'homedir')
spyOsHomedir.mockReturnValue(directory)

const action = require('./action')
await expect(await action()).resolves
expect(scope.isDone()).toBeTruthy()
done()
})
})

test('retries abuse limit errors', (done) => {
const scope = nock('https://api.github.com')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(403, {
message: 'You have triggered an abuse detection mechanism and have been temporarily blocked from content creation. Please retry your request again later.',
documentation_url: 'https://docs.github.com/rest/overview/resources-in-the-rest-api#abuse-rate-limits'
})
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(200, mockRelease)
.get('/repos/hashicorp/signore/releases/assets/3')
.replyWithFile(200, path.resolve(__dirname, 'test.zip'), { 'content-type': 'application/octet-stream' })

fs.mkdtemp(path.join(os.tmpdir(), 'setup-signore-'), async (err, directory) => {
if (err) throw err

process.env.INPUT_VERSION = 'v0.1.3'
process.env.RUNNER_TEMP = directory

const spyOsHomedir = jest.spyOn(os, 'homedir')
spyOsHomedir.mockReturnValue(directory)

const action = require('./action')
await expect(await action()).resolves
expect(scope.isDone()).toBeTruthy()
done()
})
})

test('retries rate limit errors', (done) => {
const scope = nock('https://api.github.com')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(429, 'expected rate limit error')
.get('/repos/hashicorp/signore/releases/tags/v0.1.3')
.reply(200, mockRelease)
.get('/repos/hashicorp/signore/releases/assets/3')
.replyWithFile(200, path.resolve(__dirname, 'test.zip'), { 'content-type': 'application/octet-stream' })

fs.mkdtemp(path.join(os.tmpdir(), 'setup-signore-'), async (err, directory) => {
if (err) throw err

process.env.INPUT_VERSION = 'v0.1.3'
process.env.RUNNER_TEMP = directory

const spyOsHomedir = jest.spyOn(os, 'homedir')
spyOsHomedir.mockReturnValue(directory)

const action = require('./action')
await expect(await action()).resolves
expect(scope.isDone()).toBeTruthy()
done()
})
})
})
Loading

0 comments on commit 55efb3b

Please sign in to comment.