-
Notifications
You must be signed in to change notification settings - Fork 20
feat: allow task caching to github #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
0a9943a
fe6e618
0967e73
95021e2
943c89c
6051865
0f61b1c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test"; | ||
|
|
||
| // Mock external dependencies before importing the module | ||
| vi.mock("@actions/cache", () => ({ | ||
| restoreCache: vi.fn(), | ||
| })); | ||
| vi.mock("@actions/core", () => ({ | ||
| warning: vi.fn(), | ||
| info: vi.fn(), | ||
| debug: vi.fn(), | ||
| saveState: vi.fn(), | ||
| setOutput: vi.fn(), | ||
| })); | ||
| vi.mock("./utils.js", () => ({ | ||
| getConfiguredProjectDir: vi.fn(() => "/workspace"), | ||
| })); | ||
|
|
||
| import { restoreCache as restoreCacheAction } from "@actions/cache"; | ||
| import { warning, info, saveState, setOutput } from "@actions/core"; | ||
| import { restoreTaskCache } from "./task-cache-restore.js"; | ||
| import { State, Outputs } from "./types.js"; | ||
| import type { Inputs } from "./types.js"; | ||
|
|
||
| const mockedRestoreCacheAction = vi.mocked(restoreCacheAction); | ||
| const mockedWarning = vi.mocked(warning); | ||
| const mockedInfo = vi.mocked(info); | ||
| const mockedSaveState = vi.mocked(saveState); | ||
| const mockedSetOutput = vi.mocked(setOutput); | ||
|
|
||
| const baseInputs: Inputs = { | ||
| version: "latest", | ||
| nodeVersion: undefined, | ||
| nodeVersionFile: undefined, | ||
| workingDirectory: undefined, | ||
| runInstall: [], | ||
| sfw: false, | ||
| cache: false, | ||
| cacheDependencyPath: undefined, | ||
| taskCache: true, | ||
| registryUrl: undefined, | ||
| scope: undefined, | ||
| }; | ||
|
|
||
| describe("restoreTaskCache", () => { | ||
| const originalEnv = process.env; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| process.env = { ...originalEnv }; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv; | ||
| }); | ||
|
|
||
| it("restores task cache with correct key pattern", async () => { | ||
| process.env.RUNNER_OS = "Linux"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "1"; | ||
|
|
||
| mockedRestoreCacheAction.mockResolvedValue("vite-task-Linux-x64-12345-1"); | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedRestoreCacheAction).toHaveBeenCalledWith( | ||
| expect.arrayContaining([expect.stringContaining("node_modules")]), | ||
| "vite-task-Linux-x64-12345-1", | ||
| ["vite-task-Linux-x64-"], | ||
| ); | ||
|
|
||
| expect(mockedSaveState).toHaveBeenCalledWith( | ||
| State.TaskCachePrimaryKey, | ||
| "vite-task-Linux-x64-12345-1", | ||
| ); | ||
| expect(mockedSaveState).toHaveBeenCalledWith( | ||
| State.TaskCacheMatchedKey, | ||
| "vite-task-Linux-x64-12345-1", | ||
| ); | ||
| expect(mockedSetOutput).toHaveBeenCalledWith(Outputs.TaskCacheHit, true); | ||
| expect(mockedInfo).toHaveBeenCalledWith(expect.stringContaining("Task cache restored")); | ||
| }); | ||
|
|
||
| it("sets cache-hit to false when cache is not found", async () => { | ||
| process.env.RUNNER_OS = "Linux"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "1"; | ||
|
|
||
| mockedRestoreCacheAction.mockResolvedValue(undefined); | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedSetOutput).toHaveBeenCalledWith(Outputs.TaskCacheHit, false); | ||
| expect(mockedInfo).toHaveBeenCalledWith("Task cache not found"); | ||
| }); | ||
|
|
||
| it("warns and skips when GITHUB_RUN_ID is missing", async () => { | ||
| process.env.RUNNER_OS = "Linux"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "1"; | ||
| delete process.env.GITHUB_RUN_ID; | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedWarning).toHaveBeenCalledWith( | ||
| expect.stringContaining("GitHub run ID or attempt not found"), | ||
| ); | ||
| expect(mockedSetOutput).toHaveBeenCalledWith(Outputs.TaskCacheHit, false); | ||
| expect(mockedRestoreCacheAction).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("warns and skips when GITHUB_RUN_ATTEMPT is missing", async () => { | ||
| process.env.RUNNER_OS = "Linux"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
| delete process.env.GITHUB_RUN_ATTEMPT; | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedWarning).toHaveBeenCalledWith( | ||
| expect.stringContaining("GitHub run ID or attempt not found"), | ||
| ); | ||
| expect(mockedSetOutput).toHaveBeenCalledWith(Outputs.TaskCacheHit, false); | ||
| expect(mockedRestoreCacheAction).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("uses correct cache path relative to project directory", async () => { | ||
| process.env.RUNNER_OS = "Windows"; | ||
| process.env.GITHUB_RUN_ID = "67890"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "2"; | ||
|
|
||
| mockedRestoreCacheAction.mockResolvedValue(undefined); | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| const cachePathsCall = mockedSaveState.mock.calls.find(([key]) => key === State.TaskCachePaths); | ||
| expect(cachePathsCall).toBeDefined(); | ||
| if (cachePathsCall) { | ||
| const paths = JSON.parse(cachePathsCall[1] as string); | ||
| expect(paths[0]).toMatch(/node_modules/); | ||
| expect(paths[0]).toMatch(/task-cache/); | ||
| } | ||
| }); | ||
|
|
||
| it("handles different OS and architecture combinations", async () => { | ||
| process.env.RUNNER_OS = "macOS"; | ||
| process.env.GITHUB_RUN_ID = "11111"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "3"; | ||
|
|
||
| // Mock process.arch | ||
| const originalArch = process.arch; | ||
| Object.defineProperty(process, "arch", { | ||
| value: "arm64", | ||
| configurable: true, | ||
| }); | ||
|
|
||
| mockedRestoreCacheAction.mockResolvedValue(undefined); | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedSaveState).toHaveBeenCalledWith( | ||
| State.TaskCachePrimaryKey, | ||
| "vite-task-macOS-arm64-11111-3", | ||
| ); | ||
|
|
||
| // Restore original arch | ||
| Object.defineProperty(process, "arch", { | ||
| value: originalArch, | ||
| configurable: true, | ||
| }); | ||
| }); | ||
|
|
||
| it("saves cache paths as JSON string", async () => { | ||
| process.env.RUNNER_OS = "Linux"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
| process.env.GITHUB_RUN_ATTEMPT = "1"; | ||
|
|
||
| mockedRestoreCacheAction.mockResolvedValue(undefined); | ||
|
|
||
| await restoreTaskCache(baseInputs); | ||
|
|
||
| expect(mockedSaveState).toHaveBeenCalledWith( | ||
| State.TaskCachePaths, | ||
| expect.stringMatching(/^\[.*\]$/), | ||
| ); | ||
|
|
||
| const cachePathsCall = mockedSaveState.mock.calls.find(([key]) => key === State.TaskCachePaths); | ||
| if (cachePathsCall) { | ||
| const paths = JSON.parse(cachePathsCall[1] as string); | ||
| expect(Array.isArray(paths)).toBe(true); | ||
| expect(paths.length).toBeGreaterThan(0); | ||
| } | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { restoreCache as restoreCacheAction } from "@actions/cache"; | ||
| import { warning, info, debug, saveState, setOutput } from "@actions/core"; | ||
| import { arch, platform } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import type { Inputs } from "./types.js"; | ||
| import { State, Outputs } from "./types.js"; | ||
| import { getConfiguredProjectDir } from "./utils.js"; | ||
|
|
||
| export async function restoreTaskCache(inputs: Inputs): Promise<void> { | ||
| const projectDir = getConfiguredProjectDir(inputs); | ||
|
|
||
| // Task cache path is fixed: node_modules/.vite/task-cache | ||
| const taskCachePath = resolve(projectDir, "node_modules", ".vite", "task-cache"); | ||
| const cachePaths = [taskCachePath]; | ||
|
|
||
| debug(`Task cache path: ${taskCachePath}`); | ||
| saveState(State.TaskCachePaths, JSON.stringify(cachePaths)); | ||
|
|
||
| // Generate cache key: vite-task-{runner.os}-{runner.arch}-{run_id}-{run_attempt} | ||
| const runnerOS = process.env.RUNNER_OS || platform(); | ||
| const runnerArch = arch(); | ||
| const runId = process.env.GITHUB_RUN_ID; | ||
| const runAttempt = process.env.GITHUB_RUN_ATTEMPT; | ||
|
|
||
| if (!runId || !runAttempt) { | ||
| warning( | ||
| `GitHub run ID or attempt not found. Task cache requires GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT. Skipping task cache restore.`, | ||
| ); | ||
| setOutput(Outputs.TaskCacheHit, false); | ||
| return; | ||
| } | ||
|
|
||
| const primaryKey = `vite-task-${runnerOS}-${runnerArch}-${runId}-${runAttempt}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In workflows with multiple jobs on the same OS/architecture, every job shares this exact key because Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure how I'm supposed to be doing this..,? this was the suggested key on the GitHub Actions Caching page as far as I remember |
||
| const restoreKeys = [`vite-task-${runnerOS}-${runnerArch}-`]; | ||
|
|
||
| debug(`Task cache primary key: ${primaryKey}`); | ||
| debug(`Task cache restore keys: ${restoreKeys.join(", ")}`); | ||
|
|
||
| saveState(State.TaskCachePrimaryKey, primaryKey); | ||
|
|
||
| // Attempt to restore cache | ||
| const matchedKey = await restoreCacheAction(cachePaths, primaryKey, restoreKeys); | ||
|
|
||
| if (matchedKey) { | ||
| info(`Task cache restored from key: ${matchedKey}`); | ||
| saveState(State.TaskCacheMatchedKey, matchedKey); | ||
| setOutput(Outputs.TaskCacheHit, true); | ||
| } else { | ||
| info("Task cache not found"); | ||
| setOutput(Outputs.TaskCacheHit, false); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new input and output are absent from the public interface documentation: the Inputs table at
README.md:329-344, Outputs table atREADME.md:350-355, and Caching section atREADME.md:357-375still describe only dependency caching. Since the repository's marketplace README is how users discover and configure this action, the feature is effectively hidden and users cannot learn its cache path/key behavior or consumetask-cache-hit; add the new input, output, and usage details there.Useful? React with 👍 / 👎.