From b7d83b40950d5a8944106e0b29750090dc13fad8 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 10:54:39 -0500 Subject: [PATCH] Provide better errors for unsupported event types (#68) * Validate event type during restore * PR Feedback * Format * Linting --- __tests__/restore.test.ts | 104 +++++++++++++++++++++++++++++++++++++- src/constants.ts | 6 +++ src/restore.ts | 12 ++++- src/utils/actionUtils.ts | 15 +++++- 4 files changed, 134 insertions(+), 3 deletions(-) diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 466b26c..191f804 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -3,7 +3,7 @@ import * as exec from "@actions/exec"; import * as io from "@actions/io"; import * as path from "path"; import * as cacheHttpClient from "../src/cacheHttpClient"; -import { Inputs } from "../src/constants"; +import { Events, Inputs } from "../src/constants"; import { ArtifactCacheEntry } from "../src/contracts"; import run from "../src/restore"; import * as actionUtils from "../src/utils/actionUtils"; @@ -26,12 +26,38 @@ beforeAll(() => { } ); + jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { + const actualUtils = jest.requireActual("../src/utils/actionUtils"); + return actualUtils.isValidEvent(); + }); + + jest.spyOn(actionUtils, "getSupportedEvents").mockImplementation(() => { + const actualUtils = jest.requireActual("../src/utils/actionUtils"); + return actualUtils.getSupportedEvents(); + }); + jest.spyOn(io, "which").mockImplementation(tool => { return Promise.resolve(tool); }); }); + +beforeEach(() => { + process.env[Events.Key] = Events.Push; +}); + afterEach(() => { testUtils.clearInputs(); + delete process.env[Events.Key]; +}); + +test("restore with invalid event", async () => { + const failedMock = jest.spyOn(core, "setFailed"); + const invalidEvent = "commit_comment"; + process.env[Events.Key] = invalidEvent; + await run(); + expect(failedMock).toHaveBeenCalledWith( + `Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.` + ); }); test("restore with no path should fail", async () => { @@ -255,6 +281,82 @@ test("restore with cache found", async () => { expect(failedMock).toHaveBeenCalledTimes(0); }); +test("restore with a pull request event and cache found", async () => { + const key = "node-test"; + const cachePath = path.resolve("node_modules"); + testUtils.setInputs({ + path: "node_modules", + key + }); + + process.env[Events.Key] = Events.PullRequest; + + const infoMock = jest.spyOn(core, "info"); + const warningMock = jest.spyOn(core, "warning"); + const failedMock = jest.spyOn(core, "setFailed"); + const stateMock = jest.spyOn(core, "saveState"); + + const cacheEntry: ArtifactCacheEntry = { + cacheKey: key, + scope: "refs/heads/master", + archiveLocation: "https://www.example.com/download" + }; + const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); + getCacheMock.mockImplementation(() => { + return Promise.resolve(cacheEntry); + }); + const tempPath = "/foo/bar"; + + const createTempDirectoryMock = jest.spyOn( + actionUtils, + "createTempDirectory" + ); + createTempDirectoryMock.mockImplementation(() => { + return Promise.resolve(tempPath); + }); + + const archivePath = path.join(tempPath, "cache.tgz"); + const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState"); + const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache"); + + const fileSize = 142; + const getArchiveFileSizeMock = jest + .spyOn(actionUtils, "getArchiveFileSize") + .mockReturnValue(fileSize); + + const mkdirMock = jest.spyOn(io, "mkdirP"); + const execMock = jest.spyOn(exec, "exec"); + const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); + + await run(); + + expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); + expect(getCacheMock).toHaveBeenCalledWith([key]); + expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); + expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); + expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); + expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); + expect(mkdirMock).toHaveBeenCalledWith(cachePath); + + const IS_WINDOWS = process.platform === "win32"; + const tarArchivePath = IS_WINDOWS + ? archivePath.replace(/\\/g, "/") + : archivePath; + const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; + const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; + args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith(`"tar"`, args); + + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); + + expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); + expect(warningMock).toHaveBeenCalledTimes(0); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + test("restore with cache found for restore key", async () => { const key = "node-test"; const restoreKey = "node-"; diff --git a/src/constants.ts b/src/constants.ts index 100b878..5f26e8c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,3 +12,9 @@ export enum State { CacheKey = "CACHE_KEY", CacheResult = "CACHE_RESULT" } + +export enum Events { + Key = "GITHUB_EVENT_NAME", + Push = "push", + PullRequest = "pull_request" +} diff --git a/src/restore.ts b/src/restore.ts index f0b22b4..0af62d4 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -3,12 +3,22 @@ import { exec } from "@actions/exec"; import * as io from "@actions/io"; import * as path from "path"; import * as cacheHttpClient from "./cacheHttpClient"; -import { Inputs, State } from "./constants"; +import { Events, Inputs, State } from "./constants"; import * as utils from "./utils/actionUtils"; async function run(): Promise { try { // Validate inputs, this can cause task failure + if (!utils.isValidEvent()) { + core.setFailed( + `Event Validation Error: The event type ${ + process.env[Events.Key] + } is not supported. Only ${utils + .getSupportedEvents() + .join(", ")} events are supported at this time.` + ); + } + let cachePath = utils.resolvePath( core.getInput(Inputs.Path, { required: true }) ); diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index 4732345..15b73fd 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -4,7 +4,8 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as uuidV4 from "uuid/v4"; -import { Outputs, State } from "../constants"; + +import { Events, Outputs, State } from "../constants"; import { ArtifactCacheEntry } from "../contracts"; // From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23 @@ -83,3 +84,15 @@ export function resolvePath(filePath: string): string { return path.resolve(filePath); } + +export function getSupportedEvents(): string[] { + return [Events.Push, Events.PullRequest]; +} + +// Currently the cache token is only authorized for push and pull_request events +// All other events will fail when reading and saving the cache +// See GitHub Context https://help.github.com/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context +export function isValidEvent(): boolean { + const githubEvent = process.env[Events.Key] || ""; + return getSupportedEvents().includes(githubEvent); +}