Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ inputs:
description: "Enable caching of project dependencies"
required: false
default: "false"
task-cache:
description: "Enable task caching via GitHub Actions"
required: false
default: "false"
Comment on lines +40 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new task-cache interface

The new input and output are absent from the public interface documentation: the Inputs table at README.md:329-344, Outputs table at README.md:350-355, and Caching section at README.md:357-375 still 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 consume task-cache-hit; add the new input, output, and usage details there.

Useful? React with 👍 / 👎.

cache-dependency-path:
description: "Path to lock file for cache key generation. Auto-detected if not specified."
required: false
Expand All @@ -52,6 +56,8 @@ outputs:
description: "The installed version of Vite+"
cache-hit:
description: "Boolean indicating if cache was restored"
task-cache-hit:
description: "Boolean indicating if task cache was restored"

runs:
using: node24
Expand Down
4 changes: 2 additions & 2 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/gitlab/index.mjs

Large diffs are not rendered by default.

130 changes: 65 additions & 65 deletions dist/index.mjs

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { setupSfw } from "./install-sfw.js";
import { runViteInstall } from "./run-install.js";
import { restoreCache } from "./cache-restore.js";
import { saveCache } from "./cache-save.js";
import { restoreTaskCache } from "./task-cache-restore.js";
import { saveTaskCache } from "./task-cache-save.js";
import { State, Outputs } from "./types.js";
import type { Inputs } from "./types.js";
import { resolveNodeVersionFile } from "./node-version-file.js";
Expand Down Expand Up @@ -53,6 +55,11 @@ async function runMain(inputs: Inputs): Promise<void> {
await restoreCache(inputs);
}

// Step 5.5: Restore task cache if enabled
if (inputs.taskCache) {
await restoreTaskCache(inputs);
}

// Step 6: Install Socket Firewall Free if requested (must run before vp install).
// setupSfw centralizes all the decision branches: run-install disabled, sfw
// already on PATH (e.g. via socketdev/action@<sha>), supported platform
Expand Down Expand Up @@ -88,6 +95,9 @@ async function runPost(inputs: Inputs): Promise<void> {
if (inputs.cache) {
await saveCache();
}
if (inputs.taskCache) {
await saveTaskCache();
}
}

async function main(): Promise<void> {
Expand Down
1 change: 1 addition & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function getInputs(): Inputs {
sfw: getBooleanInput("sfw"),
cache: getBooleanInput("cache"),
cacheDependencyPath: getInput("cache-dependency-path") || undefined,
taskCache: getBooleanInput("task-cache"),
registryUrl: getInput("registry-url") || undefined,
scope: getInput("scope") || undefined,
};
Expand Down
1 change: 1 addition & 0 deletions src/install-sfw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function makeInputs(overrides: Partial<Inputs> = {}): Inputs {
sfw: true,
cache: false,
cacheDependencyPath: undefined,
taskCache: false,
registryUrl: undefined,
scope: undefined,
...overrides,
Expand Down
1 change: 1 addition & 0 deletions src/install-viteplus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const baseInputs: Inputs = {
sfw: false,
cache: false,
cacheDependencyPath: undefined,
taskCache: false,
registryUrl: undefined,
scope: undefined,
};
Expand Down
191 changes: 191 additions & 0 deletions src/task-cache-restore.test.ts
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);
}
});
});
52 changes: 52 additions & 0 deletions src/task-cache-restore.ts
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}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include a job-specific discriminator in the task-cache key

In workflows with multiple jobs on the same OS/architecture, every job shares this exact key because GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT are workflow-wide. GitHub caches are immutable, so the first job to save reserves the key; later jobs either lose the save race or restore an exact hit and deliberately skip saving, meaning their task-cache entries are never persisted. Include a project/job or user-configurable discriminator so matrix and multi-project jobs can maintain independent caches.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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);
}
}
Loading
Loading