diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ad830b5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "Node.js & TypeScript", + "image": "mcr.microsoft.com/devcontainers/typescript-node:16-bullseye", + // Features to add to the dev container. More info: https://containers.dev/implementors/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install && npm run build" + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/action.yml b/action.yml index 3e158e3..4524247 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,14 @@ inputs: upload-chunk-size: description: 'The chunk size used to split up large files during upload, in bytes' required: false + exit-on-cache-miss: + description: 'Fail the workflow if the cache is not found for the primary key' + required: false + default: false + save-on-any-failure: + description: 'Save cache (on cache miss) despite of any failure during the workflow run' + required: false + default: false outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' @@ -21,7 +29,7 @@ runs: using: 'node16' main: 'dist/restore/index.js' post: 'dist/save/index.js' - post-if: 'success()' + post-if: (success() || (env.SAVE_CACHE_ON_ANY_FAILURE == 'yes')) branding: icon: 'archive' color: 'gray-dark' diff --git a/dist/restore/index.js b/dist/restore/index.js index a840efa..b5ed08c 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -1892,10 +1892,10 @@ function serial(list, iterator, callback) module.exports = minimatch minimatch.Minimatch = Minimatch -var path = { sep: '/' } -try { - path = __webpack_require__(622) -} catch (er) {} +var path = (function () { try { return __webpack_require__(622) } catch (e) {}}()) || { + sep: '/' +} +minimatch.sep = path.sep var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} var expand = __webpack_require__(306) @@ -1947,43 +1947,64 @@ function filter (pattern, options) { } function ext (a, b) { - a = a || {} b = b || {} var t = {} - Object.keys(b).forEach(function (k) { - t[k] = b[k] - }) Object.keys(a).forEach(function (k) { t[k] = a[k] }) + Object.keys(b).forEach(function (k) { + t[k] = b[k] + }) return t } minimatch.defaults = function (def) { - if (!def || !Object.keys(def).length) return minimatch + if (!def || typeof def !== 'object' || !Object.keys(def).length) { + return minimatch + } var orig = minimatch var m = function minimatch (p, pattern, options) { - return orig.minimatch(p, pattern, ext(def, options)) + return orig(p, pattern, ext(def, options)) } m.Minimatch = function Minimatch (pattern, options) { return new orig.Minimatch(pattern, ext(def, options)) } + m.Minimatch.defaults = function defaults (options) { + return orig.defaults(ext(def, options)).Minimatch + } + + m.filter = function filter (pattern, options) { + return orig.filter(pattern, ext(def, options)) + } + + m.defaults = function defaults (options) { + return orig.defaults(ext(def, options)) + } + + m.makeRe = function makeRe (pattern, options) { + return orig.makeRe(pattern, ext(def, options)) + } + + m.braceExpand = function braceExpand (pattern, options) { + return orig.braceExpand(pattern, ext(def, options)) + } + + m.match = function (list, pattern, options) { + return orig.match(list, pattern, ext(def, options)) + } return m } Minimatch.defaults = function (def) { - if (!def || !Object.keys(def).length) return Minimatch return minimatch.defaults(def).Minimatch } function minimatch (p, pattern, options) { - if (typeof pattern !== 'string') { - throw new TypeError('glob pattern string required') - } + assertValidPattern(pattern) if (!options) options = {} @@ -1992,9 +2013,6 @@ function minimatch (p, pattern, options) { return false } - // "" only matches "" - if (pattern.trim() === '') return p === '' - return new Minimatch(pattern, options).match(p) } @@ -2003,15 +2021,14 @@ function Minimatch (pattern, options) { return new Minimatch(pattern, options) } - if (typeof pattern !== 'string') { - throw new TypeError('glob pattern string required') - } + assertValidPattern(pattern) if (!options) options = {} + pattern = pattern.trim() // windows support: need to use /, not \ - if (path.sep !== '/') { + if (!options.allowWindowsEscape && path.sep !== '/') { pattern = pattern.split(path.sep).join('/') } @@ -2022,6 +2039,7 @@ function Minimatch (pattern, options) { this.negate = false this.comment = false this.empty = false + this.partial = !!options.partial // make the set of regexps etc. this.make() @@ -2031,9 +2049,6 @@ Minimatch.prototype.debug = function () {} Minimatch.prototype.make = make function make () { - // don't do it more than once. - if (this._made) return - var pattern = this.pattern var options = this.options @@ -2053,7 +2068,7 @@ function make () { // step 2: expand braces var set = this.globSet = this.braceExpand() - if (options.debug) this.debug = console.error + if (options.debug) this.debug = function debug() { console.error.apply(console, arguments) } this.debug(this.pattern, set) @@ -2133,12 +2148,11 @@ function braceExpand (pattern, options) { pattern = typeof pattern === 'undefined' ? this.pattern : pattern - if (typeof pattern === 'undefined') { - throw new TypeError('undefined pattern') - } + assertValidPattern(pattern) - if (options.nobrace || - !pattern.match(/\{.*\}/)) { + // Thanks to Yeting Li for + // improving this regexp to avoid a ReDOS vulnerability. + if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) { // shortcut. no need to expand. return [pattern] } @@ -2146,6 +2160,17 @@ function braceExpand (pattern, options) { return expand(pattern) } +var MAX_PATTERN_LENGTH = 1024 * 64 +var assertValidPattern = function (pattern) { + if (typeof pattern !== 'string') { + throw new TypeError('invalid pattern') + } + + if (pattern.length > MAX_PATTERN_LENGTH) { + throw new TypeError('pattern is too long') + } +} + // parse a component of the expanded set. // At this point, no pattern may contain "/" in it // so we're going to return a 2d array, where each entry is the full @@ -2160,14 +2185,17 @@ function braceExpand (pattern, options) { Minimatch.prototype.parse = parse var SUBPARSE = {} function parse (pattern, isSub) { - if (pattern.length > 1024 * 64) { - throw new TypeError('pattern is too long') - } + assertValidPattern(pattern) var options = this.options // shortcuts - if (!options.noglobstar && pattern === '**') return GLOBSTAR + if (pattern === '**') { + if (!options.noglobstar) + return GLOBSTAR + else + pattern = '*' + } if (pattern === '') return '' var re = '' @@ -2223,10 +2251,12 @@ function parse (pattern, isSub) { } switch (c) { - case '/': + /* istanbul ignore next */ + case '/': { // completely not allowed, even escaped. // Should already be path-split by now. return false + } case '\\': clearStateChar() @@ -2345,25 +2375,23 @@ function parse (pattern, isSub) { // handle the case where we left a class open. // "[z-a]" is valid, equivalent to "\[z-a\]" - if (inClass) { - // split where the last [ was, make sure we don't have - // an invalid re. if so, re-walk the contents of the - // would-be class to re-translate any characters that - // were passed through as-is - // TODO: It would probably be faster to determine this - // without a try/catch and a new RegExp, but it's tricky - // to do safely. For now, this is safe and works. - var cs = pattern.substring(classStart + 1, i) - try { - RegExp('[' + cs + ']') - } catch (er) { - // not a valid class! - var sp = this.parse(cs, SUBPARSE) - re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]' - hasMagic = hasMagic || sp[1] - inClass = false - continue - } + // split where the last [ was, make sure we don't have + // an invalid re. if so, re-walk the contents of the + // would-be class to re-translate any characters that + // were passed through as-is + // TODO: It would probably be faster to determine this + // without a try/catch and a new RegExp, but it's tricky + // to do safely. For now, this is safe and works. + var cs = pattern.substring(classStart + 1, i) + try { + RegExp('[' + cs + ']') + } catch (er) { + // not a valid class! + var sp = this.parse(cs, SUBPARSE) + re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]' + hasMagic = hasMagic || sp[1] + inClass = false + continue } // finish up the class. @@ -2447,9 +2475,7 @@ function parse (pattern, isSub) { // something that could conceivably capture a dot var addPatternStart = false switch (re.charAt(0)) { - case '.': - case '[': - case '(': addPatternStart = true + case '[': case '.': case '(': addPatternStart = true } // Hack to work around lack of negative lookbehind in JS @@ -2511,7 +2537,7 @@ function parse (pattern, isSub) { var flags = options.nocase ? 'i' : '' try { var regExp = new RegExp('^' + re + '$', flags) - } catch (er) { + } catch (er) /* istanbul ignore next - should be impossible */ { // If it was an invalid regular expression, then it can't match // anything. This trick looks for a character after the end of // the string, which is of course impossible, except in multi-line @@ -2569,7 +2595,7 @@ function makeRe () { try { this.regexp = new RegExp(re, flags) - } catch (ex) { + } catch (ex) /* istanbul ignore next - should be impossible */ { this.regexp = false } return this.regexp @@ -2587,8 +2613,8 @@ minimatch.match = function (list, pattern, options) { return list } -Minimatch.prototype.match = match -function match (f, partial) { +Minimatch.prototype.match = function match (f, partial) { + if (typeof partial === 'undefined') partial = this.partial this.debug('match', f, this.pattern) // short-circuit in the case of busted things. // comments, etc. @@ -2670,6 +2696,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // should be impossible. // some invalid regexp stuff in the set. + /* istanbul ignore if */ if (p === false) return false if (p === GLOBSTAR) { @@ -2743,6 +2770,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // no match was found. // However, in partial mode, we can't say this is necessarily over. // If there's more *pattern* left, then + /* istanbul ignore if */ if (partial) { // ran out of file this.debug('\n>>> no match, partial?', file, fr, pattern, pr) @@ -2756,11 +2784,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // patterns with magic have been turned into regexps. var hit if (typeof p === 'string') { - if (options.nocase) { - hit = f.toLowerCase() === p.toLowerCase() - } else { - hit = f === p - } + hit = f === p this.debug('string match', p, f, hit) } else { hit = f.match(p) @@ -2791,16 +2815,16 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // this is ok if we're doing the match as part of // a glob fs traversal. return partial - } else if (pi === pl) { + } else /* istanbul ignore else */ if (pi === pl) { // ran out of pattern, still have file left. // this is only acceptable if we're on the very last // empty segment of a file with a trailing slash. // a/* should match a/b/ - var emptyFileEnd = (fi === fl - 1) && (file[fi] === '') - return emptyFileEnd + return (fi === fl - 1) && (file[fi] === '') } // should be unreachable. + /* istanbul ignore next */ throw new Error('wtf?') } @@ -4940,13 +4964,15 @@ exports.checkBypass = checkBypass; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0; +exports.RefKey = exports.Variables = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0; var Inputs; (function (Inputs) { Inputs["Key"] = "key"; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; + Inputs["StrictRestore"] = "strict-restore"; + Inputs["SaveOnAnyFailure"] = "save-on-any-failure"; })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { @@ -4963,6 +4989,10 @@ var Events; Events["Push"] = "push"; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); +var Variables; +(function (Variables) { + Variables["SaveCacheOnAnyFailure"] = "SAVE_CACHE_ON_ANY_FAILURE"; +})(Variables = exports.Variables || (exports.Variables = {})); exports.RefKey = "GITHUB_REF"; @@ -48984,7 +49014,17 @@ function run() { required: true }); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey, restoreKeys); + //Check if user wants to save cache despite of failure in any previous job + const saveCache = core.getInput(constants_1.Inputs.SaveOnAnyFailure); + if (saveCache === "yes") { + core.debug(`save cache input variable is set to yes`); + core.exportVariable(constants_1.Variables.SaveCacheOnAnyFailure, saveCache); + core.info(`Input Variable ${constants_1.Variables.SaveCacheOnAnyFailure} is set to yes, the cache will be saved despite of any failure in the build.`); + } if (!cacheKey) { + if (core.getInput(constants_1.Inputs.StrictRestore) == "yes") { + throw new Error(`Cache with the given input key ${primaryKey} is not found, hence exiting the workflow as the strict-restore requirement is not met.`); + } core.info(`Cache not found for input keys: ${[ primaryKey, ...restoreKeys @@ -48995,6 +49035,9 @@ function run() { utils.setCacheState(cacheKey); const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey); utils.setCacheHitOutput(isExactKeyMatch); + if (!isExactKeyMatch && core.getInput(constants_1.Inputs.StrictRestore) == "yes") { + throw new Error(`Restored cache key doesn't match the given input key ${primaryKey}, hence exiting the workflow as the strict-restore requirement is not met.`); + } core.info(`Cache restored from key: ${cacheKey}`); } catch (error) { diff --git a/dist/save/index.js b/dist/save/index.js index bd9f422..800d6f0 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -1892,10 +1892,10 @@ function serial(list, iterator, callback) module.exports = minimatch minimatch.Minimatch = Minimatch -var path = { sep: '/' } -try { - path = __webpack_require__(622) -} catch (er) {} +var path = (function () { try { return __webpack_require__(622) } catch (e) {}}()) || { + sep: '/' +} +minimatch.sep = path.sep var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} var expand = __webpack_require__(306) @@ -1947,43 +1947,64 @@ function filter (pattern, options) { } function ext (a, b) { - a = a || {} b = b || {} var t = {} - Object.keys(b).forEach(function (k) { - t[k] = b[k] - }) Object.keys(a).forEach(function (k) { t[k] = a[k] }) + Object.keys(b).forEach(function (k) { + t[k] = b[k] + }) return t } minimatch.defaults = function (def) { - if (!def || !Object.keys(def).length) return minimatch + if (!def || typeof def !== 'object' || !Object.keys(def).length) { + return minimatch + } var orig = minimatch var m = function minimatch (p, pattern, options) { - return orig.minimatch(p, pattern, ext(def, options)) + return orig(p, pattern, ext(def, options)) } m.Minimatch = function Minimatch (pattern, options) { return new orig.Minimatch(pattern, ext(def, options)) } + m.Minimatch.defaults = function defaults (options) { + return orig.defaults(ext(def, options)).Minimatch + } + + m.filter = function filter (pattern, options) { + return orig.filter(pattern, ext(def, options)) + } + + m.defaults = function defaults (options) { + return orig.defaults(ext(def, options)) + } + + m.makeRe = function makeRe (pattern, options) { + return orig.makeRe(pattern, ext(def, options)) + } + + m.braceExpand = function braceExpand (pattern, options) { + return orig.braceExpand(pattern, ext(def, options)) + } + + m.match = function (list, pattern, options) { + return orig.match(list, pattern, ext(def, options)) + } return m } Minimatch.defaults = function (def) { - if (!def || !Object.keys(def).length) return Minimatch return minimatch.defaults(def).Minimatch } function minimatch (p, pattern, options) { - if (typeof pattern !== 'string') { - throw new TypeError('glob pattern string required') - } + assertValidPattern(pattern) if (!options) options = {} @@ -1992,9 +2013,6 @@ function minimatch (p, pattern, options) { return false } - // "" only matches "" - if (pattern.trim() === '') return p === '' - return new Minimatch(pattern, options).match(p) } @@ -2003,15 +2021,14 @@ function Minimatch (pattern, options) { return new Minimatch(pattern, options) } - if (typeof pattern !== 'string') { - throw new TypeError('glob pattern string required') - } + assertValidPattern(pattern) if (!options) options = {} + pattern = pattern.trim() // windows support: need to use /, not \ - if (path.sep !== '/') { + if (!options.allowWindowsEscape && path.sep !== '/') { pattern = pattern.split(path.sep).join('/') } @@ -2022,6 +2039,7 @@ function Minimatch (pattern, options) { this.negate = false this.comment = false this.empty = false + this.partial = !!options.partial // make the set of regexps etc. this.make() @@ -2031,9 +2049,6 @@ Minimatch.prototype.debug = function () {} Minimatch.prototype.make = make function make () { - // don't do it more than once. - if (this._made) return - var pattern = this.pattern var options = this.options @@ -2053,7 +2068,7 @@ function make () { // step 2: expand braces var set = this.globSet = this.braceExpand() - if (options.debug) this.debug = console.error + if (options.debug) this.debug = function debug() { console.error.apply(console, arguments) } this.debug(this.pattern, set) @@ -2133,12 +2148,11 @@ function braceExpand (pattern, options) { pattern = typeof pattern === 'undefined' ? this.pattern : pattern - if (typeof pattern === 'undefined') { - throw new TypeError('undefined pattern') - } + assertValidPattern(pattern) - if (options.nobrace || - !pattern.match(/\{.*\}/)) { + // Thanks to Yeting Li for + // improving this regexp to avoid a ReDOS vulnerability. + if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) { // shortcut. no need to expand. return [pattern] } @@ -2146,6 +2160,17 @@ function braceExpand (pattern, options) { return expand(pattern) } +var MAX_PATTERN_LENGTH = 1024 * 64 +var assertValidPattern = function (pattern) { + if (typeof pattern !== 'string') { + throw new TypeError('invalid pattern') + } + + if (pattern.length > MAX_PATTERN_LENGTH) { + throw new TypeError('pattern is too long') + } +} + // parse a component of the expanded set. // At this point, no pattern may contain "/" in it // so we're going to return a 2d array, where each entry is the full @@ -2160,14 +2185,17 @@ function braceExpand (pattern, options) { Minimatch.prototype.parse = parse var SUBPARSE = {} function parse (pattern, isSub) { - if (pattern.length > 1024 * 64) { - throw new TypeError('pattern is too long') - } + assertValidPattern(pattern) var options = this.options // shortcuts - if (!options.noglobstar && pattern === '**') return GLOBSTAR + if (pattern === '**') { + if (!options.noglobstar) + return GLOBSTAR + else + pattern = '*' + } if (pattern === '') return '' var re = '' @@ -2223,10 +2251,12 @@ function parse (pattern, isSub) { } switch (c) { - case '/': + /* istanbul ignore next */ + case '/': { // completely not allowed, even escaped. // Should already be path-split by now. return false + } case '\\': clearStateChar() @@ -2345,25 +2375,23 @@ function parse (pattern, isSub) { // handle the case where we left a class open. // "[z-a]" is valid, equivalent to "\[z-a\]" - if (inClass) { - // split where the last [ was, make sure we don't have - // an invalid re. if so, re-walk the contents of the - // would-be class to re-translate any characters that - // were passed through as-is - // TODO: It would probably be faster to determine this - // without a try/catch and a new RegExp, but it's tricky - // to do safely. For now, this is safe and works. - var cs = pattern.substring(classStart + 1, i) - try { - RegExp('[' + cs + ']') - } catch (er) { - // not a valid class! - var sp = this.parse(cs, SUBPARSE) - re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]' - hasMagic = hasMagic || sp[1] - inClass = false - continue - } + // split where the last [ was, make sure we don't have + // an invalid re. if so, re-walk the contents of the + // would-be class to re-translate any characters that + // were passed through as-is + // TODO: It would probably be faster to determine this + // without a try/catch and a new RegExp, but it's tricky + // to do safely. For now, this is safe and works. + var cs = pattern.substring(classStart + 1, i) + try { + RegExp('[' + cs + ']') + } catch (er) { + // not a valid class! + var sp = this.parse(cs, SUBPARSE) + re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]' + hasMagic = hasMagic || sp[1] + inClass = false + continue } // finish up the class. @@ -2447,9 +2475,7 @@ function parse (pattern, isSub) { // something that could conceivably capture a dot var addPatternStart = false switch (re.charAt(0)) { - case '.': - case '[': - case '(': addPatternStart = true + case '[': case '.': case '(': addPatternStart = true } // Hack to work around lack of negative lookbehind in JS @@ -2511,7 +2537,7 @@ function parse (pattern, isSub) { var flags = options.nocase ? 'i' : '' try { var regExp = new RegExp('^' + re + '$', flags) - } catch (er) { + } catch (er) /* istanbul ignore next - should be impossible */ { // If it was an invalid regular expression, then it can't match // anything. This trick looks for a character after the end of // the string, which is of course impossible, except in multi-line @@ -2569,7 +2595,7 @@ function makeRe () { try { this.regexp = new RegExp(re, flags) - } catch (ex) { + } catch (ex) /* istanbul ignore next - should be impossible */ { this.regexp = false } return this.regexp @@ -2587,8 +2613,8 @@ minimatch.match = function (list, pattern, options) { return list } -Minimatch.prototype.match = match -function match (f, partial) { +Minimatch.prototype.match = function match (f, partial) { + if (typeof partial === 'undefined') partial = this.partial this.debug('match', f, this.pattern) // short-circuit in the case of busted things. // comments, etc. @@ -2670,6 +2696,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // should be impossible. // some invalid regexp stuff in the set. + /* istanbul ignore if */ if (p === false) return false if (p === GLOBSTAR) { @@ -2743,6 +2770,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // no match was found. // However, in partial mode, we can't say this is necessarily over. // If there's more *pattern* left, then + /* istanbul ignore if */ if (partial) { // ran out of file this.debug('\n>>> no match, partial?', file, fr, pattern, pr) @@ -2756,11 +2784,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // patterns with magic have been turned into regexps. var hit if (typeof p === 'string') { - if (options.nocase) { - hit = f.toLowerCase() === p.toLowerCase() - } else { - hit = f === p - } + hit = f === p this.debug('string match', p, f, hit) } else { hit = f.match(p) @@ -2791,16 +2815,16 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // this is ok if we're doing the match as part of // a glob fs traversal. return partial - } else if (pi === pl) { + } else /* istanbul ignore else */ if (pi === pl) { // ran out of pattern, still have file left. // this is only acceptable if we're on the very last // empty segment of a file with a trailing slash. // a/* should match a/b/ - var emptyFileEnd = (fi === fl - 1) && (file[fi] === '') - return emptyFileEnd + return (fi === fl - 1) && (file[fi] === '') } // should be unreachable. + /* istanbul ignore next */ throw new Error('wtf?') } @@ -4940,13 +4964,15 @@ exports.checkBypass = checkBypass; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0; +exports.RefKey = exports.Variables = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0; var Inputs; (function (Inputs) { Inputs["Key"] = "key"; Inputs["Path"] = "path"; Inputs["RestoreKeys"] = "restore-keys"; Inputs["UploadChunkSize"] = "upload-chunk-size"; + Inputs["StrictRestore"] = "strict-restore"; + Inputs["SaveOnAnyFailure"] = "save-on-any-failure"; })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { @@ -4963,6 +4989,10 @@ var Events; Events["Push"] = "push"; Events["PullRequest"] = "pull_request"; })(Events = exports.Events || (exports.Events = {})); +var Variables; +(function (Variables) { + Variables["SaveCacheOnAnyFailure"] = "SAVE_CACHE_ON_ANY_FAILURE"; +})(Variables = exports.Variables || (exports.Variables = {})); exports.RefKey = "GITHUB_REF"; @@ -47298,7 +47328,7 @@ function run() { } 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(constants_1.State.CachePrimaryKey); + const primaryKey = core.getState(constants_1.State.CachePrimaryKey) || core.getInput(constants_1.Inputs.Key); if (!primaryKey) { utils.logWarning(`Error retrieving key from state.`); return; diff --git a/package-lock.json b/package-lock.json index fe28d4f..0f6a02f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4055,18 +4055,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -8129,9 +8117,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12754,15 +12742,6 @@ "has": "^1.0.3" } }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -15888,9 +15867,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } diff --git a/restore/action.yml b/restore/action.yml new file mode 100644 index 0000000..e319d18 --- /dev/null +++ b/restore/action.yml @@ -0,0 +1,26 @@ +name: 'Restore Cache' +description: 'Restore Cache artifacts like dependencies and build outputs to improve workflow execution time' +author: 'GitHub' +inputs: + path: + description: 'A list of files, directories, and wildcard patterns to cache and restore' + required: true + key: + description: 'An explicit key for restoring and saving the cache' + required: true + restore-keys: + description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.' + required: false + strict-restore: + description: 'Fail the workflow if the cache is not found for the given key.' + required: false + default: "no" +outputs: + cache-hit: + description: 'A boolean value to indicate an exact match was found for the primary key' +runs: + using: 'node16' + main: '../dist/restore/index.js' +branding: + icon: 'archive' + color: 'gray-dark' \ No newline at end of file diff --git a/save/action.yml b/save/action.yml new file mode 100644 index 0000000..af9f7b9 --- /dev/null +++ b/save/action.yml @@ -0,0 +1,19 @@ +name: 'Save Cache' +description: 'Save Cache artifacts like dependencies and build outputs to improve workflow execution time' +author: 'GitHub' +inputs: + path: + description: 'A list of files, directories, and wildcard patterns to cache and restore' + required: true + key: + description: 'An explicit key for restoring and saving the cache' + required: true + upload-chunk-size: + description: 'The chunk size used to split up large files during upload, in bytes' + required: false +runs: + using: 'node16' + main: '../dist/save/index.js' +branding: + icon: 'archive' + color: 'gray-dark' \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 133f47d..341961a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,9 @@ export enum Inputs { Key = "key", Path = "path", RestoreKeys = "restore-keys", - UploadChunkSize = "upload-chunk-size" + UploadChunkSize = "upload-chunk-size", + StrictRestore = "strict-restore", + SaveOnAnyFailure = "save-on-any-failure" } export enum Outputs { @@ -20,4 +22,8 @@ export enum Events { PullRequest = "pull_request" } +export enum Variables { + SaveCacheOnAnyFailure = "SAVE_CACHE_ON_ANY_FAILURE" +} + export const RefKey = "GITHUB_REF"; diff --git a/src/restore.ts b/src/restore.ts index 5bc17fa..302bc93 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -1,7 +1,7 @@ import * as cache from "@actions/cache"; import * as core from "@actions/core"; -import { Events, Inputs, State } from "./constants"; +import { Events, Inputs, State, Variables } from "./constants"; import * as utils from "./utils/actionUtils"; async function run(): Promise { @@ -35,22 +35,41 @@ async function run(): Promise { restoreKeys ); + //Check if user wants to save cache despite of failure in any previous job + const saveCache = core.getInput(Inputs.SaveOnAnyFailure); + if (saveCache === "yes") { + core.debug(`save cache input variable is set to yes`); + core.exportVariable(Variables.SaveCacheOnAnyFailure, saveCache); + core.info( + `Input Variable ${Variables.SaveCacheOnAnyFailure} is set to yes, the cache will be saved despite of any failure in the build.` + ); + } + if (!cacheKey) { + if (core.getInput(Inputs.StrictRestore) == "yes") { + throw new Error( + `Cache with the given input key ${primaryKey} is not found, hence exiting the workflow as the strict-restore requirement is not met.` + ); + } 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); + + if (!isExactKeyMatch && core.getInput(Inputs.StrictRestore) == "yes") { + throw new Error( + `Restored cache key doesn't match the given input key ${primaryKey}, hence exiting the workflow as the strict-restore requirement is not met.` + ); + } core.info(`Cache restored from key: ${cacheKey}`); } catch (error: unknown) { core.setFailed((error as Error).message); diff --git a/src/save.ts b/src/save.ts index a0a21bf..88f1c0a 100644 --- a/src/save.ts +++ b/src/save.ts @@ -27,7 +27,9 @@ async function run(): Promise { 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); + const primaryKey = + core.getState(State.CachePrimaryKey) || core.getInput(Inputs.Key); + if (!primaryKey) { utils.logWarning(`Error retrieving key from state.`); return;