mirror of
https://code.forgejo.org/actions/cache.git
synced 2025-04-23 03:45:31 +00:00
Update for actions/cache@v3
This commit is contained in:
commit
9420979f7c
65 changed files with 173609 additions and 137441 deletions
|
@ -1,8 +1,11 @@
|
|||
export enum Inputs {
|
||||
Key = "key",
|
||||
Path = "path",
|
||||
RestoreKeys = "restore-keys",
|
||||
UploadChunkSize = "upload-chunk-size",
|
||||
Key = "key", // Input for cache, restore, save action
|
||||
Path = "path", // Input for cache, restore, save action
|
||||
RestoreKeys = "restore-keys", // Input for cache, restore action
|
||||
UploadChunkSize = "upload-chunk-size", // Input for cache, save action
|
||||
EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action
|
||||
FailOnCacheMiss = "fail-on-cache-miss", // Input for cache, restore action
|
||||
LookupOnly = "lookup-only", // Input for cache, restore action
|
||||
AWSS3Bucket = "aws-s3-bucket",
|
||||
AWSAccessKeyId = "aws-access-key-id",
|
||||
AWSSecretAccessKey = "aws-secret-access-key",
|
||||
|
@ -13,7 +16,9 @@ export enum Inputs {
|
|||
}
|
||||
|
||||
export enum Outputs {
|
||||
CacheHit = "cache-hit"
|
||||
CacheHit = "cache-hit", // Output from cache, restore action
|
||||
CachePrimaryKey = "cache-primary-key", // Output from restore action
|
||||
CacheMatchedKey = "cache-matched-key" // Output from restore action
|
||||
}
|
||||
|
||||
export enum State {
|
||||
|
|
|
@ -1,70 +1,3 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import { restoreRun } from "./restoreImpl";
|
||||
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
// Validate inputs, this can cause task failure
|
||||
if (!utils.isValidEvent()) {
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||
|
||||
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
const s3BucketName = core.getInput(Inputs.AWSS3Bucket);
|
||||
const s3config = utils.getInputS3ClientConfig();
|
||||
|
||||
try {
|
||||
const cacheKey = await cache.restoreCache(
|
||||
cachePaths,
|
||||
primaryKey,
|
||||
restoreKeys,
|
||||
undefined,
|
||||
s3config,
|
||||
s3BucketName
|
||||
);
|
||||
if (!cacheKey) {
|
||||
core.info(
|
||||
`Cache not found for input keys: ${[
|
||||
primaryKey,
|
||||
...restoreKeys
|
||||
].join(", ")}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the matched cache key
|
||||
utils.setCacheState(cacheKey);
|
||||
|
||||
const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey);
|
||||
utils.setCacheHitOutput(isExactKeyMatch);
|
||||
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
} catch (error) {
|
||||
if (error.name === cache.ValidationError.name) {
|
||||
throw error;
|
||||
} else {
|
||||
utils.logWarning(error.message);
|
||||
utils.setCacheHitOutput(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
export default run;
|
||||
restoreRun(true);
|
||||
|
|
127
src/restoreImpl.ts
Normal file
127
src/restoreImpl.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Events, Inputs, Outputs, State } from "./constants";
|
||||
import {
|
||||
IStateProvider,
|
||||
NullStateProvider,
|
||||
StateProvider
|
||||
} from "./stateProvider";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
export async function restoreImpl(
|
||||
stateProvider: IStateProvider
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
if (!utils.isCacheFeatureAvailable()) {
|
||||
core.setOutput(Outputs.CacheHit, "false");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate inputs, this can cause task failure
|
||||
if (!utils.isValidEvent()) {
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||
stateProvider.setState(State.CachePrimaryKey, primaryKey);
|
||||
|
||||
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
const s3BucketName = core.getInput(Inputs.AWSS3Bucket);
|
||||
const s3config = utils.getInputS3ClientConfig();
|
||||
|
||||
const enableCrossOsArchive = utils.getInputAsBool(
|
||||
Inputs.EnableCrossOsArchive
|
||||
);
|
||||
const failOnCacheMiss = utils.getInputAsBool(Inputs.FailOnCacheMiss);
|
||||
const lookupOnly = utils.getInputAsBool(Inputs.LookupOnly);
|
||||
|
||||
const cacheKey = await cache.restoreCache(
|
||||
cachePaths,
|
||||
primaryKey,
|
||||
restoreKeys,
|
||||
{ lookupOnly: lookupOnly },
|
||||
enableCrossOsArchive,
|
||||
s3BucketName,
|
||||
s3config
|
||||
);
|
||||
|
||||
if (!cacheKey) {
|
||||
if (failOnCacheMiss) {
|
||||
throw new Error(
|
||||
`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${primaryKey}`
|
||||
);
|
||||
}
|
||||
core.info(
|
||||
`Cache not found for input keys: ${[
|
||||
primaryKey,
|
||||
...restoreKeys
|
||||
].join(", ")}`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the matched cache key in states
|
||||
stateProvider.setState(State.CacheMatchedKey, cacheKey);
|
||||
|
||||
const isExactKeyMatch = utils.isExactKeyMatch(
|
||||
core.getInput(Inputs.Key, { required: true }),
|
||||
cacheKey
|
||||
);
|
||||
|
||||
core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString());
|
||||
if (lookupOnly) {
|
||||
core.info(`Cache found and can be restored from key: ${cacheKey}`);
|
||||
} else {
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
}
|
||||
|
||||
return cacheKey;
|
||||
} catch (error: unknown) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(
|
||||
stateProvider: IStateProvider,
|
||||
earlyExit: boolean | undefined
|
||||
): Promise<void> {
|
||||
try {
|
||||
await restoreImpl(stateProvider);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (earlyExit) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// node will stay alive if any promises are not resolved,
|
||||
// which is a possibility if HTTP requests are dangling
|
||||
// due to retries or timeouts. We know that if we got here
|
||||
// that all promises that we care about have successfully
|
||||
// resolved, so simply exit with success.
|
||||
if (earlyExit) {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreOnlyRun(
|
||||
earlyExit?: boolean | undefined
|
||||
): Promise<void> {
|
||||
await run(new NullStateProvider(), earlyExit);
|
||||
}
|
||||
|
||||
export async function restoreRun(
|
||||
earlyExit?: boolean | undefined
|
||||
): Promise<void> {
|
||||
await run(new StateProvider(), earlyExit);
|
||||
}
|
3
src/restoreOnly.ts
Normal file
3
src/restoreOnly.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { restoreOnlyRun } from "./restoreImpl";
|
||||
|
||||
restoreOnlyRun(true);
|
62
src/save.ts
62
src/save.ts
|
@ -1,64 +1,8 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||
// throw an uncaught exception. Instead of failing this action, just warn.
|
||||
process.on("uncaughtException", e => utils.logWarning(e.message));
|
||||
import saveImpl from "./saveImpl";
|
||||
import { StateProvider } from "./stateProvider";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
if (!utils.isValidEvent()) {
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = utils.getCacheState();
|
||||
|
||||
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||
if (!primaryKey) {
|
||||
utils.logWarning(`Error retrieving key from state.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (utils.isExactKeyMatch(primaryKey, state)) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
const s3BucketName = core.getInput(Inputs.AWSS3Bucket);
|
||||
const s3config = utils.getInputS3ClientConfig();
|
||||
|
||||
try {
|
||||
await cache.saveCache(cachePaths, primaryKey, {
|
||||
uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize)
|
||||
}, s3config, s3BucketName);
|
||||
core.info(`Cache saved with key: ${primaryKey}`);
|
||||
} catch (error) {
|
||||
if (error.name === cache.ValidationError.name) {
|
||||
throw error;
|
||||
} else if (error.name === cache.ReserveCacheError.name) {
|
||||
core.info(error.message);
|
||||
} else {
|
||||
utils.logWarning(error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
utils.logWarning(error.message);
|
||||
}
|
||||
await saveImpl(new StateProvider());
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
79
src/saveImpl.ts
Normal file
79
src/saveImpl.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import { IStateProvider } from "./stateProvider";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||
// throw an uncaught exception. Instead of failing this action, just warn.
|
||||
process.on("uncaughtException", e => utils.logWarning(e.message));
|
||||
|
||||
async function saveImpl(stateProvider: IStateProvider): Promise<number | void> {
|
||||
let cacheId = -1;
|
||||
try {
|
||||
if (!utils.isCacheFeatureAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.isValidEvent()) {
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If restore has stored a primary key in state, reuse that
|
||||
// Else re-evaluate from inputs
|
||||
const primaryKey =
|
||||
stateProvider.getState(State.CachePrimaryKey) ||
|
||||
core.getInput(Inputs.Key);
|
||||
|
||||
if (!primaryKey) {
|
||||
utils.logWarning(`Key is not specified.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If matched restore key is same as primary key, then do not save cache
|
||||
// NO-OP in case of SaveOnly action
|
||||
const restoredKey = stateProvider.getCacheState();
|
||||
|
||||
if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
|
||||
core.info(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
const s3BucketName = core.getInput(Inputs.AWSS3Bucket);
|
||||
const s3config = utils.getInputS3ClientConfig();
|
||||
|
||||
const enableCrossOsArchive = utils.getInputAsBool(
|
||||
Inputs.EnableCrossOsArchive
|
||||
);
|
||||
|
||||
cacheId = await cache.saveCache(
|
||||
cachePaths,
|
||||
primaryKey,
|
||||
{ uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize) },
|
||||
enableCrossOsArchive,
|
||||
s3config,
|
||||
s3BucketName
|
||||
);
|
||||
|
||||
if (cacheId != -1) {
|
||||
core.info(`Cache saved with key: ${primaryKey}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
utils.logWarning((error as Error).message);
|
||||
}
|
||||
return cacheId;
|
||||
}
|
||||
|
||||
export default saveImpl;
|
15
src/saveOnly.ts
Normal file
15
src/saveOnly.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import * as core from "@actions/core";
|
||||
|
||||
import saveImpl from "./saveImpl";
|
||||
import { NullStateProvider } from "./stateProvider";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const cacheId = await saveImpl(new NullStateProvider());
|
||||
if (cacheId === -1) {
|
||||
core.warning(`Cache save failed.`);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
export default run;
|
46
src/stateProvider.ts
Normal file
46
src/stateProvider.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as core from "@actions/core";
|
||||
|
||||
import { Outputs, State } from "./constants";
|
||||
|
||||
export interface IStateProvider {
|
||||
setState(key: string, value: string): void;
|
||||
getState(key: string): string;
|
||||
|
||||
getCacheState(): string | undefined;
|
||||
}
|
||||
|
||||
class StateProviderBase implements IStateProvider {
|
||||
getCacheState(): string | undefined {
|
||||
const cacheKey = this.getState(State.CacheMatchedKey);
|
||||
if (cacheKey) {
|
||||
core.debug(`Cache state/key: ${cacheKey}`);
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
setState = (key: string, value: string) => {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getState = (key: string) => "";
|
||||
}
|
||||
|
||||
export class StateProvider extends StateProviderBase {
|
||||
setState = core.saveState;
|
||||
getState = core.getState;
|
||||
}
|
||||
|
||||
export class NullStateProvider extends StateProviderBase {
|
||||
stateToOutputMap = new Map<string, string>([
|
||||
[State.CacheMatchedKey, Outputs.CacheMatchedKey],
|
||||
[State.CachePrimaryKey, Outputs.CachePrimaryKey]
|
||||
]);
|
||||
|
||||
setState = (key: string, value: string) => {
|
||||
core.setOutput(this.stateToOutputMap.get(key) as string, value);
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getState = (key: string) => "";
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Inputs, Outputs, RefKey, State } from "../constants";
|
||||
|
@ -20,30 +21,6 @@ export function isExactKeyMatch(key: string, cacheKey?: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function setCacheState(state: string): void {
|
||||
core.saveState(State.CacheMatchedKey, state);
|
||||
}
|
||||
|
||||
export function setCacheHitOutput(isCacheHit: boolean): void {
|
||||
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
|
||||
}
|
||||
|
||||
export function setOutputAndState(key: string, cacheKey?: string): void {
|
||||
setCacheHitOutput(isExactKeyMatch(key, cacheKey));
|
||||
// Store the matched cache key if it exists
|
||||
cacheKey && setCacheState(cacheKey);
|
||||
}
|
||||
|
||||
export function getCacheState(): string | undefined {
|
||||
const cacheKey = core.getState(State.CacheMatchedKey);
|
||||
if (cacheKey) {
|
||||
core.debug(`Cache state/key: ${cacheKey}`);
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function logWarning(message: string): void {
|
||||
const warningPrefix = "[warning]";
|
||||
core.info(`${warningPrefix}${message}`);
|
||||
|
@ -62,7 +39,7 @@ export function getInputAsArray(
|
|||
return core
|
||||
.getInput(name, options)
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.map(s => s.replace(/^!\s+/, "!").trim())
|
||||
.filter(x => x !== "");
|
||||
}
|
||||
|
||||
|
@ -77,6 +54,33 @@ export function getInputAsInt(
|
|||
return value;
|
||||
}
|
||||
|
||||
export function getInputAsBool(
|
||||
name: string,
|
||||
options?: core.InputOptions
|
||||
): boolean {
|
||||
const result = core.getInput(name, options);
|
||||
return result.toLowerCase() === "true";
|
||||
}
|
||||
|
||||
export function isCacheFeatureAvailable(): boolean {
|
||||
if (cache.isFeatureAvailable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isGhes()) {
|
||||
logWarning(
|
||||
`Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.
|
||||
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logWarning(
|
||||
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getInputS3ClientConfig(): S3ClientConfig | undefined {
|
||||
const s3BucketName = core.getInput(Inputs.AWSS3Bucket)
|
||||
if (!s3BucketName) {
|
||||
|
|
|
@ -13,6 +13,9 @@ interface CacheInput {
|
|||
path: string;
|
||||
key: string;
|
||||
restoreKeys?: string[];
|
||||
enableCrossOsArchive?: boolean;
|
||||
failOnCacheMiss?: boolean;
|
||||
lookupOnly?: boolean;
|
||||
}
|
||||
|
||||
export function setInputs(input: CacheInput): void {
|
||||
|
@ -20,6 +23,15 @@ export function setInputs(input: CacheInput): void {
|
|||
setInput(Inputs.Key, input.key);
|
||||
input.restoreKeys &&
|
||||
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
||||
input.enableCrossOsArchive !== undefined &&
|
||||
setInput(
|
||||
Inputs.EnableCrossOsArchive,
|
||||
input.enableCrossOsArchive.toString()
|
||||
);
|
||||
input.failOnCacheMiss !== undefined &&
|
||||
setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString());
|
||||
input.lookupOnly !== undefined &&
|
||||
setInput(Inputs.LookupOnly, input.lookupOnly.toString());
|
||||
}
|
||||
|
||||
export function clearInputs(): void {
|
||||
|
@ -27,4 +39,7 @@ export function clearInputs(): void {
|
|||
delete process.env[getInputName(Inputs.Key)];
|
||||
delete process.env[getInputName(Inputs.RestoreKeys)];
|
||||
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
||||
delete process.env[getInputName(Inputs.EnableCrossOsArchive)];
|
||||
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
|
||||
delete process.env[getInputName(Inputs.LookupOnly)];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue