diff --git a/.env.example b/.env.example index d667ed1..e592dfd 100644 --- a/.env.example +++ b/.env.example @@ -1,66 +1,373 @@ -# Do not commit your actual .env file to Git! This may contain secrets or other -# private information. +################################################################################ +# Do not commit your actual .env file to Git! This may contain secrets or # +# other sensitive information. # +################################################################################ -# Enable/disable step debug logging (default: `false`). For local debugging, it -# may be useful to set it to `true`. -ACTIONS_STEP_DEBUG=true - -# GitHub Actions inputs should follow `INPUT_` format (case-sensitive). -# Hyphens should not be converted to underscores! -INPUT_MILLISECONDS=2400 +################################################################################ +# @github/local-action Variables # +################################################################################ # Environment variables specific to the @github/local-action tool. -# -# LOCAL_ACTION_ARTIFACT_PATH: Local path where any artifacts will be saved. Will -# throw an error if the action attempts to use the -# @actions/artifact package without setting this. + +###################################################################### +# The path to a workspace that can be used by `@github/local-action` # +# during local testing. This will be used as the root directory for # +# any files created by your action, as well as any environment files # +# that are created automatically by `@github/local-action`. If you # +# try to interact with the `@actions/artifact` or `@actions/cache` # +# packages, this tool will throw an error if this is not set to a # +# valid path. # +###################################################################### +LOCAL_ACTION_WORKSPACE="" + +###################################################################### +# Local path where any artifacts will be saved. Will throw an error # +# if the action attempts to use the `@actions/artifact` package # +# without setting this value first. # +###################################################################### LOCAL_ACTION_ARTIFACT_PATH="" -# GitHub Actions default environment variables. These are set for every run of a -# workflow and can be used in your actions. Setting the value here will override -# any value set by the local-action tool. +###################################################################### +# Local path where any caches will be read or saved. Will throw an # +# error if the action attempts to use the `@actions/cache` package # +# without setting this value first. # +###################################################################### +LOCAL_ACTION_CACHE_PATH="" + +################################################################################ +# GitHub Actions Inputs # +# # +# GitHub Actions inputs should follow `INPUT_` format (all uppercase). # +# Hyphens should not be converted to underscores! If your `action.yml` file # +# contains default values for inputs, you do not need to set them here too. # +################################################################################ + +INPUT_MILLISECONDS=2400 + +################################################################################ +# GitHub Actions Environment Variables # +################################################################################ + +# The following variables are set for every run of a workflow and can be used in +# your action. The initial values in this file are the defaults set by GitHub +# Actions. You can override them to suit your needs. +# +# For more information, see: # https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables -# CI="true" +###################################################################### +# Enable/disable step debug logging. Default is normally `false`, # +# but it is set to `true` for local debugging purposes. # +###################################################################### +ACTIONS_STEP_DEBUG=true + +###################################################################### +# Always set to `true`. # +###################################################################### +CI="true" + +###################################################################### +# The name of the action currently running, or the id of a step. For # +# example, the second invocation of `actions/checkout` will be # +# `actionscheckout2`. # +###################################################################### # GITHUB_ACTION="" + +###################################################################### +# Only supported in composite actions. # +###################################################################### # GITHUB_ACTION_PATH="" + +###################################################################### +# For a step executing an action, this is the owner and repository # +# name of the action. For example, `actions/checkout`. # +###################################################################### # GITHUB_ACTION_REPOSITORY="" -# GITHUB_ACTIONS="" -# GITHUB_ACTOR="mona" -# GITHUB_ACTOR_ID="123456789" -# GITHUB_API_URL="" + +###################################################################### +# Always set to true when GitHub Actions is running the workflow. # +# You can use this variable to differentiate when tests are being # +# run locally or by GitHub Actions. # +###################################################################### +GITHUB_ACTIONS="true" + +###################################################################### +# The name of the person or app that initiated the workflow. For # +# example, `octocat`. # +###################################################################### +# GITHUB_ACTOR="" + +###################################################################### +# The account ID of the person or app that triggered the initial # +# workflow run. For example, `1234567`. Note that this is different # +# from the actor username. # +###################################################################### +# GITHUB_ACTOR_ID="" + +###################################################################### +# Returns the API URL. For example: `https://api.github.com`. # +###################################################################### +GITHUB_API_URL="https://api.github.com" + +###################################################################### +# The name of the base ref or target branch of the pull request in a # +# workflow run. This is only set when the event that triggers a # +# workflow run is either pull_request or pull_request_target. For # +# example, `main`. # +###################################################################### # GITHUB_BASE_REF="" -# GITHUB_ENV="" + +###################################################################### +# The name of the event that triggered the workflow. For example, # +# `workflow_dispatch`. # +###################################################################### # GITHUB_EVENT_NAME="" + +###################################################################### +# The path to the file on the runner that contains the full event # +# webhook payload. For example, `/github/workflow/event.json`. # +###################################################################### # GITHUB_EVENT_PATH="" -# GITHUB_GRAPHQL_URL="" + +###################################################################### +# Returns the GraphQL API URL. For example: # +# `https://api.github.com/graphql`. # +###################################################################### +GITHUB_GRAPHQL_URL="https://api.github.com/graphql" + +###################################################################### +# The head ref or source branch of the pull request in a workflow # +# run. This property is only set when the event that triggers a # +# workflow run is either pull_request or pull_request_target. For # +# example, `feature-branch-1`. # +###################################################################### # GITHUB_HEAD_REF="" + +###################################################################### +# The job_id of the current job. For example, `greeting_job`. # +###################################################################### # GITHUB_JOB="" -# GITHUB_OUTPUT="" -# GITHUB_PATH="" + +###################################################################### +# The fully-formed ref of the branch or tag that triggered the # +# workflow run. For workflows triggered by push, this is the branch # +# or tag ref that was pushed. For workflows triggered by # +# pull_request, this is the pull request merge branch. For workflows # +# triggered by release, this is the release tag created. For other # +# triggers, this is the branch or tag ref that triggered the # +# workflow run. This is only set if a branch or tag is available for # +# the event type. The ref given is fully-formed, meaning that for # +# branches the format is refs/heads/. For pull requests # +# events except pull_request_target, it is # +# refs/pull//merge. pull_request_target events have the # +# ref from the base branch. For tags it is refs/tags/. For # +# example, refs/heads/feature-branch-1. # +###################################################################### # GITHUB_REF="" + +###################################################################### +# The short ref name of the branch or tag that triggered the # +# workflow run. This value matches the branch or tag name shown on # +# GitHub. For example, `feature-branch-1`. For pull requests, the # +# format is `/merge`. # +###################################################################### # GITHUB_REF_NAME="" + +###################################################################### +# `true` if branch protections or rulesets are configured for the # +# ref that triggered the workflow run. # +###################################################################### # GITHUB_REF_PROTECTED="" + +###################################################################### +# The type of ref that triggered the workflow run. Valid values are # +# `branch` or `tag`. # +###################################################################### # GITHUB_REF_TYPE="" + +###################################################################### +# The owner and repository name. For example, `octocat/Hello-World`. # +###################################################################### # GITHUB_REPOSITORY="" + +###################################################################### +# The ID of the repository. For example, `123456789`. Note that this # +# is different from the repository name. # +###################################################################### # GITHUB_REPOSITORY_ID="" + +###################################################################### +# The repository owner's name. For example, `octocat`. # +###################################################################### # GITHUB_REPOSITORY_OWNER="" + +###################################################################### +# The repository owner's account ID. For example, `1234567`. Note # +# that this is different from the owner's name. # +###################################################################### # GITHUB_REPOSITORY_OWNER_ID="" -# GITHUB_RETENTION_DAYS="" -# GITHUB_RUN_ATTEMPT="" + +###################################################################### +# The number of days that workflow run logs and artifacts are kept. # +# For example, 90. # +###################################################################### +GITHUB_RETENTION_DAYS="90" + +###################################################################### +# A unique number for each attempt of a particular workflow run in a # +# repository. This number begins at `1` for the workflow run's first # +# attempt, and increments with each re-run. For example, `3`. # +###################################################################### +GITHUB_RUN_ATTEMPT="1" + +###################################################################### +# A unique number for each workflow run within a repository. This # +# number does not change if you re-run the workflow run. For # +# example, `1658821493`. # +###################################################################### # GITHUB_RUN_ID="" -# GITHUB_RUN_NUMBER="" -# GITHUB_SERVER_URL="" + +###################################################################### +# A unique number for each run of a particular workflow in a # +# repository. This number begins at `1` for the workflow's first # +# run, and increments with each new run. This number does not # +# change if you re-run the workflow run. For example, `3`. # +###################################################################### +GITHUB_RUN_NUMBER="1" + +###################################################################### +# The URL of the GitHub server. For example: `https://github.com`. # +###################################################################### +GITHUB_SERVER_URL="https://github.com" + +###################################################################### +# The commit SHA that triggered the workflow. The value of this # +# commit SHA depends on the event that triggered the workflow. For # +# more information, see Events that trigger workflows. For example, # +# `ffac537e6cbbf934b08745a378932722df287a53`. # +###################################################################### # GITHUB_SHA="" -# GITHUB_STEP_SUMMARY="" + +###################################################################### +# The username of the user that initiated the workflow run. If the # +# workflow run is a re-run, this value may differ from # +# `github.actor`. Any workflow re-runs will use the privileges of # +# `github.actor`, even if the actor initiating the re-run # +# (`github.triggering_actor`) has different privileges. # +###################################################################### # GITHUB_TRIGGERING_ACTOR="" -# GITHUB_WORKFLOW="" + +###################################################################### +# The name of the workflow. For example, `My test workflow``. If the # +# workflow file doesn't specify a name, the value of this variable # +# is the full path of the workflow file in the repository. # +###################################################################### +GITHUB_WORKFLOW="Local Action" + +###################################################################### +# The ref path to the workflow. # +###################################################################### # GITHUB_WORKFLOW_REF="" + +###################################################################### +# The commit SHA for the workflow file. # +###################################################################### # GITHUB_WORKFLOW_SHA="" + +###################################################################### +# The default working directory on the runner for steps, and the # +# default location of your repository when using the checkout # +# action. For example, `/home/runner/work/my-repo-name/repo-name`. # +###################################################################### # GITHUB_WORKSPACE="" + +###################################################################### +# The architecture of the runner executing the job. Possible values # +# are `X86`, X6`4, `ARM`, or `ARM64`. # +###################################################################### # RUNNER_ARCH="" -# RUNNER_DEBUG="" -# RUNNER_NAME="" + +###################################################################### +# This is set only if debug logging is enabled, and always has the # +# value of 1. It can be useful as an indicator to enable additional # +# debugging or verbose logging in your own job steps. Default is # +# normally not set, but it is set to `1` for local debugging # +# purposes. # +###################################################################### +RUNNER_DEBUG="1" + +###################################################################### +# The environment of the runner executing the job. Possible values # +# are: github-hosted for GitHub-hosted runners provided by GitHub, # +# and self-hosted for self-hosted runners configured by the # +# repository owner. # +###################################################################### +RUNNER_ENVIRONMENT="github-hosted" + +###################################################################### +# The name of the runner executing the job. This name may not be # +# unique in a workflow run as runners at the repository and # +# organization levels could use the same name. For example, # +# `Hosted Agent`. # +###################################################################### +RUNNER_NAME="Local Action" + +###################################################################### +# The operating system of the runner executing the job. Possible # +# values are `Linux`, `Windows`, or `macOS`. For example, `Windows`. # +###################################################################### # RUNNER_OS="" + +###################################################################### +# The path to a temporary directory on the runner. This directory is # +# emptied at the beginning and end of each job. Note that files will # +# not be removed if the runner's user account does not have # +# permission to delete them. For example, `D:\a\_temp`. # +###################################################################### # RUNNER_TEMP="" -# RUNNER_TOOL_CACHE="" \ No newline at end of file + +###################################################################### +# The path to the directory containing preinstalled tools for # +# GitHub-hosted runners. For more information, see Using # +# GitHub-hosted runners. For example, `C:\hostedtoolcache\windows`. # +###################################################################### +# RUNNER_TOOL_CACHE="" + + +################################################################################ +# GitHub Actions Environment Files # +# # +# The following variables point to specific files on the runner that are used # +# to set environment variables, outputs, and other information. These files # +# are created and managed by GitHub Actions. # +# # +# Currently these are not used by `@github/local-action`. # +################################################################################ + +###################################################################### +# The path on the runner to the file that sets variables from # +# workflow commands. The path to this file is unique to the current # +# step and changes for each step in a job. # +###################################################################### +# GITHUB_ENV="" + +###################################################################### +# The path on the runner to the file that sets the current step's # +# outputs from workflow commands. The path to this file is unique to # +# the current step and changes for each step in a job. # +###################################################################### +# GITHUB_OUTPUT="" + +###################################################################### +# The path on the runner to the file that sets system `PATH` # +# variables from workflow commands. The path to this file is unique # +# to the current step and changes for each step in a job. # +###################################################################### +# GITHUB_PATH="" + +###################################################################### +# The path on the runner to the file that contains job summaries # +# from workflow commands. The path to this file is unique to the # +# current step and changes for each step in a job. # +###################################################################### +# GITHUB_STEP_SUMMARY="" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd1364..d98d910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v6 + +Adds support for the `@actions/cache` package, allowing for local caching of +dependencies and other files between runs of a GitHub Action. This is achieved +by setting the `LOCAL_ACTION_CACHE_PATH` environment variable to a directory +where cache files will be stored. + +For both `@actions/artifact` and `@actions/cache`, the `LOCAL_ACTION_WORKSPACE` +environment variable must be set. Otherwise, calling functions will throw an +error. Similarly, `@actions/artifact` requires the `LOCAL_ACTION_ARTIFACT_PATH` +environment variable to be set, and `@actions/cache` requires the +`LOCAL_ACTION_CACHE_PATH` environment variable to be set. + ## v5 Removes support for custom `paths` in the target action's `tsconfig.json`. This diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99fbb6a..5f28c38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,3 +104,37 @@ npm run test > [!NOTE] > > This requires that you have already run `npm install` + +### Toolkit Stub Updates + +When updating any of the supported stubs for the GitHub Actions Toolkit, please +make sure to also update the following. This helps ensure that we are using the +latest supported versions of the toolkit packages, and makes comparing changes +across commits easier. + +1. The supported version in the [`README.md`](./README.md) + + ```markdown + | Package | Version | + | ---------------------------------------------------------------------- | -------- | + | [`@actions/artifact`](https://www.npmjs.com/package/@actions/artifact) | `2.3.2` | + | [`@actions/core`](https://www.npmjs.com/package/@actions/core) | `1.11.1` | + | [`@actions/github`](https://www.npmjs.com/package/@actions/github) | `6.0.1` | + ``` + +1. The latest reviewed commit URL in the corresponding `src/stubs/*.ts` file + + ```typescript + /** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/artifact.ts + * Last Reviewed Date: 2025-09-10 + */ + ``` + +When comparing commits to see what has been updated in the toolkit packages, be +sure to compare against the last reviewed commit in the corresponding stub file. +This can be done more easily using the following URL format: + +```plain +https://github.com/actions/toolkit/compare/..main +``` diff --git a/README.md b/README.md index 2b7e06c..7f4f7a8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ currently implemented by this tool. | ---------------------------------------------------------------------- | -------- | | [`@actions/artifact`](https://www.npmjs.com/package/@actions/artifact) | `2.3.2` | | [`@actions/core`](https://www.npmjs.com/package/@actions/core) | `1.11.1` | -| [`@actions/github`](https://www.npmjs.com/package/@actions/github) | `6.0.0` | +| [`@actions/github`](https://www.npmjs.com/package/@actions/github) | `6.0.1` | ## Changelog diff --git a/__fixtures__/@actions/core.ts b/__fixtures__/@actions/core.ts index dd8793f..d6fc9e3 100644 --- a/__fixtures__/@actions/core.ts +++ b/__fixtures__/@actions/core.ts @@ -3,6 +3,7 @@ import { jest } from '@jest/globals' export const debug = jest.fn().mockImplementation(() => {}) export const error = jest.fn().mockImplementation(() => {}) export const info = jest.fn().mockImplementation(() => {}) +export const isDebug = jest.fn().mockImplementation(() => false) export const getInput = jest.fn().mockImplementation(() => {}) export const setOutput = jest.fn().mockImplementation(() => {}) export const setFailed = jest.fn().mockImplementation(() => {}) diff --git a/__fixtures__/@actions/io.ts b/__fixtures__/@actions/io.ts new file mode 100644 index 0000000..a2b43bc --- /dev/null +++ b/__fixtures__/@actions/io.ts @@ -0,0 +1,9 @@ +import { jest } from '@jest/globals' + +export const which = jest.fn().mockImplementation(() => {}) +export const mkdirP = jest.fn().mockImplementation(() => {}) + +export default { + which, + mkdirP +} diff --git a/__fixtures__/fs.ts b/__fixtures__/fs.ts index 740bff7..f67ae7d 100644 --- a/__fixtures__/fs.ts +++ b/__fixtures__/fs.ts @@ -1,21 +1,27 @@ import { jest } from '@jest/globals' export const accessSync = jest.fn() +export const copyFileSync = jest.fn() export const createWriteStream = jest.fn() export const createReadStream = jest.fn() export const existsSync = jest.fn() export const mkdirSync = jest.fn() +export const readdirSync = jest.fn() export const readFileSync = jest.fn() export const rmSync = jest.fn() export const statSync = jest.fn() +export const writeFileSync = jest.fn() export default { accessSync, + copyFileSync, createWriteStream, createReadStream, existsSync, mkdirSync, + readdirSync, readFileSync, rmSync, - statSync + statSync, + writeFileSync } diff --git a/__tests__/stubs/artifact/internal/client.test.ts b/__tests__/stubs/artifact/internal/client.test.ts index c4ca69b..603208e 100644 --- a/__tests__/stubs/artifact/internal/client.test.ts +++ b/__tests__/stubs/artifact/internal/client.test.ts @@ -67,6 +67,7 @@ const { DefaultArtifactClient } = await import( describe('DefaultArtifactClient', () => { beforeEach(() => { // Set environment variables + process.env.LOCAL_ACTION_WORKSPACE = '/tmp/artifacts' process.env.LOCAL_ACTION_ARTIFACT_PATH = '/tmp/artifacts' // Reset metadata @@ -80,10 +81,23 @@ describe('DefaultArtifactClient', () => { jest.resetAllMocks() // Unset environment variables + delete process.env.LOCAL_ACTION_WORKSPACE delete process.env.LOCAL_ACTION_ARTIFACT_PATH }) describe('uploadArtifact', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + const client = new DefaultArtifactClient() + + await expect( + client.uploadArtifact('artifact-name', ['file1', 'file2'], 'root') + ).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) + }) + it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => { delete process.env.LOCAL_ACTION_ARTIFACT_PATH @@ -114,6 +128,16 @@ describe('DefaultArtifactClient', () => { }) describe('downloadArtifact', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + const client = new DefaultArtifactClient() + + await expect(client.downloadArtifact(1)).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) + }) + it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => { delete process.env.LOCAL_ACTION_ARTIFACT_PATH @@ -157,6 +181,16 @@ describe('DefaultArtifactClient', () => { }) describe('listArtifacts', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + const client = new DefaultArtifactClient() + + await expect(client.listArtifacts()).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) + }) + it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => { delete process.env.LOCAL_ACTION_ARTIFACT_PATH @@ -200,6 +234,16 @@ describe('DefaultArtifactClient', () => { }) describe('getArtifact', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + const client = new DefaultArtifactClient() + + await expect(client.getArtifact('artifact-name')).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) + }) + it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => { delete process.env.LOCAL_ACTION_ARTIFACT_PATH @@ -243,6 +287,16 @@ describe('DefaultArtifactClient', () => { }) describe('deleteArtifact', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + const client = new DefaultArtifactClient() + + await expect(client.deleteArtifact('artifact-name')).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) + }) + it('Throws if LOCAL_ACTION_ARTIFACT_PATH is not set', async () => { delete process.env.LOCAL_ACTION_ARTIFACT_PATH diff --git a/__tests__/stubs/cache/cache.test.ts b/__tests__/stubs/cache/cache.test.ts new file mode 100644 index 0000000..0145134 --- /dev/null +++ b/__tests__/stubs/cache/cache.test.ts @@ -0,0 +1,358 @@ +import { jest } from '@jest/globals' +import * as core from '../../../__fixtures__/@actions/core.js' +import * as fs from '../../../__fixtures__/fs.js' + +jest.unstable_mockModule('@actions/core', () => core) +jest.unstable_mockModule('fs', () => fs) + +// Mock the internal dependencies +const mockTar = { + createTar: jest.fn() as jest.MockedFunction, + extractTar: jest.fn() as jest.MockedFunction, + listTar: jest.fn() as jest.MockedFunction +} + +jest.unstable_mockModule( + '../../../src/stubs/cache/internal/tar.js', + () => mockTar +) + +const mockUtils = { + createTempDirectory: jest.fn() as jest.MockedFunction<() => Promise>, + getArchiveFileSizeInBytes: jest.fn() as jest.MockedFunction< + (path: string) => number + >, + getCacheFileName: jest.fn() as jest.MockedFunction< + (compression: string) => string + >, + getCacheVersion: jest.fn() as jest.MockedFunction< + (paths: string[], compression: string, crossOs: boolean) => string + >, + getCompressionMethod: jest.fn() as jest.MockedFunction<() => Promise>, + resolvePaths: jest.fn() as jest.MockedFunction< + (paths: string[]) => Promise + >, + unlinkFile: jest.fn() as jest.MockedFunction<(path: string) => Promise> +} + +jest.unstable_mockModule( + '../../../src/stubs/cache/internal/cacheUtils.js', + () => mockUtils +) + +// Mock env +const mockEnvMeta = { + caches: {} +} + +jest.unstable_mockModule('../../../src/stubs/env.js', () => ({ + EnvMeta: mockEnvMeta +})) + +// Import after mocking +const { CACHE_STUBS, restoreCache, saveCache } = await import( + '../../../src/stubs/cache/cache.js' +) + +describe('cache/cache', () => { + const originalWorkspace = process.env.LOCAL_ACTION_WORKSPACE + const originalCachePath = process.env.LOCAL_ACTION_CACHE_PATH + + beforeEach(() => { + jest.clearAllMocks() + + process.env.LOCAL_ACTION_WORKSPACE = '/tmp/workspace' + process.env.LOCAL_ACTION_CACHE_PATH = '/tmp/cache' + + // Reset default mocks + mockUtils.getArchiveFileSizeInBytes.mockReturnValue(1024 * 1024) // 1MB + mockUtils.getCacheFileName.mockReturnValue('cache.tgz') + mockUtils.getCacheVersion.mockReturnValue('abc123') + mockUtils.getCompressionMethod.mockResolvedValue('gzip') + mockUtils.resolvePaths.mockResolvedValue(['/path/to/files']) + mockUtils.createTempDirectory.mockResolvedValue('/tmp/archive') + mockUtils.unlinkFile.mockResolvedValue() + + core.isDebug.mockReturnValue(false) + fs.readdirSync.mockReturnValue([]) + fs.existsSync.mockReturnValue(false) + + mockTar.createTar.mockResolvedValue(undefined) + mockTar.extractTar.mockResolvedValue(undefined) + mockTar.listTar.mockResolvedValue(undefined) + + mockEnvMeta.caches = {} + }) + + afterEach(() => { + process.env.LOCAL_ACTION_WORKSPACE = originalWorkspace + process.env.LOCAL_ACTION_CACHE_PATH = originalCachePath + }) + + describe('CACHE_STUBS', () => { + it('Exports the correct cache stub functions', () => { + expect(CACHE_STUBS).toHaveProperty('restoreCache') + expect(CACHE_STUBS).toHaveProperty('saveCache') + expect(typeof CACHE_STUBS.restoreCache).toBe('function') + expect(typeof CACHE_STUBS.saveCache).toBe('function') + }) + }) + + describe('restoreCache', () => { + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + await expect(restoreCache(['src/'], 'cache-key')).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!' + ) + }) + + it('Throws if LOCAL_ACTION_CACHE_PATH is not set', async () => { + delete process.env.LOCAL_ACTION_CACHE_PATH + + await expect(restoreCache(['src/'], 'cache-key')).rejects.toThrow( + 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!' + ) + }) + + it('Throws if no paths are provided', async () => { + await expect(restoreCache([], 'cache-key')).rejects.toThrow( + 'Path Validation Error: At least one directory or file path is required' + ) + }) + + it('Throws if too many keys are provided', async () => { + const manyKeys = Array(11).fill('key') + + await expect( + restoreCache(['src/'], 'primary-key', manyKeys) + ).rejects.toThrow( + 'Key Validation Error: Keys are limited to a maximum of 10.' + ) + }) + + it('Returns undefined if no cache is found', async () => { + fs.readdirSync.mockReturnValue([]) + + const result = await restoreCache(['src/'], 'cache-key') + + expect(result).toBeUndefined() + }) + + it('Returns matched key for exact cache hit', async () => { + // Mock file system to return cache files - the key must match exactly + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'cache-key') + + expect(result).toBe('cache-key') + expect(core.info).toHaveBeenCalledWith('Cache hit for: cache-key') + }) + + it('Handles undefined restore keys (nullish coalescing coverage)', async () => { + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'cache-key', undefined) + + expect(result).toBe('cache-key') + expect(core.info).toHaveBeenCalledWith('Cache hit for: cache-key') + }) + + it('Returns matched key for restore key hit', async () => { + // Mock file system to return cache files that match restore key + fs.readdirSync.mockReturnValue(['restore-key-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'primary-key', [ + 'restore-key' + ]) + + expect(result).toBe('restore-key') + expect(core.info).toHaveBeenCalledWith( + 'Cache hit for restore-key: restore-key' + ) + }) + + it('Handles lookup only option', async () => { + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'cache-key', [], { + lookupOnly: true + }) + + expect(result).toBe('cache-key') + expect(core.info).toHaveBeenCalledWith('Lookup only - skipping download') + expect(mockTar.extractTar).not.toHaveBeenCalled() + }) + + it('Extracts cache when found', async () => { + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'cache-key') + + expect(result).toBe('cache-key') + expect(mockTar.extractTar).toHaveBeenCalled() + expect(core.info).toHaveBeenCalledWith('Cache restored successfully') + }) + + it('Lists tar contents in debug mode', async () => { + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + core.isDebug.mockReturnValue(true) + + await restoreCache(['src/'], 'cache-key') + + expect(mockTar.listTar).toHaveBeenCalled() + }) + + it('Handles errors during restoration', async () => { + fs.readdirSync.mockReturnValue(['cache-key-abc123def456.cache']) + mockTar.extractTar.mockRejectedValue(new Error('Extraction failed')) + + const result = await restoreCache(['src/'], 'cache-key') + + expect(result).toBeUndefined() + expect(core.warning).toHaveBeenCalledWith( + 'Failed to restore: Extraction failed' + ) + }) + + it('Finds cache with partial key match', async () => { + // Reset to clean state and setup specific mock for this test + fs.readdirSync.mockReturnValue(['cache-key-extra-abc123def456.cache']) + + const result = await restoreCache(['src/'], 'cache-key') + + expect(result).toBe('cache-key') + }) + }) + + describe('saveCache', () => { + beforeEach(() => { + mockEnvMeta.caches = { 1: 'existing-cache.cache' } + }) + + it('Throws if LOCAL_ACTION_WORKSPACE is not set', async () => { + delete process.env.LOCAL_ACTION_WORKSPACE + + await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!' + ) + }) + + it('Throws if LOCAL_ACTION_CACHE_PATH is not set', async () => { + delete process.env.LOCAL_ACTION_CACHE_PATH + + await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow( + 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!' + ) + }) + + it('Throws if no paths are provided', async () => { + await expect(saveCache([], 'cache-key')).rejects.toThrow( + 'Path Validation Error: At least one directory or file path is required' + ) + }) + + it('Throws if key is too long', async () => { + const longKey = 'a'.repeat(513) + + await expect(saveCache(['src/'], longKey)).rejects.toThrow( + `Key Validation Error: ${longKey} cannot be larger than 512 characters.` + ) + }) + + it('Throws if key contains commas', async () => { + const invalidKey = 'cache,key' + + await expect(saveCache(['src/'], invalidKey)).rejects.toThrow( + `Key Validation Error: ${invalidKey} cannot contain commas.` + ) + }) + + it('Returns -1 if no paths resolve', async () => { + mockUtils.resolvePaths.mockResolvedValue([]) + + // The implementation throws an error when no paths resolve + await expect(saveCache(['src/'], 'cache-key')).rejects.toThrow( + 'Path(s) specified in the action for caching do(es) not exist' + ) + }) + + it('Throws if cache already exists', async () => { + // Setup successful path resolution but existing cache + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(true) + + const result = await saveCache(['src/'], 'cache-key') + + // The implementation catches the error and returns -1 + expect(result).toBe(-1) + }) + + it('Successfully saves cache', async () => { + // Setup successful path resolution and no existing cache + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + + const result = await saveCache(['src/'], 'cache-key') + + expect(result).toBe(2) // Next ID after existing cache + expect(mockTar.createTar).toHaveBeenCalled() + expect(fs.copyFileSync).toHaveBeenCalled() + }) + + it('Lists tar contents in debug mode', async () => { + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + core.isDebug.mockReturnValue(true) + + await saveCache(['src/'], 'cache-key') + + expect(mockTar.listTar).toHaveBeenCalled() + }) + + it('Handles errors during save', async () => { + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + mockTar.createTar.mockRejectedValue(new Error('Create failed')) + + const result = await saveCache(['src/'], 'cache-key') + + expect(result).toBe(-1) + expect(core.warning).toHaveBeenCalledWith('Failed to save: Create failed') + }) + + it('Cleans up archive file after save', async () => { + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + + await saveCache(['src/'], 'cache-key') + + expect(mockUtils.unlinkFile).toHaveBeenCalled() + }) + + it('Handles archive cleanup errors', async () => { + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + mockUtils.unlinkFile.mockRejectedValue(new Error('Cleanup failed')) + + await saveCache(['src/'], 'cache-key') + + expect(core.debug).toHaveBeenCalledWith( + 'Failed to delete archive: Error: Cleanup failed' + ) + }) + + it('Uses cross OS archive setting', async () => { + mockUtils.resolvePaths.mockResolvedValue(['/resolved/path']) + fs.existsSync.mockReturnValue(false) + + await saveCache(['src/'], 'cache-key', {}, true) + + expect(mockUtils.getCacheVersion).toHaveBeenCalledWith( + ['src/'], + 'gzip', + true + ) + }) + }) +}) diff --git a/__tests__/stubs/cache/errors.test.ts b/__tests__/stubs/cache/errors.test.ts new file mode 100644 index 0000000..d9060c3 --- /dev/null +++ b/__tests__/stubs/cache/errors.test.ts @@ -0,0 +1,55 @@ +import { + InvalidResponseError, + ReserveCacheError, + ValidationError +} from '../../../src/stubs/cache/errors.js' + +describe('cache/errors', () => { + describe('ValidationError', () => { + it('Creates a ValidationError with the correct message', () => { + const message = 'Test validation error' + const error = new ValidationError(message) + + expect(error.message).toBe(message) + expect(error.name).toBe('ValidationError') + expect(error).toBeInstanceOf(ValidationError) + expect(error).toBeInstanceOf(Error) + }) + + it('Sets the prototype correctly', () => { + const error = new ValidationError('test') + + expect(Object.getPrototypeOf(error)).toBe(ValidationError.prototype) + }) + }) + + describe('ReserveCacheError', () => { + it('Creates a ReserveCacheError with the correct message', () => { + const message = 'Test reserve cache error' + const error = new ReserveCacheError(message) + + expect(error.message).toBe(message) + expect(error.name).toBe('ReserveCacheError') + expect(error).toBeInstanceOf(ReserveCacheError) + expect(error).toBeInstanceOf(Error) + }) + + it('Sets the prototype correctly', () => { + const error = new ReserveCacheError('test') + + expect(Object.getPrototypeOf(error)).toBe(ReserveCacheError.prototype) + }) + }) + + describe('InvalidResponseError', () => { + it('Creates an InvalidResponseError with the correct message', () => { + const message = 'Test invalid response error' + const error = new InvalidResponseError(message) + + expect(error.message).toBe(message) + expect(error.name).toBe('InvalidResponseError') + expect(error).toBeInstanceOf(InvalidResponseError) + expect(error).toBeInstanceOf(Error) + }) + }) +}) diff --git a/__tests__/stubs/cache/internal/cacheUtils.test.ts b/__tests__/stubs/cache/internal/cacheUtils.test.ts new file mode 100644 index 0000000..d97128d --- /dev/null +++ b/__tests__/stubs/cache/internal/cacheUtils.test.ts @@ -0,0 +1,110 @@ +import { jest } from '@jest/globals' +import * as io from '../../../../__fixtures__/@actions/io.js' +import * as fs from '../../../../__fixtures__/fs.js' + +jest.unstable_mockModule('@actions/io', () => io) +jest.unstable_mockModule('fs', () => fs) + +// Import after mocking +const { + getCacheFileName, + getCacheVersion, + createTempDirectory, + getArchiveFileSizeInBytes +} = await import('../../../../src/stubs/cache/internal/cacheUtils.js') +const { CacheFilename, CompressionMethod } = await import( + '../../../../src/stubs/cache/internal/constants.js' +) + +describe('cache/internal/cacheUtils', () => { + beforeEach(() => { + jest.clearAllMocks() + + process.env.LOCAL_ACTION_WORKSPACE = '/tmp/workspace' + + io.mkdirP.mockResolvedValue(undefined as never) + fs.statSync.mockReturnValue({ size: 1024 }) + }) + + describe('getCacheFileName', () => { + it('Returns gzip filename for gzip compression', () => { + const result = getCacheFileName(CompressionMethod.Gzip) + + expect(result).toBe(CacheFilename.Gzip) + }) + + it('Returns zstd filename for zstd compression', () => { + const result = getCacheFileName(CompressionMethod.Zstd) + + expect(result).toBe(CacheFilename.Zstd) + }) + + it('Returns zstd filename for zstd without long compression', () => { + const result = getCacheFileName(CompressionMethod.ZstdWithoutLong) + + expect(result).toBe(CacheFilename.Zstd) + }) + }) + + describe('getCacheVersion', () => { + it('Generates cache version hash from paths', () => { + const paths = ['src/', 'dist/'] + + const result = getCacheVersion(paths) + + // Should be a 64-character hex string (SHA-256) + expect(result).toMatch(/^[a-f0-9]{64}$/) + }) + + it('Includes compression method in cache version', () => { + const paths = ['src/'] + + const resultGzip = getCacheVersion(paths, CompressionMethod.Gzip) + const resultZstd = getCacheVersion(paths, CompressionMethod.Zstd) + + expect(resultGzip).not.toBe(resultZstd) + }) + + it('Does not modify original paths array', () => { + const paths = ['src/', 'dist/'] + const originalPaths = [...paths] + + getCacheVersion(paths) + + expect(paths).toEqual(originalPaths) + }) + }) + + describe('createTempDirectory', () => { + beforeEach(() => { + process.env.LOCAL_ACTION_WORKSPACE = '/tmp/workspace' + }) + + it('Creates a temporary directory', async () => { + const tempDir = await createTempDirectory() + + expect(tempDir).toMatch( + /^\/tmp\/workspace\/actions\/temp\/[a-f0-9-]{36}$/ + ) + }) + + it('Creates unique directories on multiple calls', async () => { + const tempDir1 = await createTempDirectory() + const tempDir2 = await createTempDirectory() + + expect(tempDir1).not.toBe(tempDir2) + }) + }) + + describe('getArchiveFileSizeInBytes', () => { + it('Returns the size of an archive file', () => { + const filePath = '/path/to/archive.tar.gz' + fs.statSync.mockReturnValue({ size: 2048 }) + + const size = getArchiveFileSizeInBytes(filePath) + + expect(fs.statSync).toHaveBeenCalledWith(filePath) + expect(size).toBe(2048) + }) + }) +}) diff --git a/__tests__/stubs/cache/internal/constants.test.ts b/__tests__/stubs/cache/internal/constants.test.ts new file mode 100644 index 0000000..2988a19 --- /dev/null +++ b/__tests__/stubs/cache/internal/constants.test.ts @@ -0,0 +1,81 @@ +import { + ArchiveToolType, + CacheFilename, + CacheFileSizeLimit, + CompressionMethod, + DefaultRetryAttempts, + DefaultRetryDelay, + GnuTarPathOnWindows, + ManifestFilename, + SocketTimeout, + SystemTarPathOnWindows, + TarFilename +} from '../../../../src/stubs/cache/internal/constants.js' + +describe('cache/internal/constants', () => { + describe('CacheFilename', () => { + it('Has the correct values', () => { + expect(CacheFilename.Gzip).toBe('cache.tgz') + expect(CacheFilename.Zstd).toBe('cache.tzst') + }) + }) + + describe('CompressionMethod', () => { + it('Has the correct values', () => { + expect(CompressionMethod.Gzip).toBe('gzip') + expect(CompressionMethod.ZstdWithoutLong).toBe('zstd-without-long') + expect(CompressionMethod.Zstd).toBe('zstd') + }) + }) + + describe('ArchiveToolType', () => { + it('Has the correct values', () => { + expect(ArchiveToolType.GNU).toBe('gnu') + expect(ArchiveToolType.BSD).toBe('bsd') + }) + }) + + describe('Default values', () => { + it('Has the correct default retry attempts', () => { + expect(DefaultRetryAttempts).toBe(2) + }) + + it('Has the correct default retry delay', () => { + expect(DefaultRetryDelay).toBe(5000) + }) + + it('Has the correct socket timeout', () => { + expect(SocketTimeout).toBe(5000) + }) + }) + + describe('Windows tar paths', () => { + it('Has the correct GNU tar path', () => { + expect(GnuTarPathOnWindows).toBe( + `${process.env['PROGRAMFILES']}\\Git\\usr\\bin\\tar.exe` + ) + }) + + it('Has the correct system tar path', () => { + expect(SystemTarPathOnWindows).toBe( + `${process.env['SYSTEMDRIVE']}\\Windows\\System32\\tar.exe` + ) + }) + }) + + describe('File names', () => { + it('Has the correct tar filename', () => { + expect(TarFilename).toBe('cache.tar') + }) + + it('Has the correct manifest filename', () => { + expect(ManifestFilename).toBe('manifest.txt') + }) + }) + + describe('Cache file size limit', () => { + it('Has the correct size limit (10GiB)', () => { + expect(CacheFileSizeLimit).toBe(10 * Math.pow(1024, 3)) + }) + }) +}) diff --git a/__tests__/stubs/cache/internal/tar.test.ts b/__tests__/stubs/cache/internal/tar.test.ts new file mode 100644 index 0000000..40c5029 --- /dev/null +++ b/__tests__/stubs/cache/internal/tar.test.ts @@ -0,0 +1,131 @@ +import { jest } from '@jest/globals' +import * as exec from '../../../../__fixtures__/@actions/exec.js' +import * as io from '../../../../__fixtures__/@actions/io.js' +import * as fs from '../../../../__fixtures__/fs.js' + +// Mock dependencies first +const mockGetGnuTarPathOnWindows = jest.fn() as jest.MockedFunction + +jest.unstable_mockModule('@actions/exec', () => exec) +jest.unstable_mockModule('@actions/io', () => io) +jest.unstable_mockModule('fs', () => fs) + +jest.unstable_mockModule( + '../../../../src/stubs/cache/internal/cacheUtils.js', + () => ({ + getGnuTarPathOnWindows: mockGetGnuTarPathOnWindows, + getCacheFileName: jest.fn().mockReturnValue('cache.tgz') + }) +) + +// Import after mocking +const { CompressionMethod } = await import( + '../../../../src/stubs/cache/internal/constants.js' +) +const { createTar, extractTar, listTar } = await import( + '../../../../src/stubs/cache/internal/tar.js' +) + +describe('cache/internal/tar', () => { + beforeEach(() => { + jest.clearAllMocks() + process.env.LOCAL_ACTION_WORKSPACE = '/tmp/workspace' + + // Default mock values + exec.exec.mockResolvedValue(0 as never) + fs.existsSync.mockReturnValue(true) + io.which.mockResolvedValue('/usr/bin/tar' as never) + mockGetGnuTarPathOnWindows.mockResolvedValue('/usr/bin/tar') + fs.writeFileSync.mockImplementation(() => {}) + }) + + describe('listTar', () => { + it('Lists tar contents with gzip compression', async () => { + await listTar('/path/to/archive.tar.gz', CompressionMethod.Gzip) + + expect(exec.exec).toHaveBeenCalledWith( + expect.stringContaining('tar'), + undefined, + expect.objectContaining({ + env: expect.any(Object) + }) + ) + }) + }) + + describe('extractTar', () => { + it('Extracts tar with gzip compression', async () => { + await extractTar('/path/to/archive.tar.gz', CompressionMethod.Gzip) + + expect(exec.exec).toHaveBeenCalledWith( + expect.stringContaining('tar'), + undefined, + expect.objectContaining({ + env: expect.any(Object) + }) + ) + }) + }) + + describe('createTar', () => { + it('Creates tar with gzip compression', async () => { + const sourceDirectories = ['src/', 'dist/'] + await createTar( + '/path/to/archive.tar.gz', + sourceDirectories, + CompressionMethod.Gzip + ) + + expect(fs.writeFileSync).toHaveBeenCalled() + expect(exec.exec).toHaveBeenCalledWith( + expect.stringContaining('tar'), + undefined, + expect.objectContaining({ + cwd: expect.any(String), + env: expect.any(Object) + }) + ) + }) + + it('Creates tar with platform-specific args on macOS', async () => { + // Mock the process.platform to be darwin to test the platform-specific code path + const originalPlatform = Object.getOwnPropertyDescriptor( + process, + 'platform' + ) + Object.defineProperty(process, 'platform', { + value: 'darwin', + configurable: true + }) + + // Mock gtar being available to trigger GNU tar type + // The first call to which('gtar', false) should return the gtar path + // The subsequent calls to which('tar', true) can return regular tar + io.which.mockImplementation(((cmd: string) => { + if (cmd === 'gtar') { + return Promise.resolve('/usr/local/bin/gtar') + } + return Promise.resolve('/usr/bin/tar') + }) as never) + + const sourceDirectories = ['src/'] + await createTar( + '/path/to/archive.tar.gz', + sourceDirectories, + CompressionMethod.Gzip + ) + + expect(exec.exec).toHaveBeenCalledWith( + expect.stringContaining('--delay-directory-restore'), + undefined, + expect.objectContaining({ + env: expect.any(Object) + }) + ) + + // Restore original platform + if (originalPlatform) + Object.defineProperty(process, 'platform', originalPlatform) + }) + }) +}) diff --git a/docs/supported-functionality.md b/docs/supported-functionality.md index 066a3e0..9a3e6bf 100644 --- a/docs/supported-functionality.md +++ b/docs/supported-functionality.md @@ -37,6 +37,34 @@ to the `local-action` command. > workflow runs (e.g. if you try to download an artifact from a different > repository), these requests **will be passed to the GitHub API**. +## [`@actions/cache`](https://github.com/actions/toolkit/tree/main/packages/cache) + +The stubbed version of `@actions/cache` functions similarly to the real package. +However, any caches that are created as part of a `local-action` run will be +stored on your local workstation. The specific path must be set using the +`LOCAL_ACTION_CACHE_PATH` environment variable in the `.env` file passed to the +`local-action` command. + +- Since this is a local cache, the GitHub Actions cache limits (e.g. size, + retention) do not apply. However, the stubbed version does enforce that the + `key` and `restoreKeys` parameters are unique per call to `saveCache()`. +- The current implementation of the `@actions/cache` package checks whether to + use `v1` or `v2` of the caching service. This is ignored in the local + implementation. +- Additionally, since no actual caches are downloaded/uploaded, concepts like + chunking, concurrency, and timeouts no longer apply. Because of this, most + properties in the `options` parameter of `restoreCache()` and `saveCache()` + are ignored. +- Any upload/download progress tracking is ignored. E.g. the `DownloadProgress` + and `UploadProgress` classes are not implemented. + +| Feature | Supported | Notes | +| ------------------ | ------------------ | ------------------------------------------ | +| `restoreCache()` | :white_check_mark: | Only the `lookupOnly` option is respected. | +| `saveCache()` | :white_check_mark: | All options are ignored | +| `DownloadProgress` | :x: | | +| `UploadProgress` | :x: | | + ## [`@actions/core`](https://github.com/actions/toolkit/blob/main/packages/core/README.md) | Feature | Supported | Notes | @@ -82,9 +110,6 @@ be `undefined`. For more information, see The following packages are under investigation for how to integrate with `local-action`. Make sure to check back later! -- [`@actions/attest`](https://github.com/actions/toolkit/tree/main/packages/attest) -- [`@actions/cache`](https://github.com/actions/toolkit/tree/main/packages/cache) - ## No Action Needed Currently, there shouldn't be any need to stub the functionality of the @@ -93,6 +118,7 @@ expected when run using `local-action`. If you do encounter a scenario where this doesn't work correctly, please [open an issue!](https://github.com/github/local-action/issues/new) +- [`@actions/attest`](https://github.com/actions/toolkit/tree/main/packages/attest) - [`@actions/exec`](https://github.com/actions/toolkit/tree/main/packages/exec) - [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) - [`@actions/http-client`](https://github.com/actions/toolkit/tree/main/packages/http-client) diff --git a/package-lock.json b/package-lock.json index 271915d..d4da80a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,22 @@ { "name": "@github/local-action", - "version": "5.2.0", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@github/local-action", - "version": "5.2.0", + "version": "6.0.0", "license": "MIT", "dependencies": { "@actions/artifact": "^2.3.2", + "@actions/cache": "^4.0.5", "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", - "@actions/github": "^6.0.0", + "@actions/github": "^6.0.1", "@actions/http-client": "^2.2.3", "@eslint/compat": "^1.2.8", - "@octokit/core": "^6.1.5", + "@octokit/core": "^7.0.3", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0", @@ -28,6 +29,7 @@ "dotenv": "^17.0.0", "figlet": "^1.8.1", "quibble": "^0.9.2", + "semver": "^7.7.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.19.3", "typescript": "^5.6.3", @@ -44,6 +46,7 @@ "@types/figlet": "^1.7.0", "@types/jest": "^29.5.14", "@types/node": "^22.14.1", + "@types/semver": "^7.7.1", "@types/unzip-stream": "^0.3.4", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", @@ -192,6 +195,45 @@ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "license": "ISC" }, + "node_modules/@actions/cache": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@actions/cache/-/cache-4.0.5.tgz", + "integrity": "sha512-RjLz1/vvntOfp3FpkY3wB0MjVRbLq7bfQEuQG9UUTKwdtcYmFrKVmuD+9B6ADbzbkSfHM+dM4sMjdr3R4XIkFg==", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/exec": "^1.0.1", + "@actions/glob": "^0.1.0", + "@actions/http-client": "^2.1.1", + "@actions/io": "^1.0.1", + "@azure/abort-controller": "^1.1.0", + "@azure/ms-rest-js": "^2.6.0", + "@azure/storage-blob": "^12.13.0", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "semver": "^6.3.1" + } + }, + "node_modules/@actions/cache/node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@actions/cache/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@actions/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", @@ -215,6 +257,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", @@ -235,15 +278,15 @@ } }, "node_modules/@actions/github/node_modules/@octokit/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", - "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" @@ -253,12 +296,12 @@ } }, "node_modules/@actions/github/node_modules/@octokit/graphql": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", - "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", "license": "MIT", "dependencies": { - "@octokit/request": "^8.3.0", + "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" }, @@ -306,6 +349,7 @@ "version": "5.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -319,6 +363,38 @@ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "license": "ISC" }, + "node_modules/@actions/glob": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.2.tgz", + "integrity": "sha512-SclLR7Ia5sEqjkJTPs7Sd86maMDw43p769YxBOxvPvEWuPEhpAnBsQfENOpXjFYMmhCqd127bmf+YdvJqVqR4A==", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.2.6", + "minimatch": "^3.0.4" + } + }, + "node_modules/@actions/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@actions/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@actions/http-client": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", @@ -515,6 +591,28 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/ms-rest-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.7.0.tgz", + "integrity": "sha512-ngbzWbqF+NmztDOpLBVDxYM+XLcUj7nKhxGbSU9WtIsXfRB//cf2ZbAG5HkOrhU9/wd/ORRB6lM/d69RKVjiyA==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.1.4", + "abort-controller": "^3.0.0", + "form-data": "^2.5.0", + "node-fetch": "^2.6.7", + "tslib": "^1.10.0", + "tunnel": "0.0.6", + "uuid": "^8.3.2", + "xml2js": "^0.5.0" + } + }, + "node_modules/@azure/ms-rest-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@azure/storage-blob": { "version": "12.27.0", "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.27.0.tgz", @@ -2585,30 +2683,30 @@ } }, "node_modules/@octokit/auth-token": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", - "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", + "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", "license": "MIT", "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", + "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { @@ -2655,38 +2753,39 @@ } }, "node_modules/@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", "license": "MIT", "dependencies": { - "@octokit/request": "^9.2.3", + "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/openapi-types": "^25.1.0" } }, "node_modules/@octokit/openapi-types": { "version": "24.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "13.0.1", @@ -2862,72 +2961,11 @@ "node": ">= 20" } }, - "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/core": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", - "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT" - }, - "node_modules/@octokit/rest/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/rest/node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "license": "Apache-2.0" - }, "node_modules/@octokit/types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { "@octokit/openapi-types": "^24.2.0" } @@ -3017,18 +3055,18 @@ } }, "node_modules/@protobuf-ts/runtime": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.9.6.tgz", - "integrity": "sha512-C0CfpKx4n4LBbUrajOdRj2BTbd3qBoK0SiKWLq7RgCoU6xiN4wesBMFHUOBp3fFzKeZwgU8Q2KtzaqzIvPLRXg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@protobuf-ts/runtime-rpc": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.9.6.tgz", - "integrity": "sha512-0UeqDRzNxgsh08lY5eWzFJNfD3gZ8Xf+WG1HzbIAbVAigzigwjwsYNNhTeas5H3gco1U5owTzCg177aambKOOw==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", "license": "Apache-2.0", "dependencies": { - "@protobuf-ts/runtime": "^2.9.6" + "@protobuf-ts/runtime": "^2.11.1" } }, "node_modules/@rtsao/scc": { @@ -3252,6 +3290,13 @@ "@types/node": "*" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -4184,6 +4229,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4398,9 +4449,9 @@ "license": "MIT" }, "node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, "node_modules/binary": { @@ -4569,7 +4620,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -4743,6 +4793,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", @@ -4782,7 +4844,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -5021,6 +5082,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -5105,7 +5175,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5163,6 +5232,17 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5245,7 +5325,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5254,7 +5333,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5264,7 +5342,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -5276,7 +5353,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -6238,12 +6314,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -6265,6 +6341,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6348,7 +6441,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -6382,7 +6474,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -6544,7 +6635,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6651,7 +6741,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6663,7 +6752,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6729,6 +6817,20 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8882,7 +8984,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8918,6 +9019,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -9012,6 +9134,26 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10336,11 +10478,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -10945,6 +11101,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -11407,7 +11569,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -11505,6 +11666,22 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11710,6 +11887,28 @@ "dev": true, "license": "MIT" }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 7dbe997..7cbbe5a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@github/local-action", "description": "Local Debugging for GitHub Actions", - "version": "5.2.0", + "version": "6.0.0", "type": "module", "author": "Nick Alteen ", "private": false, @@ -43,12 +43,13 @@ }, "dependencies": { "@actions/artifact": "^2.3.2", + "@actions/cache": "^4.0.5", "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", - "@actions/github": "^6.0.0", + "@actions/github": "^6.0.1", "@actions/http-client": "^2.2.3", "@eslint/compat": "^1.2.8", - "@octokit/core": "^6.1.5", + "@octokit/core": "^7.0.3", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0", @@ -61,6 +62,7 @@ "dotenv": "^17.0.0", "figlet": "^1.8.1", "quibble": "^0.9.2", + "semver": "^7.7.2", "tsconfig-paths": "^4.2.0", "tsx": "^4.19.3", "typescript": "^5.6.3", @@ -74,6 +76,7 @@ "@types/figlet": "^1.7.0", "@types/jest": "^29.5.14", "@types/node": "^22.14.1", + "@types/semver": "^7.7.1", "@types/unzip-stream": "^0.3.4", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", diff --git a/src/commands/run.ts b/src/commands/run.ts index 3d555e0..4b273fe 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -7,6 +7,7 @@ import * as quibble from 'quibble' import { loadConfig, register } from 'tsconfig-paths' import { fileURLToPath } from 'url' import { ARTIFACT_STUBS } from '../stubs/artifact/artifact.js' +import { CACHE_STUBS } from '../stubs/cache/cache.js' import { CORE_STUBS, CoreMeta } from '../stubs/core/core.js' import { EnvMeta } from '../stubs/env.js' import { Context } from '../stubs/github/context.js' @@ -131,6 +132,25 @@ export async function action( EnvMeta.inputs = actionYaml.inputs || {} EnvMeta.outputs = actionYaml.outputs || {} + // If the LOCAL_ACTION_CACHE_PATH env var is set, read it to populate the + // existing caches. + if ( + process.env.LOCAL_ACTION_CACHE_PATH && + process.env.LOCAL_ACTION_CACHE_PATH !== '' + ) { + const cacheFiles = fs + .readdirSync(process.env.LOCAL_ACTION_CACHE_PATH) + .filter(file => file.endsWith('.cache')) + + // The cache ID will be the next available ID based on existing caches. + let nextCacheId = 1 + + for (const file of cacheFiles) { + EnvMeta.caches[nextCacheId] = file + nextCacheId++ + } + } + // Output the action metadata printTitle(CoreMeta.colors.blue, 'Action Metadata') console.log() @@ -149,6 +169,16 @@ export async function action( ) console.log() + if (Object.keys(EnvMeta.caches).length > 0) { + console.table( + Object.keys(EnvMeta.caches).map(i => ({ + 'Cache ID': i, + Filename: EnvMeta.caches[parseInt(i)] + })) + ) + console.log() + } + // Defining the stubs. Next, we will load their paths based on the package // manager in use. const stubs = { @@ -157,6 +187,11 @@ export async function action( lib: ['lib', 'artifact.js'], stubs: ARTIFACT_STUBS }, + '@actions/cache': { + base: undefined as string | undefined, + lib: ['lib', 'cache.js'], + stubs: CACHE_STUBS + }, '@actions/core': { base: undefined as string | undefined, lib: ['lib', 'core.js'], @@ -340,13 +375,22 @@ export async function action( // is written in ESM. The stubs should only be loaded if the corresponding // package is installed. if (actionType === 'esm') { - if (stubs['@actions/github'].base) + if (stubs['@actions/artifact'].base) await quibble.default.esm( path.resolve( - stubs['@actions/github'].base, - ...stubs['@actions/github'].lib + stubs['@actions/artifact'].base, + ...stubs['@actions/artifact'].lib ), - stubs['@actions/github'].stubs + stubs['@actions/artifact'].stubs + ) + + if (stubs['@actions/cache'].base) + await quibble.default.esm( + path.resolve( + stubs['@actions/cache'].base, + ...stubs['@actions/cache'].lib + ), + stubs['@actions/cache'].stubs ) if (stubs['@actions/core'].base) @@ -358,13 +402,13 @@ export async function action( stubs['@actions/core'].stubs ) - if (stubs['@actions/artifact'].base) + if (stubs['@actions/github'].base) await quibble.default.esm( path.resolve( - stubs['@actions/artifact'].base, - ...stubs['@actions/artifact'].lib + stubs['@actions/github'].base, + ...stubs['@actions/github'].lib ), - stubs['@actions/artifact'].stubs + stubs['@actions/github'].stubs ) try { @@ -419,13 +463,22 @@ export async function action( replug(fs, packageJsonPath, Object.keys(stubs)) } } else { - if (stubs['@actions/github'].base) + if (stubs['@actions/artifact'].base) quibble.default( path.resolve( - stubs['@actions/github'].base, - ...stubs['@actions/github'].lib + stubs['@actions/artifact'].base, + ...stubs['@actions/artifact'].lib ), - stubs['@actions/github'].stubs + stubs['@actions/artifact'].stubs + ) + + if (stubs['@actions/cache'].base) + quibble.default( + path.resolve( + stubs['@actions/cache'].base, + ...stubs['@actions/cache'].lib + ), + stubs['@actions/cache'].stubs ) if (stubs['@actions/core'].base) @@ -437,13 +490,13 @@ export async function action( stubs['@actions/core'].stubs ) - if (stubs['@actions/artifact'].base) + if (stubs['@actions/github'].base) quibble.default( path.resolve( - stubs['@actions/artifact'].base, - ...stubs['@actions/artifact'].lib + stubs['@actions/github'].base, + ...stubs['@actions/github'].lib ), - stubs['@actions/artifact'].stubs + stubs['@actions/github'].stubs ) try { diff --git a/src/stubs/artifact/artifact.ts b/src/stubs/artifact/artifact.ts index 7c8c29d..7a02282 100644 --- a/src/stubs/artifact/artifact.ts +++ b/src/stubs/artifact/artifact.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/artifact.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/artifact.ts + * Last Reviewed Date: 2025-09-10 */ import { diff --git a/src/stubs/artifact/internal/client.ts b/src/stubs/artifact/internal/client.ts index 996701f..a95ba63 100644 --- a/src/stubs/artifact/internal/client.ts +++ b/src/stubs/artifact/internal/client.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/client.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/client.ts + * Last Reviewed Date: 2025-09-10 */ import { warning } from '../../core/core.js' @@ -123,7 +124,7 @@ export class DefaultArtifactClient implements ArtifactClient { * * @remarks * - * - Adds a check for the LOCAL_ACTION_ARTIFACT_PATH variable. + * - Adds a check for required environment variables. * * @param name Artifact Name * @param files File(s) to Upload @@ -137,6 +138,10 @@ export class DefaultArtifactClient implements ArtifactClient { rootDirectory: string, options?: UploadArtifactOptions ): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) if (!process.env.LOCAL_ACTION_ARTIFACT_PATH) throw new Error( 'LOCAL_ACTION_ARTIFACT_PATH must be set when interacting with @actions/artifact!' @@ -167,7 +172,7 @@ If the error persists, please check whether Actions is operating normally at [ht * * @remarks * - * - Adds a check for the LOCAL_ACTION_ARTIFACT_PATH variable. + * - Adds a check for required environment variables. * * @param artifactId Artifact ID * @param options Download Artifact Options @@ -177,6 +182,10 @@ If the error persists, please check whether Actions is operating normally at [ht artifactId: number, options?: DownloadArtifactOptions & FindOptions ): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) if (!process.env.LOCAL_ACTION_ARTIFACT_PATH) throw new Error( 'LOCAL_ACTION_ARTIFACT_PATH must be set when interacting with @actions/artifact!' @@ -222,7 +231,7 @@ If the error persists, please check whether Actions and API requests are operati * * @remarks * - * - Adds a check for the LOCAL_ACTION_ARTIFACT_PATH variable. + * - Adds a check for required environment variables. * * @param options Extra options that allow for the customization of the list behavior * @returns ListArtifactResponse object @@ -230,6 +239,10 @@ If the error persists, please check whether Actions and API requests are operati async listArtifacts( options?: ListArtifactsOptions & FindOptions ): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) if (!process.env.LOCAL_ACTION_ARTIFACT_PATH) throw new Error( 'LOCAL_ACTION_ARTIFACT_PATH must be set when interacting with @actions/artifact!' @@ -278,7 +291,7 @@ If the error persists, please check whether Actions and API requests are operati * * @remarks * - * - Adds a check for the LOCAL_ACTION_ARTIFACT_PATH variable. + * - Adds a check for required environment variables. * * @param artifactName Artifact Name * @param options Get Artifact Options @@ -288,6 +301,10 @@ If the error persists, please check whether Actions and API requests are operati artifactName: string, options?: FindOptions ): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) if (!process.env.LOCAL_ACTION_ARTIFACT_PATH) throw new Error( 'LOCAL_ACTION_ARTIFACT_PATH must be set when interacting with @actions/artifact!' @@ -331,7 +348,7 @@ If the error persists, please check whether Actions and API requests are operati * * @remarks * - * - Adds a check for the LOCAL_ACTION_ARTIFACT_PATH variable. + * - Adds a check for required environment variables. * * @param artifactName Artifact Name * @param options Delete Artifact Options @@ -341,6 +358,10 @@ If the error persists, please check whether Actions and API requests are operati artifactName: string, options?: FindOptions ): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/artifact!' + ) if (!process.env.LOCAL_ACTION_ARTIFACT_PATH) throw new Error( 'LOCAL_ACTION_ARTIFACT_PATH must be set when interacting with @actions/artifact!' diff --git a/src/stubs/artifact/internal/delete/delete-artifact.ts b/src/stubs/artifact/internal/delete/delete-artifact.ts index 9dabc46..459c635 100644 --- a/src/stubs/artifact/internal/delete/delete-artifact.ts +++ b/src/stubs/artifact/internal/delete/delete-artifact.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/delete/delete-artifact.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/delete/delete-artifact.ts + * Last Reviewed Date: 2025-09-10 */ import type { OctokitOptions } from '@octokit/core' diff --git a/src/stubs/artifact/internal/download/download-artifact.ts b/src/stubs/artifact/internal/download/download-artifact.ts index ff5c211..774f47e 100644 --- a/src/stubs/artifact/internal/download/download-artifact.ts +++ b/src/stubs/artifact/internal/download/download-artifact.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/download/download-artifact.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/download/download-artifact.ts + * Last Reviewed Date: 2025-09-10 */ import * as httpClient from '@actions/http-client' diff --git a/src/stubs/artifact/internal/find/get-artifact.ts b/src/stubs/artifact/internal/find/get-artifact.ts index d333127..1294e94 100644 --- a/src/stubs/artifact/internal/find/get-artifact.ts +++ b/src/stubs/artifact/internal/find/get-artifact.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/find/get-artifact.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/find/get-artifact.ts + * Last Reviewed Date: 2025-09-10 */ import type { OctokitOptions } from '@octokit/core' diff --git a/src/stubs/artifact/internal/find/list-artifacts.ts b/src/stubs/artifact/internal/find/list-artifacts.ts index 050db1b..6899900 100644 --- a/src/stubs/artifact/internal/find/list-artifacts.ts +++ b/src/stubs/artifact/internal/find/list-artifacts.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/find/list-artifacts.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/find/get-artifact.ts + * Last Reviewed Date: 2025-09-10 */ import type { OctokitOptions } from '@octokit/core' diff --git a/src/stubs/artifact/internal/find/retry-options.ts b/src/stubs/artifact/internal/find/retry-options.ts index a586ca9..33e3785 100644 --- a/src/stubs/artifact/internal/find/retry-options.ts +++ b/src/stubs/artifact/internal/find/retry-options.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/find/retry-options.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/find/retry-options.ts + * Last Reviewed Date: 2025-09-10 */ import type { OctokitOptions } from '@octokit/core' diff --git a/src/stubs/artifact/internal/shared/config.ts b/src/stubs/artifact/internal/shared/config.ts index 32379f9..084845e 100644 --- a/src/stubs/artifact/internal/shared/config.ts +++ b/src/stubs/artifact/internal/shared/config.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/shared/config.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/shared/config.ts + * Last Reviewed Date: 2025-09-10 */ /** diff --git a/src/stubs/artifact/internal/shared/errors.ts b/src/stubs/artifact/internal/shared/errors.ts index b91f82f..e5b450e 100644 --- a/src/stubs/artifact/internal/shared/errors.ts +++ b/src/stubs/artifact/internal/shared/errors.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/shared/errors.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/shared/errors.ts + * Last Reviewed Date: 2025-09-10 */ /** diff --git a/src/stubs/artifact/internal/shared/interfaces.ts b/src/stubs/artifact/internal/shared/interfaces.ts index 5ec23a4..edc3a42 100644 --- a/src/stubs/artifact/internal/shared/interfaces.ts +++ b/src/stubs/artifact/internal/shared/interfaces.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/shared/interfaces.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/shared/interfaces.ts + * Last Reviewed Date: 2025-09-10 */ /** diff --git a/src/stubs/artifact/internal/shared/user-agent.ts b/src/stubs/artifact/internal/shared/user-agent.ts index 04879b8..33af208 100644 --- a/src/stubs/artifact/internal/shared/user-agent.ts +++ b/src/stubs/artifact/internal/shared/user-agent.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/shared/user-agent.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/shared/user-agent.ts + * Last Reviewed Date: 2025-09-10 */ /** diff --git a/src/stubs/artifact/internal/upload/path-and-artifact-name-validation.ts b/src/stubs/artifact/internal/upload/path-and-artifact-name-validation.ts index 1629dbc..5b36677 100644 --- a/src/stubs/artifact/internal/upload/path-and-artifact-name-validation.ts +++ b/src/stubs/artifact/internal/upload/path-and-artifact-name-validation.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts + * Last Reviewed Date: 2025-09-10 */ import { info } from '../../../core/core.js' diff --git a/src/stubs/artifact/internal/upload/upload-artifact.ts b/src/stubs/artifact/internal/upload/upload-artifact.ts index f4fdeb2..b453633 100644 --- a/src/stubs/artifact/internal/upload/upload-artifact.ts +++ b/src/stubs/artifact/internal/upload/upload-artifact.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/upload/upload-artifact.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/upload/upload-artifact.ts + * Last Reviewed Date: 2025-09-10 */ import crypto from 'crypto' diff --git a/src/stubs/artifact/internal/upload/upload-zip-specification.ts b/src/stubs/artifact/internal/upload/upload-zip-specification.ts index 9e72b3d..6583ffa 100644 --- a/src/stubs/artifact/internal/upload/upload-zip-specification.ts +++ b/src/stubs/artifact/internal/upload/upload-zip-specification.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/upload/upload-zip-specification.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/upload/upload-zip-specification.ts + * Last Reviewed Date: 2025-09-10 */ import * as fs from 'fs' diff --git a/src/stubs/artifact/internal/upload/zip.ts b/src/stubs/artifact/internal/upload/zip.ts index b128b14..f052425 100644 --- a/src/stubs/artifact/internal/upload/zip.ts +++ b/src/stubs/artifact/internal/upload/zip.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/artifact/src/internal/upload/zip.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/artifact/src/internal/upload/zip.ts + * Last Reviewed Date: 2025-09-10 */ import archiver from 'archiver' diff --git a/src/stubs/cache/cache.ts b/src/stubs/cache/cache.ts new file mode 100644 index 0000000..29b9b18 --- /dev/null +++ b/src/stubs/cache/cache.ts @@ -0,0 +1,334 @@ +/** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/cache/src/internal/cache.ts + * Last Reviewed Date: 2025-09-10 + */ + +import * as core from '@actions/core' +import * as fs from 'fs' +import * as path from 'path' +import { EnvMeta } from '../env.js' +import { ReserveCacheError, ValidationError } from './errors.js' +import * as utils from './internal/cacheUtils.js' +import { createTar, extractTar, listTar } from './internal/tar.js' +import { DownloadOptions, UploadOptions } from './options.js' + +export const CACHE_STUBS = { + restoreCache, + saveCache +} + +/** + * Validates at least one path is provided. + * + * @param paths List of paths to check + */ +/* istanbul ignore next */ +function checkPaths(paths: string[]): void { + if (!paths || paths.length === 0) + throw new ValidationError( + `Path Validation Error: At least one directory or file path is required` + ) +} + +/** + * Validates the provided key is sufficient length and contains valid characters + * + * @param key Key to check + */ +/* istanbul ignore next */ +function checkKey(key: string): void { + if (key.length > 512) + throw new ValidationError( + `Key Validation Error: ${key} cannot be larger than 512 characters.` + ) + + const regex = /^[^,]*$/ + + if (!regex.test(key)) + throw new ValidationError( + `Key Validation Error: ${key} cannot contain commas.` + ) +} + +/** + * Restores cache from keys + * + * @param paths List of file paths to restore from the cache + * @param primaryKey Explicit key for restoring the cache. Lookup is done with prefix matching. + * @param restoreKeys Optional ordered list of keys to use for restoring the cache if no cache hit occurred for primaryKey + * @param downloadOptions Cache download options + * @param enableCrossOsArchive Optional boolean enabled to restore on Windows any cache created on any platform + * @returns Key for the cache hit, otherwise undefined + * + * @remarks + * + * - Checks for required environment variables. + * - Caches are only managed locally...removed check for service version. + */ +export async function restoreCache( + paths: string[], + primaryKey: string, + restoreKeys?: string[], + options?: DownloadOptions, + enableCrossOsArchive = false +): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!' + ) + if (!process.env.LOCAL_ACTION_CACHE_PATH) + throw new Error( + 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!' + ) + + checkPaths(paths) + + return await restoreCacheFromFilesystem( + paths, + primaryKey, + restoreKeys, + options, + enableCrossOsArchive + ) +} + +/** + * Restores cache from local filesystem + * + * @param paths List of file paths to restore from the cache + * @param primaryKey Explicit key for restoring the cache. Lookup is done with prefix matching + * @param restoreKeys Optional ordered list of keys to use for restoring the cache if no cache hit occurred for primaryKey + * @param downloadOptions Cache download options + * @param enableCrossOsArchive Optional boolean enabled to restore on Windows any cache created on any platform + * @returns Key for the cache hit, otherwise undefined + * + * @remarks + * + * - Replaces v1 and v2 caching implementations. + * - Download options, other than `lookupOnly`, are ignored. + */ +async function restoreCacheFromFilesystem( + paths: string[], + primaryKey: string, + restoreKeys?: string[], + options?: DownloadOptions, + /* istanbul ignore next */ + enableCrossOsArchive = false +): Promise { + const keys = [primaryKey, ...(restoreKeys ?? [])] + + core.debug('Resolved Keys:') + core.debug(JSON.stringify(keys)) + + if (keys.length > 10) + throw new ValidationError( + `Key Validation Error: Keys are limited to a maximum of 10.` + ) + + for (const key of keys) checkKey(key) + + let archivePath = '' + let matchedKey = undefined + let matchedCache = undefined + + try { + const compressionMethod = await utils.getCompressionMethod() + const cacheVersion = utils.getCacheVersion( + paths, + compressionMethod, + enableCrossOsArchive + ) + + const caches = fs + .readdirSync(process.env.LOCAL_ACTION_CACHE_PATH!) + .filter(file => file.endsWith('.cache')) + .map(file => file.replace('.cache', '')) + + // Check the caches for a matching key + for (const key of keys) { + const cache = caches.find(c => c === key || c.startsWith(key)) + + if (cache) { + matchedKey = key + matchedCache = cache + break + } + } + + if (matchedCache === undefined) { + core.debug( + `Cache not found for version ${cacheVersion} of keys: ${keys.join( + ', ' + )}` + ) + + return undefined + } + + if (primaryKey !== matchedKey) + core.info(`Cache hit for restore-key: ${matchedKey}`) + else core.info(`Cache hit for: ${matchedKey}`) + + if (options?.lookupOnly) { + core.info('Lookup only - skipping download') + return matchedKey + } + + archivePath = path.join( + process.env.LOCAL_ACTION_CACHE_PATH!, + `${matchedCache}.cache` + ) + core.debug(`Archive path: ${archivePath}`) + + const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath) + core.info( + `Cache Size: ~${Math.round( + archiveFileSize / (1024 * 1024) + )} MB (${archiveFileSize} B)` + ) + + if (core.isDebug()) await listTar(archivePath, compressionMethod) + await extractTar(archivePath, compressionMethod) + + core.info('Cache restored successfully') + return matchedKey + } catch (error: any) { + core.warning(`Failed to restore: ${error.message}`) + } + + return undefined +} + +/** + * Saves a list of files with the specified key + * + * @param paths List of file paths to be cached + * @param key Explicit key for restoring the cache + * @param enableCrossOsArchive Optional boolean enabled to save cache on Windows which could be restored on any platform + * @param options Cache upload options + * @returns Cache ID if the cache was saved successfully + * @throws Error if save fails + * + * @remarks + * + * - Checks for required environment variables. + * - Caches are only managed locally...removed check for service version. + */ +/* istanbul ignore next */ +export async function saveCache( + paths: string[], + key: string, + options?: UploadOptions, + enableCrossOsArchive = false +): Promise { + if (!process.env.LOCAL_ACTION_WORKSPACE) + throw new Error( + 'LOCAL_ACTION_WORKSPACE must be set when interacting with @actions/cache!' + ) + if (!process.env.LOCAL_ACTION_CACHE_PATH) + throw new Error( + 'LOCAL_ACTION_CACHE_PATH must be set when interacting with @actions/cache!' + ) + + checkPaths(paths) + checkKey(key) + + return await saveCacheToFilesystem(paths, key, options, enableCrossOsArchive) +} + +/** + * Saves cache to local filesystem + * + * @param paths List of file paths to be cached + * @param key Explicit key for restoring the cache + * @param options Cache upload options + * @param enableCrossOsArchive Optional boolean enabled to save cache on Windows which could be restored on any platform + * @returns Cache ID if the cache was saved successfully + * + * @remarks + * + * - Replaces v1 and v2 caching implementations. + * - Azure SDK settings and upload options are ignored. + * - Cache reserving is replaced with file conflict checks. + */ +async function saveCacheToFilesystem( + paths: string[], + key: string, + options?: UploadOptions, + /* istanbul ignore next */ + enableCrossOsArchive = false +): Promise { + const compressionMethod = await utils.getCompressionMethod() + const cachePaths = await utils.resolvePaths(paths) + + core.debug('Cache Paths:') + core.debug(`${JSON.stringify(cachePaths)}`) + + if (cachePaths.length === 0) + throw new Error( + `Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.` + ) + + const archiveFolder = await utils.createTempDirectory() + const archivePath = path.join( + archiveFolder, + utils.getCacheFileName(compressionMethod) + ) + + core.debug(`Archive Path: ${archivePath}`) + + try { + await createTar(archiveFolder, cachePaths, compressionMethod) + + if (core.isDebug()) await listTar(archivePath, compressionMethod) + + const archiveSizeBytes = utils.getArchiveFileSizeInBytes(archivePath) + core.debug(`File Size: ${archiveSizeBytes}`) + + // Attempt to reserve the cache. In local-action, this is done by checking + // if there is an existing file with the same name. If so, we fail the save. + core.debug('Reserving Cache') + const version = utils.getCacheVersion( + paths, + compressionMethod, + enableCrossOsArchive + ) + + if ( + fs.existsSync( + path.join( + process.env.LOCAL_ACTION_CACHE_PATH!, + `${key}-${version}.cache` + ) + ) + ) + throw new ReserveCacheError( + `Unable to reserve cache with key ${key}, another job may be creating this cache.` + ) + + // Copy the archive to the cache folder. In local-action, this is simply + // copying the file. + core.debug(`Attempting to upload cache located at: ${archivePath}`) + fs.copyFileSync( + archivePath, + path.join(process.env.LOCAL_ACTION_CACHE_PATH!, `${key}-${version}.cache`) + ) + + // Generate a cache ID. This can just be the next available ID based on + // existing caches. + return ( + Math.max(...Object.keys(EnvMeta.caches).map(key => parseInt(key))) + 1 + ) + } catch (error: any) { + core.warning(`Failed to save: ${error.message}`) + } finally { + // Try to delete the original archive to save space + try { + await utils.unlinkFile(archivePath) + } catch (error) { + core.debug(`Failed to delete archive: ${error}`) + } + } + + return -1 +} diff --git a/src/stubs/cache/errors.ts b/src/stubs/cache/errors.ts new file mode 100644 index 0000000..52e4f35 --- /dev/null +++ b/src/stubs/cache/errors.ts @@ -0,0 +1,31 @@ +/** + * Caching validation error + */ +export class ValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'ValidationError' + Object.setPrototypeOf(this, ValidationError.prototype) + } +} + +/** + * Cache reservation error + */ +export class ReserveCacheError extends Error { + constructor(message: string) { + super(message) + this.name = 'ReserveCacheError' + Object.setPrototypeOf(this, ReserveCacheError.prototype) + } +} + +/** + * Invalid response error + */ +export class InvalidResponseError extends Error { + constructor(message: string) { + super(message) + this.name = 'InvalidResponseError' + } +} diff --git a/src/stubs/cache/interfaces.ts b/src/stubs/cache/interfaces.ts new file mode 100644 index 0000000..70d1e45 --- /dev/null +++ b/src/stubs/cache/interfaces.ts @@ -0,0 +1,4 @@ +export interface ArchiveTool { + path: string + type: string +} diff --git a/src/stubs/cache/internal/cacheUtils.ts b/src/stubs/cache/internal/cacheUtils.ts new file mode 100644 index 0000000..6bf8904 --- /dev/null +++ b/src/stubs/cache/internal/cacheUtils.ts @@ -0,0 +1,207 @@ +/** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/main/packages/cache/src/internal/cacheUtils.ts + * Last Reviewed Date: 2025-09-10 + * + * @remarks + * + * - Deleted assertDefined function + * - Deleted getRuntimeToken function + */ +import * as core from '@actions/core' +import * as exec from '@actions/exec' +import * as glob from '@actions/glob' +import * as io from '@actions/io' +import * as crypto from 'crypto' +import * as fs from 'fs' +import * as path from 'path' +import * as semver from 'semver' +import * as util from 'util' +import { + CacheFilename, + CompressionMethod, + GnuTarPathOnWindows +} from './constants.js' + +const versionSalt = '1.0' + +/** + * Creates a temporary directory and returns the path + * + * @returns Path to the temporary directory + * + * @remarks + * + * - Replaces location with LOCAL_ACTION_WORKSPACE/actions/temp + */ +export async function createTempDirectory(): Promise { + const tempDirectory = path.join( + process.env.LOCAL_ACTION_WORKSPACE!, + 'actions', + 'temp' + ) + const dest = path.join(tempDirectory, crypto.randomUUID()) + + await io.mkdirP(dest) + + return dest +} + +/** + * Gets the size of the archive file in bytes + * + * @param filePath Path to the archive file + * @returns Size of the archive file in bytes + */ +/* istanbul ignore next */ +export function getArchiveFileSizeInBytes(filePath: string): number { + return fs.statSync(filePath).size +} + +/** + * Resolves the paths to be cached + * + * @param patterns Array of glob patterns + * @returns Array of resolved paths + */ +/* istanbul ignore next */ +export async function resolvePaths(patterns: string[]): Promise { + const paths: string[] = [] + const workspace = process.env['GITHUB_WORKSPACE'] ?? process.cwd() + const globber = await glob.create(patterns.join('\n'), { + implicitDescendants: false + }) + + for await (const file of globber.globGenerator()) { + const relativeFile = path + .relative(workspace, file) + .replace(new RegExp(`\\${path.sep}`, 'g'), '/') + core.debug(`Matched: ${relativeFile}`) + + // Paths are made relative so the tar entries are all relative to the root + // of the workspace. + if (relativeFile === '') paths.push('.') + else paths.push(`${relativeFile}`) + } + + return paths +} + +/** + * Deletes a file + * + * @param filePath Path to the file + * @returns Promise that resolves when the file is deleted + */ +/* istanbul ignore next */ +export async function unlinkFile(filePath: fs.PathLike): Promise { + return util.promisify(fs.unlink)(filePath) +} + +/** + * Gets the version of an application by executing it with the provided + * arguments + * + * @param app Application to get the version of + * @param additionalArgs Additional arguments to pass to the application + * @returns Version of the application + */ +/* istanbul ignore next */ +async function getVersion( + app: string, + additionalArgs: string[] = [] +): Promise { + let versionOutput = '' + additionalArgs.push('--version') + core.debug(`Checking ${app} ${additionalArgs.join(' ')}`) + + try { + await exec.exec(`${app}`, additionalArgs, { + ignoreReturnCode: true, + silent: true, + listeners: { + stdout: (data: Buffer): string => (versionOutput += data.toString()), + stderr: (data: Buffer): string => (versionOutput += data.toString()) + } + }) + } catch (err: any) { + core.debug(err.message) + } + + versionOutput = versionOutput.trim() + core.debug(versionOutput) + + return versionOutput +} + +/** + * Gets the compression method to use when creating caches. + * + * Use zstandard if possible to maximize cache performance. + */ +/* istanbul ignore next */ +export async function getCompressionMethod(): Promise { + const versionOutput = await getVersion('zstd', ['--quiet']) + const version = semver.clean(versionOutput) + + core.debug(`zstd version: ${version}`) + + if (versionOutput === '') return CompressionMethod.Gzip + else return CompressionMethod.ZstdWithoutLong +} + +/** + * Gets the cache file name based on the compression method + * + * @param compressionMethod Compression method + * @returns Cache file name + */ +/* istanbul ignore next */ +export function getCacheFileName(compressionMethod: CompressionMethod): string { + return compressionMethod === CompressionMethod.Gzip + ? CacheFilename.Gzip + : CacheFilename.Zstd +} + +/** + * Gets the path to GNU tar on Windows, if available + * + * @returns Path to GNU tar on Windows, or empty string if not available + */ +/* istanbul ignore next */ +export async function getGnuTarPathOnWindows(): Promise { + if (fs.existsSync(GnuTarPathOnWindows)) return GnuTarPathOnWindows + + const versionOutput = await getVersion('tar') + return versionOutput.toLowerCase().includes('gnu tar') ? io.which('tar') : '' +} + +/** + * Get the cache version + * + * @param paths Paths included in the cache + * @param compressionMethod Compression method + * @param enableCrossOsArchive Enable cross OS archive + * @returns Cache version + */ +/* istanbul ignore next */ +export function getCacheVersion( + paths: string[], + compressionMethod?: CompressionMethod, + enableCrossOsArchive = false +): string { + // don't pass changes upstream + const components = paths.slice() + + // Add compression method to cache version to restore + // compressed cache as per compression method + if (compressionMethod) components.push(compressionMethod) + + // Only check for windows platforms if enableCrossOsArchive is false + if (process.platform === 'win32' && !enableCrossOsArchive) + components.push('windows-only') + + // Add salt to cache version to support breaking changes in cache entry + components.push(versionSalt) + + return crypto.createHash('sha256').update(components.join('|')).digest('hex') +} diff --git a/src/stubs/cache/internal/constants.ts b/src/stubs/cache/internal/constants.ts new file mode 100644 index 0000000..8405e79 --- /dev/null +++ b/src/stubs/cache/internal/constants.ts @@ -0,0 +1,45 @@ +/** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/main/packages/cache/src/internal/constants.ts + * Last Reviewed Date: 2025-09-10 + */ + +export enum CacheFilename { + Gzip = 'cache.tgz', + Zstd = 'cache.tzst' +} + +export enum CompressionMethod { + Gzip = 'gzip', + // Long range mode was added to zstd in v1.3.2. + // This enum is for earlier version of zstd that does not have --long support + ZstdWithoutLong = 'zstd-without-long', + Zstd = 'zstd' +} + +export enum ArchiveToolType { + GNU = 'gnu', + BSD = 'bsd' +} + +// The default number of retry attempts. +export const DefaultRetryAttempts = 2 + +// The default delay in milliseconds between retry attempts. +export const DefaultRetryDelay = 5000 + +// Socket timeout in milliseconds during download. If no traffic is received +// over the socket during this period, the socket is destroyed and the download +// is aborted. +export const SocketTimeout = 5000 + +// The default path of GNUtar on hosted Windows runners +export const GnuTarPathOnWindows = `${process.env['PROGRAMFILES']}\\Git\\usr\\bin\\tar.exe` + +// The default path of BSDtar on hosted Windows runners +export const SystemTarPathOnWindows = `${process.env['SYSTEMDRIVE']}\\Windows\\System32\\tar.exe` + +export const TarFilename = 'cache.tar' + +export const ManifestFilename = 'manifest.txt' + +export const CacheFileSizeLimit = 10 * Math.pow(1024, 3) // 10GiB per repository diff --git a/src/stubs/cache/internal/tar.ts b/src/stubs/cache/internal/tar.ts new file mode 100644 index 0000000..3bd4048 --- /dev/null +++ b/src/stubs/cache/internal/tar.ts @@ -0,0 +1,373 @@ +/** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/main/packages/cache/src/internal/tar.ts + * Last Reviewed Date: 2025-09-10 + */ + +import { exec } from '@actions/exec' +import * as io from '@actions/io' +import { existsSync, writeFileSync } from 'fs' +import * as path from 'path' +import { ArchiveTool } from '../interfaces.js' +import * as utils from './cacheUtils.js' +import { + ArchiveToolType, + CompressionMethod, + ManifestFilename, + SystemTarPathOnWindows, + TarFilename +} from './constants.js' + +const IS_WINDOWS = process.platform === 'win32' + +/** + * Returns tar path and type: BSD or GNU + * + * @returns Path and type of tar + */ +/* istanbul ignore next */ +async function getTarPath(): Promise { + switch (process.platform) { + case 'win32': { + const gnuTar = await utils.getGnuTarPathOnWindows() + const systemTar = SystemTarPathOnWindows + if (gnuTar) { + // Use GNUtar as default on windows + return { path: gnuTar, type: ArchiveToolType.GNU } + } else if (existsSync(systemTar)) { + return { path: systemTar, type: ArchiveToolType.BSD } + } + break + } + case 'darwin': { + const gnuTar = await io.which('gtar', false) + if (gnuTar) { + // fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527 + return { path: gnuTar, type: ArchiveToolType.GNU } + } else { + return { + path: await io.which('tar', true), + type: ArchiveToolType.BSD + } + } + } + default: + break + } + // Default assumption is GNU tar is present in path + return { + path: await io.which('tar', true), + type: ArchiveToolType.GNU + } +} + +/** + * Return arguments for tar as per tarPath, compressionMethod, method type and os + * + * @param tarPath Path and type of tar + * @param compressionMethod Compression method + * @param type Type of operation: create, extract, list + * @param archivePath Archive path for extract and list operations + * @returns List of arguments for tar command + * + * @remarks + * + * - Made archivePath required for create operation + * - Removed exclude statement for creation since it happens in a different path + */ +async function getTarArgs( + tarPath: ArchiveTool, + compressionMethod: CompressionMethod, + type: string, + archivePath: string +): Promise { + const args = [`"${tarPath.path}"`] + const cacheFileName = utils.getCacheFileName(compressionMethod) + const tarFile = 'cache.tar' + const workingDirectory = getWorkingDirectory() + + // Speficic args for BSD tar on windows for workaround + /* istanbul ignore next */ + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS + + // Method specific args + switch (type) { + case 'create': + args.push( + '--posix', + '-cf', + /* istanbul ignore next */ + BSD_TAR_ZSTD + ? tarFile + : path + .join(archivePath, cacheFileName) + .replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P', + '-C', + workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '--files-from', + path.join(process.env.LOCAL_ACTION_WORKSPACE!, ManifestFilename) + ) + break + case 'extract': + args.push( + '-xf', + /* istanbul ignore next */ + BSD_TAR_ZSTD + ? tarFile + : archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P', + '-C', + workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/') + ) + break + case 'list': + args.push( + '-tf', + /* istanbul ignore next */ + BSD_TAR_ZSTD + ? tarFile + : archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P' + ) + break + } + + // Platform specific args + /* istanbul ignore next */ + if (tarPath.type === ArchiveToolType.GNU) { + switch (process.platform) { + case 'win32': + args.push('--force-local') + break + case 'darwin': + args.push('--delay-directory-restore') + break + } + } + + return args +} + +/** + * Returns commands to run TAR and compression program + * + * @param compressionMethod Compression method + * @param type Type of operation: create, extract, list + * @param archivePath Archive path for create, extract, and list operations + * @returns List of commands to run + */ +/* istanbul ignore next */ +async function getCommands( + compressionMethod: CompressionMethod, + type: string, + archivePath = '' +): Promise { + let args + + const tarPath = await getTarPath() + const tarArgs = await getTarArgs( + tarPath, + compressionMethod, + type, + archivePath + ) + + const compressionArgs = + type !== 'create' + ? await getDecompressionProgram(tarPath, compressionMethod, archivePath) + : await getCompressionProgram(tarPath, compressionMethod) + + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS + + if (BSD_TAR_ZSTD && type !== 'create') + args = [[...compressionArgs].join(' '), [...tarArgs].join(' ')] + else args = [[...tarArgs].join(' '), [...compressionArgs].join(' ')] + + if (BSD_TAR_ZSTD) return args + + return [args.join(' ')] +} + +/** + * Gets the working directory (local or GitHub Actions) + * + * @returns Working directory + */ +/* istanbul ignore next */ +function getWorkingDirectory(): string { + return process.env.GITHUB_WORKSPACE ?? process.cwd() +} + +/** + * Common function for extractTar and listTar to get the compression method + * + * @param tarPath Path and type of tar + * @param compressionMethod Compression method + * @param archivePath Archive path for extract and list operations + * @returns List of arguments for decompression program + */ +/* istanbul ignore next */ +async function getDecompressionProgram( + tarPath: ArchiveTool, + compressionMethod: CompressionMethod, + archivePath: string +): Promise { + // -d: Decompress. + // unzstd is equivalent to 'zstd -d' + // --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit. + // Using 30 here because we also support 32-bit self-hosted runners. + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS + switch (compressionMethod) { + case CompressionMethod.Zstd: + return BSD_TAR_ZSTD + ? [ + 'zstd -d --long=30 --force -o', + TarFilename, + archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/') + ] + : [ + '--use-compress-program', + IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30' + ] + case CompressionMethod.ZstdWithoutLong: + return BSD_TAR_ZSTD + ? [ + 'zstd -d --force -o', + TarFilename, + archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/') + ] + : ['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd'] + default: + return ['-z'] + } +} + +/** + * Used for creating the archive + * + * -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores. + * zstdmt is equivalent to 'zstd -T0' + * --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit. + * Using 30 here because we also support 32-bit self-hosted runners. + * Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd. + */ +/* istanbul ignore next */ +async function getCompressionProgram( + tarPath: ArchiveTool, + compressionMethod: CompressionMethod +): Promise { + const cacheFileName = utils.getCacheFileName(compressionMethod) + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS + switch (compressionMethod) { + case CompressionMethod.Zstd: + return BSD_TAR_ZSTD + ? [ + 'zstd -T0 --long=30 --force -o', + cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + TarFilename + ] + : [ + '--use-compress-program', + IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30' + ] + case CompressionMethod.ZstdWithoutLong: + return BSD_TAR_ZSTD + ? [ + 'zstd -T0 --force -o', + cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + TarFilename + ] + : ['--use-compress-program', IS_WINDOWS ? '"zstd -T0"' : 'zstdmt'] + default: + return ['-z'] + } +} + +/** + * Executes all commands as separate processes + * + * @param commands List of commands to execute + * @param cwd Optional current working directory for the commands + */ +/* istanbul ignore next */ +async function execCommands(commands: string[], cwd?: string): Promise { + for (const command of commands) { + try { + await exec(command, undefined, { + cwd, + env: { ...(process.env as object), MSYS: 'winsymlinks:nativestrict' } + }) + } catch (error: any) { + throw new Error( + `${command.split(' ')[0]} failed with error: ${error?.message}` + ) + } + } +} + +/** + * List the contents of a tar + * + * @param archivePath Path to the archive to list + * @param compressionMethod Compression method to use + */ +/* istanbul ignore next */ +export async function listTar( + archivePath: string, + compressionMethod: CompressionMethod +): Promise { + const commands = await getCommands(compressionMethod, 'list', archivePath) + await execCommands(commands) +} + +/** + * Extracts a TAR + * + * @param archivePath Path to the archive to extract + * @param compressionMethod Compression method to use + */ +/* istanbul ignore next */ +export async function extractTar( + archivePath: string, + compressionMethod: CompressionMethod +): Promise { + // Create directory to extract tar into + const workingDirectory = getWorkingDirectory() + await io.mkdirP(workingDirectory) + const commands = await getCommands(compressionMethod, 'extract', archivePath) + await execCommands(commands) +} + +/** + * Creates a TAR archive from a list of directories + * + * @param archiveFolder The folder where the archive will be created + * @param sourceDirectories List of directories to include in the archive + * @param compressionMethod Compression method to use + */ +/* istanbul ignore next */ +export async function createTar( + archiveFolder: string, + sourceDirectories: string[], + compressionMethod: CompressionMethod +): Promise { + // Write source directories to manifest.txt to avoid command length limits + writeFileSync( + path.join(process.env.LOCAL_ACTION_WORKSPACE!, ManifestFilename), + sourceDirectories.join('\n') + ) + const commands = await getCommands(compressionMethod, 'create', archiveFolder) + await execCommands(commands, archiveFolder) +} diff --git a/src/stubs/cache/options.ts b/src/stubs/cache/options.ts new file mode 100644 index 0000000..95d7e45 --- /dev/null +++ b/src/stubs/cache/options.ts @@ -0,0 +1,91 @@ +/** + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/main/packages/cache/src/options.ts + * Last Reviewed Date: 2025-09-10 + * + * @remarks + * + * - Removed getUploadOptions function + * - Removed getDownloadOptions function + */ + +/** + * Options to control cache upload + */ +export interface UploadOptions { + /** + * Indicates whether to use the Azure Blob SDK to download caches + * that are stored on Azure Blob Storage to improve reliability and + * performance + * + * @default false + */ + useAzureSdk?: boolean + /** + * Number of parallel cache upload + * + * @default 4 + */ + uploadConcurrency?: number + /** + * Maximum chunk size in bytes for cache upload + * + * @default 32MB + */ + uploadChunkSize?: number + /** + * Archive size in bytes + */ + archiveSizeBytes?: number +} + +/** + * Options to control cache download + */ +export interface DownloadOptions { + /** + * Indicates whether to use the Azure Blob SDK to download caches + * that are stored on Azure Blob Storage to improve reliability and + * performance + * + * @default true + */ + useAzureSdk?: boolean + + /** + * Number of parallel downloads (this option only applies when using + * the Azure SDK) + * + * @default 8 + */ + downloadConcurrency?: number + + /** + * Indicates whether to use Actions HttpClient with concurrency + * for Azure Blob Storage + */ + concurrentBlobDownloads?: boolean + + /** + * Maximum time for each download request, in milliseconds (this + * option only applies when using the Azure SDK) + * + * @default 30000 + */ + timeoutInMs?: number + + /** + * Time after which a segment download should be aborted if stuck + * + * @default 3600000 + */ + segmentTimeoutInMs?: number + + /** + * Weather to skip downloading the cache entry. + * If lookupOnly is set to true, the restore function will only check if + * a matching cache entry exists and return the cache key if it does. + * + * @default false + */ + lookupOnly?: boolean +} diff --git a/src/stubs/core/core.ts b/src/stubs/core/core.ts index 139263c..8c275f4 100644 --- a/src/stubs/core/core.ts +++ b/src/stubs/core/core.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/core/src/core.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/core/src/core.ts + * Last Reviewed Date: 2025-09-10 * * @remarks * diff --git a/src/stubs/core/path-utils.ts b/src/stubs/core/path-utils.ts index afd67d0..89524e4 100644 --- a/src/stubs/core/path-utils.ts +++ b/src/stubs/core/path-utils.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/core/src/path-utils.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/core/src/path-utils.ts + * Last Reviewed Date: 2025-09-10 */ import * as path from 'path' diff --git a/src/stubs/core/platform.ts b/src/stubs/core/platform.ts index c48bc4e..680ff07 100644 --- a/src/stubs/core/platform.ts +++ b/src/stubs/core/platform.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/core/src/platform.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/core/src/platform.ts + * Last Reviewed Date: 2025-09-10 */ import * as exec from '@actions/exec' diff --git a/src/stubs/core/summary.ts b/src/stubs/core/summary.ts index 1a7c37c..840ed4c 100644 --- a/src/stubs/core/summary.ts +++ b/src/stubs/core/summary.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/core/src/summary.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/core/src/summary.ts + * Last Reviewed Date: 2025-09-10 */ import fs from 'fs' diff --git a/src/stubs/env.ts b/src/stubs/env.ts index 4c471cd..2b31a23 100644 --- a/src/stubs/env.ts +++ b/src/stubs/env.ts @@ -7,6 +7,7 @@ export const EnvMeta: EnvMetadata = { actionFile: '', actionPath: '', artifacts: [], + caches: {}, dotenvFile: '', entrypoint: '', env: {}, @@ -27,6 +28,7 @@ export function ResetEnvMetadata(): void { EnvMeta.actionFile = '' EnvMeta.actionPath = '' EnvMeta.artifacts = [] + EnvMeta.caches = {} EnvMeta.dotenvFile = '' EnvMeta.entrypoint = '' EnvMeta.env = {} diff --git a/src/stubs/github/context.ts b/src/stubs/github/context.ts index 4d1f8ab..1aa163d 100644 --- a/src/stubs/github/context.ts +++ b/src/stubs/github/context.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/github/src/context.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/github/src/context.ts + * Last Reviewed Date: 2025-09-10 */ import { existsSync, readFileSync } from 'fs' diff --git a/src/stubs/github/github.ts b/src/stubs/github/github.ts index 39052a6..e4804db 100644 --- a/src/stubs/github/github.ts +++ b/src/stubs/github/github.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/github/src/github.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/github/src/github.ts + * Last Reviewed Date: 2025-09-10 * * @remarks * diff --git a/src/stubs/github/interfaces.ts b/src/stubs/github/interfaces.ts index 4dab9c9..2585d46 100644 --- a/src/stubs/github/interfaces.ts +++ b/src/stubs/github/interfaces.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/github/src/interfaces.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/github/src/interfaces.ts + * Last Reviewed Date: 2025-09-10 */ /** diff --git a/src/stubs/github/internal/utils.ts b/src/stubs/github/internal/utils.ts index de61dca..e74cbfa 100644 --- a/src/stubs/github/internal/utils.ts +++ b/src/stubs/github/internal/utils.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/github/src/internal/utils.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/github/src/internal/utils.ts + * Last Reviewed Date: 2025-09-10 */ import * as httpClient from '@actions/http-client' diff --git a/src/stubs/github/utils.ts b/src/stubs/github/utils.ts index 9ecea16..e48316c 100644 --- a/src/stubs/github/utils.ts +++ b/src/stubs/github/utils.ts @@ -1,5 +1,6 @@ /** - * Last Reviewed Commit: https://github.com/actions/toolkit/blob/930c89072712a3aac52d74b23338f00bb0cfcb24/packages/github/src/utils.ts + * Last Reviewed Commit: https://github.com/actions/toolkit/blob/f58042f9cc16bcaa87afaa86c2974a8c771ce1ea/packages/github/src/utils.ts + * Last Reviewed Date: 2025-09-10 * * @remarks * diff --git a/src/types.ts b/src/types.ts index 8004c70..020511a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,8 +6,10 @@ export type EnvMetadata = { actionFile: string /** Path to Action Directory */ actionPath: string - /** Map of Action Artifacts */ + /** List of Action Artifacts */ artifacts: Artifact[] + /** Map of Cache IDs and Cache Names */ + caches: { [key: number]: string } /** Path to `.env` */ dotenvFile: string /** Environment Variables */