diff --git a/.github.template/workflows/check-updates.yml.template b/.github.template/workflows/check-updates.yml.template index 709a087..fe50b44 100644 --- a/.github.template/workflows/check-updates.yml.template +++ b/.github.template/workflows/check-updates.yml.template @@ -3,7 +3,7 @@ name: Check updates on: schedule: - - cron: '0 0 1,15 * *' + - cron: '0 0 1 * *' env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true @@ -38,7 +38,7 @@ jobs: shell: bash - name: Create PR if: ${{ steps.git-diff.outputs.count > 0 }} - uses: peter-evans/create-pull-request@v3.6.0 + uses: peter-evans/create-pull-request@v3.8.2 env: HUSKY: 0 with: diff --git a/.github.template/workflows/security-tests.yml.template b/.github.template/workflows/security-tests.yml similarity index 82% rename from .github.template/workflows/security-tests.yml.template rename to .github.template/workflows/security-tests.yml index db0bd29..dcb08c7 100644 --- a/.github.template/workflows/security-tests.yml.template +++ b/.github.template/workflows/security-tests.yml @@ -18,10 +18,9 @@ jobs: - uses: actions/checkout@v2.3.4 with: ref: ${{ github.ref }} - - name: Snyk authenticate - run: ${PM_CLI} run security:auth ${{ secrets.SNYK_API_TOKEN }} - - name: Snyk test - run: ${PM_CLI} run security:test + - uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_API_TOKEN }} codeql_tests: name: CodeQL runs-on: ubuntu-latest diff --git a/.github.template/workflows/unit-tests.yml.template b/.github.template/workflows/unit-tests.yml.template index 385c5bf..ad568a5 100644 --- a/.github.template/workflows/unit-tests.yml.template +++ b/.github.template/workflows/unit-tests.yml.template @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v2.3.4 - name: Cache node_modules id: cache - uses: actions/cache@v2.1.3 + uses: actions/cache@v2.1.4 with: path: '**/node_modules' key: ${{ runner.os }}-modules-${{ hashFiles('**/${PM_LOCK_FILE}') }} diff --git a/.gitignore b/.gitignore index 4086c1a..026db7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store .vscode/* !.vscode/settings.json .env diff --git a/Makefile b/Makefile index 4d02721..4bdb0d1 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,18 @@ default: read -p "📊 CodeClimate test coverage badge (leave empty if you do not have it): " cc_test_coverage_badge; \ if [[ -z "$$cc_test_coverage_badge" ]] ; then cc_test_coverage_badge=''; else cc_test_coverage_badge="$$cc_test_coverage_badge "; fi; \ export CC_TESTS_COVERAGE_BADGE=$$cc_test_coverage_badge; \ + read -p "🔧 Name of a CLI that you want to setup using this GitHub Action, e.g. wren: " cli_name; \ + [ -z "$$cli_name" ] && echo '❌ CLI name cannot be empty.' && exit 1; \ + export CLI_NAME=$$cli_name; \ + read -p "🗄 File extension that this GitHub Action will download to install, e.g. zip (zip): " cli_extension; \ + [ -z "$$cli_extension" ] && cli_extension='zip'; \ + export CLI_EXTENSION=$$cli_extension; \ + read -p "🌐 First part of URL that will be used to download CLI tool, e.g. if url to download CLI tool looks like https://github.com/wren-lang/wren-cli/releases/download/0.3.0/wren_cli-linux-0.3.0.zip then you should enter https://github.com/wren-lang/wren-cli/releases/download: " cli_url; \ + [ -z "$$cli_url" ] && echo '❌ CLI URL cannot be empty.' && exit 1; \ + export CLI_URL=$$cli_url; \ + read -p "🔢 Latest available version of $$cli_name tool: " latest_version; \ + [ -z "$$latest_version" ] && echo '❌ Version cannot be empty.' && exit 1; \ + export LATEST_VERSION=$$latest_version; \ envsubst < README.md.template > README.md; \ rm -f README.md.template; \ envsubst < action.yml.template > action.yml; \ @@ -42,14 +54,14 @@ default: rm -f .github.template/ISSUE_TEMPLATE/feature_request.md.template; \ envsubst < .github.template/workflows/check-updates.yml.template > .github.template/workflows/check-updates.yml; \ rm -f .github.template/workflows/check-updates.yml.template; \ - envsubst < .github.template/workflows/security-tests.yml.template > .github.template/workflows/security-tests.yml; \ - rm -f .github.template/workflows/security-tests.yml.template; \ envsubst < .github.template/workflows/unit-tests.yml.template > .github.template/workflows/unit-tests.yml; \ rm -f .github.template/workflows/unit-tests.yml.template; \ envsubst < .husky.template/pre-commit.template > .husky.template/pre-commit; \ rm -f .husky.template/pre-commit.template; \ envsubst < .husky.template/pre-push.template > .husky.template/pre-push; \ - rm -f .husky.template/pre-push.template + rm -f .husky.template/pre-push.template; \ + envsubst < src/consts.ts.template > src/consts.ts; \ + rm -f src/consts.ts.template @rm -rf .github @mv .github.template .github @mv .husky.template .husky diff --git a/action.yml.template b/action.yml.template index 0776de2..a41a008 100644 --- a/action.yml.template +++ b/action.yml.template @@ -5,6 +5,11 @@ description: '${REPO_TITLE} GitHub Action' branding: icon: terminal color: gray-dark +inputs: + version: + description: '${CLI_NAME} version.' + required: false + default: ${LATEST_VERSION} runs: using: 'node12' main: 'dist/index.js' diff --git a/package.json.template b/package.json.template index 5797998..3464418 100644 --- a/package.json.template +++ b/package.json.template @@ -33,24 +33,24 @@ "winston": "3.3.3" }, "devDependencies": { - "@types/chai": "4.2.14", + "@types/chai": "4.2.15", "@types/jest": "26.0.20", - "@types/node": "14.14.21", - "@typescript-eslint/eslint-plugin": "4.13.0", - "@typescript-eslint/parser": "4.13.0", + "@types/node": "14.14.31", + "@typescript-eslint/eslint-plugin": "4.15.2", + "@typescript-eslint/parser": "4.15.2", "@vercel/ncc": "0.27.0", - "chai": "4.2.0", - "eslint": "7.18.0", + "chai": "4.3.0", + "eslint": "7.20.0", "eslint-config-google": "0.14.0", "git-branch-is": "4.0.0", - "husky": "5.0.6", + "husky": "5.1.1", "jest": "26.6.3", "jest-circus": "26.6.3", "markdownlint-cli": "0.26.0", "mocha-param": "2.0.1", - "snyk": "1.437.4", - "ts-jest": "26.4.4", - "typescript": "4.1.3" + "snyk": "1.461.0", + "ts-jest": "26.5.2", + "typescript": "4.2.2" }, "snyk": true } diff --git a/src/Cache.ts b/src/Cache.ts index 011620d..1ecfa05 100644 --- a/src/Cache.ts +++ b/src/Cache.ts @@ -1,6 +1,6 @@ import { addPath } from '@actions/core' import { cacheDir } from '@actions/tool-cache' -import fs from 'fs' +import { chmodSync } from 'fs' import path from 'path' import { Logger } from 'winston' import CliExeNameProvider from './CliExeNameProvider' @@ -13,14 +13,14 @@ export default class Cache implements ICache { constructor( version: string, - provider: ICliExeNameProvider = new CliExeNameProvider()) { + provider: ICliExeNameProvider = new CliExeNameProvider(version)) { this.version = version this.provider = provider this.log = LoggerFactory.create('Cache') } async cache(execFilePath: string): Promise { - fs.chmodSync(execFilePath, '777') + chmodSync(execFilePath, '777') this.log.info( `Access permissions of ${execFilePath} file was changed to 777.`) const folderPath: string = path.dirname(execFilePath) diff --git a/src/Downloader.ts b/src/Downloader.ts index fc0bce5..c566c0d 100644 --- a/src/Downloader.ts +++ b/src/Downloader.ts @@ -1,7 +1,7 @@ import { downloadTool } from '@actions/tool-cache' import fs from 'fs' import { Logger } from 'winston' -import { CLI_NAME } from './consts' +import { CLI_EXTENSION, CLI_NAME } from './consts' import LoggerFactory from './LoggerFactory' export default class Downloader implements IDownloader { @@ -11,7 +11,7 @@ export default class Downloader implements IDownloader { this.log.info(`Downloading ${CLI_NAME} from ${url}`) const zipPathOld: string = await downloadTool(url) this.log.info(`Downloaded to ${zipPathOld}`) - const zipPathNew: string = zipPathOld + '.zip' + const zipPathNew: string = zipPathOld + '.' + CLI_EXTENSION fs.renameSync(zipPathOld, zipPathNew) this.log.info(`Renamed to ${zipPathNew}`) return zipPathNew diff --git a/src/UrlProvider.ts b/src/UrlProvider.ts index 5f9d6c4..dcf8dcb 100644 --- a/src/UrlProvider.ts +++ b/src/UrlProvider.ts @@ -1,4 +1,5 @@ import CliFileNameBuilder from './CliFileNameBuilder' +import { CLI_EXTENSION, CLI_URL } from './consts' export default class UrlProvider implements IUrlProvider { private builder: ICliFileNameBuilder @@ -12,6 +13,6 @@ export default class UrlProvider implements IUrlProvider { } getUrl(): string { - return '{PROJECT_URL}' + `${this.version}/${this.builder.build()}.zip` + return `${CLI_URL}/${this.version}/${this.builder.build()}.${CLI_EXTENSION}` } } diff --git a/src/__tests__/Cache.spec.ts b/src/__tests__/Cache.spec.ts index 02d66a4..beb44d2 100644 --- a/src/__tests__/Cache.spec.ts +++ b/src/__tests__/Cache.spec.ts @@ -1,41 +1,39 @@ -import * as core from '@actions/core' -import * as tc from '@actions/tool-cache' -import fs from 'fs' +import { addPath } from '@actions/core' +import { cacheDir } from '@actions/tool-cache' +import { chmodSync } from 'fs' import path from 'path' -import { restore, SinonStub, stub } from 'sinon' import Cache from '../Cache' -describe('Cache', () => { - let addPathStub: SinonStub<[inputPath: string], void> - let cacheDirStub: SinonStub<[sourceDir: string, - tool: string, version: string, arch?: string], Promise> - let chmodSyncStub: SinonStub<[path: fs.PathLike, mode: fs.Mode], void> - - beforeEach(() => { - addPathStub = stub(core, 'addPath') - cacheDirStub = stub(tc, 'cacheDir') - chmodSyncStub = stub(fs, 'chmodSync') - }) +jest.mock('@actions/core', () => ({ addPath: jest.fn() })); +jest.mock('@actions/tool-cache', () => ({ cacheDir: jest.fn() })); +jest.mock('fs', () => ({ chmodSync: jest.fn() })); +describe('Cache', () => { it('should cache successfully', async () => { const version: string = 'ey1r6c00' const exeFileName: string = 'O7DF0gox' const getExeFileNameMock: jest.Mock = jest.fn(() => exeFileName) const folderPath: string = '1ef84ehe' const execFilePath: string = path.join(folderPath, 'm8x9p1sw') - const cachedPath: string = '1r4wn1iw' - cacheDirStub.returns(Promise.resolve(cachedPath)) + const cachedPath: string = '1r4wn1iw'; + (cacheDir as jest.Mock) + .mockImplementation(() => Promise.resolve(cachedPath)) const cache: Cache = new Cache(version, { getExeFileName: getExeFileNameMock }) await cache.cache(execFilePath) expect(getExeFileNameMock.mock.calls.length).toBe(1) - expect(chmodSyncStub.withArgs(execFilePath, '777').callCount).toBe(1) - expect(cacheDirStub.withArgs(folderPath, exeFileName, version).callCount) - .toBe(1) - expect(addPathStub.withArgs(cachedPath).callCount).toBe(1) + expect((chmodSync as jest.Mock).mock.calls.length).toBe(1) + expect(chmodSync).toHaveBeenCalledWith(execFilePath, '777') + expect((addPath as jest.Mock).mock.calls.length).toBe(1) + expect((cacheDir as jest.Mock).mock.calls.length).toBe(1) + expect(cacheDir).toHaveBeenCalledWith(folderPath, exeFileName, version) }) - afterEach(() => restore()) + afterEach(() => { + (addPath as jest.Mock).mockClear(), + (cacheDir as jest.Mock).mockClear(), + (chmodSync as jest.Mock).mockClear() + }) }) diff --git a/src/__tests__/CliExeNameProvider.spec.ts b/src/__tests__/CliExeNameProvider.spec.ts index f171723..2cde813 100644 --- a/src/__tests__/CliExeNameProvider.spec.ts +++ b/src/__tests__/CliExeNameProvider.spec.ts @@ -1,17 +1,16 @@ import itParam from 'mocha-param' -import os from 'os' -import { restore, SinonStub, stub } from 'sinon' +import { type } from 'os' import CliExeNameProvider from '../CliExeNameProvider' import { CLI_NAME } from '../consts' +jest.mock('os', () => ({ type: jest.fn() })) + interface IFixture { os: string execFileName: string } describe('CliExeNameProvider', () => { - let osTypeStub: SinonStub<[], string> - const expectedVersion: string = 'ey1r6c00' const items: IFixture[] = [{ os: 'Windows_NT', @@ -24,16 +23,12 @@ describe('CliExeNameProvider', () => { execFileName: CLI_NAME }] - beforeEach(() => { - osTypeStub = stub(os, 'type') - }) - itParam('should return exe name successfully', items, (item: IFixture) => { - osTypeStub.returns(item.os) + (type as jest.Mock).mockImplementation(() => item.os) const provider: CliExeNameProvider = new CliExeNameProvider(expectedVersion) const actual: string = provider.getExeFileName() expect(actual).toBe(item.execFileName) }) - afterEach(() => restore()) + afterEach(() => (type as jest.Mock).mockClear()) }) diff --git a/src/__tests__/CliFileNameBuilder.spec.ts b/src/__tests__/CliFileNameBuilder.spec.ts index e6b89bf..8b0a08d 100644 --- a/src/__tests__/CliFileNameBuilder.spec.ts +++ b/src/__tests__/CliFileNameBuilder.spec.ts @@ -1,9 +1,10 @@ import itParam from 'mocha-param' -import os from 'os' -import { restore, SinonStub, stub } from 'sinon' +import { type } from 'os' import CliFileNameBuilder from '../CliFileNameBuilder' import { CLI_NAME } from '../consts' +jest.mock('os', () => ({ type: jest.fn() })) + interface IFixture { os1: string os2: string @@ -22,18 +23,12 @@ describe('CliFileNameBuilder', () => { os2: 'linux' }] - let osTypeStub: SinonStub<[], string> - - beforeEach(() => { - osTypeStub = stub(os, 'type') - }) - itParam('should build successfully (${value.os1})', items, (item: IFixture) => { - osTypeStub.returns(item.os1) + (type as jest.Mock).mockImplementation(() => item.os1) const b: CliFileNameBuilder = new CliFileNameBuilder(expectedVersion) expect(b.build()).toBe(`${CLI_NAME}-${item.os2}-${expectedVersion}`) }) - afterEach(() => restore()) + afterEach(() => (type as jest.Mock).mockClear()) }) diff --git a/src/__tests__/Downloader.spec.ts b/src/__tests__/Downloader.spec.ts index 6023d8a..8cd573b 100644 --- a/src/__tests__/Downloader.spec.ts +++ b/src/__tests__/Downloader.spec.ts @@ -1,28 +1,27 @@ -import * as tc from '@actions/tool-cache' -import fs from 'fs' -import { restore, SinonStub, stub } from 'sinon' +import { downloadTool } from '@actions/tool-cache' +import { renameSync } from 'fs' +import { CLI_EXTENSION } from '../consts' import Downloader from '../Downloader' -describe('Downloader', () => { - let fsRenameSyncStub: - SinonStub<[oldPath: fs.PathLike, newPath: fs.PathLike], void> - let downloadToolStub: SinonStub - - beforeEach(() => { - fsRenameSyncStub = stub(fs, 'renameSync') - downloadToolStub = stub(tc, 'downloadTool') - }) +jest.mock('@actions/tool-cache', () => ({ downloadTool: jest.fn() })) +jest.mock('fs', () => ({ renameSync: jest.fn() })) +describe('Downloader', () => { it('should download successfully', async () => { const zipPathOld: string = 'yw86z9qw' - const zipPathNew: string = zipPathOld + '.zip' - const url: string = '9r1y2ryp' - downloadToolStub.returns(Promise.resolve(zipPathOld)) + const zipPathNew: string = zipPathOld + '.' + CLI_EXTENSION + const url: string = '9r1y2ryp'; + (downloadTool as jest.Mock) + .mockImplementation(() => Promise.resolve(zipPathOld)) const d: Downloader = new Downloader() const actual: string = await d.download(url) - expect(fsRenameSyncStub.withArgs(zipPathOld, zipPathNew).callCount).toBe(1) + expect((renameSync as jest.Mock).mock.calls.length).toBe(1) + expect(renameSync).toHaveBeenCalledWith(zipPathOld, zipPathNew) expect(actual).toBe(zipPathNew) }) - afterEach(() => restore()) + afterEach(() => { + (downloadTool as jest.Mock).mockClear(); + (renameSync as jest.Mock).mockClear() + }) }) diff --git a/src/__tests__/ExecutableFileFinder.spec.ts b/src/__tests__/ExecutableFileFinder.spec.ts index c2ee082..2ddf64a 100644 --- a/src/__tests__/ExecutableFileFinder.spec.ts +++ b/src/__tests__/ExecutableFileFinder.spec.ts @@ -1,10 +1,11 @@ -import glob, { IOptions } from 'glob' +import { sync } from 'glob' import itParam from 'mocha-param' import path from 'path' -import { restore, SinonStub, stub } from 'sinon' import { CLI_NAME } from '../consts' import ExecutableFileFinder from '../ExecutableFileFinder' +jest.mock('glob', () => ({ sync: jest.fn() })) + interface IFixture { message: string suffix: string @@ -19,29 +20,25 @@ describe('ExecutableFileFinder', () => { message: 'Execution file has not been found', suffix: 'u4h0t03e' }] - let globSyncStub: SinonStub<[pattern: string, options?: IOptions], string[]> - - beforeEach(() => { - globSyncStub = stub(glob, 'sync') - }) it('should find successfully', () => { const folderPath: string = '4se2ov6f' - const files: string[] = [`1clx8w43${SUFFIX}`, '1clx8w43'] - globSyncStub.returns(files) + const files: string[] = [`1clx8w43${SUFFIX}`, '1clx8w43']; + (sync as jest.Mock).mockImplementation(() => files) const finder: ExecutableFileFinder = new ExecutableFileFinder('1uu02vbj', { getExeFileName: (): string => SUFFIX }) const actual: string = finder.find(folderPath) - expect(globSyncStub.withArgs( - `${folderPath}${path.sep}**${path.sep}${CLI_NAME}*`).callCount).toBe(1) + expect((sync as jest.Mock).mock.calls.length).toBe(1) + expect(sync).toHaveBeenCalledWith( + `${folderPath}${path.sep}**${path.sep}${CLI_NAME}*`) expect(actual).toBe(files[0]) }) itParam('should throw error (${value.message})', items, (item: IFixture) => { const folderPath: string = '4se2ov6f' - const files: string[] = [`1clx8w43${SUFFIX}`, `gt11c1zr${SUFFIX}`] - globSyncStub.returns(files) + const files: string[] = [`1clx8w43${SUFFIX}`, `gt11c1zr${SUFFIX}`]; + (sync as jest.Mock).mockImplementation(() => files) const finder: ExecutableFileFinder = new ExecutableFileFinder('1uu02vbj', { getExeFileName: (): string => item.suffix }) @@ -49,12 +46,13 @@ describe('ExecutableFileFinder', () => { finder.find(folderPath) } catch (e) { expect((e).message).toContain(item.message) - expect(globSyncStub.withArgs( - `${folderPath}${path.sep}**${path.sep}${CLI_NAME}*`).callCount).toBe(1) + expect((sync as jest.Mock).mock.calls.length).toBe(1) + expect(sync).toHaveBeenCalledWith( + `${folderPath}${path.sep}**${path.sep}${CLI_NAME}*`) return } fail() }) - afterEach(() => restore()) + afterEach(() => (sync as jest.Mock).mockClear()) }) diff --git a/src/__tests__/UrlProvider.spec.ts b/src/__tests__/UrlProvider.spec.ts index 875f4b7..b706146 100644 --- a/src/__tests__/UrlProvider.spec.ts +++ b/src/__tests__/UrlProvider.spec.ts @@ -1,3 +1,4 @@ +import { CLI_EXTENSION, CLI_URL } from '../consts' import UrlProvider from '../UrlProvider' describe('UrlProvider', () => { @@ -8,6 +9,6 @@ describe('UrlProvider', () => { build: (): string => fileName }) const actual: string = provider.getUrl() - expect(actual).toBe('{PROJECT_URL}' + `/${version}/${fileName}.zip`) + expect(actual).toBe(`${CLI_URL}/${version}/${fileName}.${CLI_EXTENSION}`) }) }) diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index fe73f09..005e38c 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { error, getInput, InputOptions } from '@actions/core' +import { getInput, InputOptions, setFailed } from '@actions/core' import { assert } from 'chai' import { run } from '../index' @@ -13,11 +13,11 @@ class InstallerMock { } describe('Main runner', () => { - let errorMocked + let setFailedMocked let getInputMocked beforeEach(() => { - errorMocked = jest.fn((m: string) => assert.isNotNull(m)) + setFailedMocked = jest.fn((m: string) => assert.isNotNull(m)) getInputMocked = jest.fn((name: string, options?: InputOptions): string => { assert.isUndefined(options) assert.equal(name, 'version') @@ -32,7 +32,7 @@ describe('Main runner', () => { assert.equal(version, TEST_VERSION) return installerMock } - }, getInputMocked as typeof getInput, errorMocked as typeof error) + }, getInputMocked as typeof getInput, setFailedMocked as typeof setFailed) expect(installerMock.calls).toBe(1) }) @@ -47,13 +47,13 @@ describe('Main runner', () => { } } } - }, getInputMocked as typeof getInput, errorMocked as typeof error) - expect(errorMocked.mock.calls.length).toBe(1) - expect(errorMocked.mock.calls[0][0]).toBe(expectedMessage) + }, getInputMocked as typeof getInput, setFailedMocked as typeof setFailed) + expect(setFailedMocked.mock.calls.length).toBe(1) + expect(setFailedMocked.mock.calls[0][0]).toBe(expectedMessage) }) afterEach(() => { - errorMocked.mockReset(); + setFailedMocked.mockReset(); getInputMocked.mockReset(); }) }) diff --git a/src/consts.ts b/src/consts.ts deleted file mode 100644 index 90ae6b7..0000000 --- a/src/consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const CLI_NAME: string = '{PROJECT_CLI}' diff --git a/src/consts.ts.template b/src/consts.ts.template new file mode 100644 index 0000000..828da25 --- /dev/null +++ b/src/consts.ts.template @@ -0,0 +1,3 @@ +export const CLI_EXTENSION: string = '${CLI_EXTENSION}' +export const CLI_NAME: string = '${CLI_NAME}' +export const CLI_URL: string = '${CLI_URL}' diff --git a/src/index.ts b/src/index.ts index fd279ad..33e0e59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { error, getInput } from '@actions/core'; +import { getInput, setFailed } from '@actions/core'; import Installer from './Installer'; export interface IInstallerFactory { @@ -9,12 +9,12 @@ export interface IInstallerFactory { export const run = async ( factory: IInstallerFactory = { get: (v: string) => new Installer(v) }, gi: typeof getInput = getInput, - err: typeof error = error) => { + sf: typeof setFailed = setFailed) => { const installer = factory.get(gi('version')); try { await installer.install(); } catch (e) { - err((e).message) + sf((e).message) } }