golangci-lint-action/src/cache.ts
Eric Zimanyi 537aa1903e
Expire cache periodically to avoid unbounded size (#466)
* Expire cache periodically to avoid unbounded size

The cache key includes a sequence number that rotates every 7 days
but because we are also using the base `golangci-lint.cache` as a
restore key, the new cache will always be seeded with the full
contents of the old cache.

In particular for the go module cache, this leads to an ever
increasing number of cached packages that never get pruned.

This commit updates it so we stop using `golangci-lint.cache` as
a restore key, which will force a build from an empty cache once
every 7 days.

* Rebuild files in dist/
2022-05-10 21:30:26 -07:00

152 lines
4.7 KiB
TypeScript

import * as cache from "@actions/cache"
import * as core from "@actions/core"
import * as crypto from "crypto"
import * as fs from "fs"
import path from "path"
import { Events, State } from "./constants"
import * as utils from "./utils/actionUtils"
function checksumFile(hashName: string, path: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName)
const stream = fs.createReadStream(path)
stream.on("error", (err) => reject(err))
stream.on("data", (chunk) => hash.update(chunk))
stream.on("end", () => resolve(hash.digest("hex")))
})
}
const pathExists = async (path: string): Promise<boolean> => !!(await fs.promises.stat(path).catch(() => false))
const getLintCacheDir = (): string => {
return path.resolve(`${process.env.HOME}/.cache/golangci-lint`)
}
const getCacheDirs = (): string[] => {
// Not existing dirs are ok here: it works.
const skipPkgCache = core.getInput(`skip-pkg-cache`, { required: true }).trim()
const skipBuildCache = core.getInput(`skip-build-cache`, { required: true }).trim()
const dirs = [getLintCacheDir()]
if (skipBuildCache.toLowerCase() == "true") {
core.info(`Omitting ~/.cache/go-build from cache directories`)
} else {
dirs.push(path.resolve(`${process.env.HOME}/.cache/go-build`))
}
if (skipPkgCache.toLowerCase() == "true") {
core.info(`Omitting ~/go/pkg from cache directories`)
} else {
dirs.push(path.resolve(`${process.env.HOME}/go/pkg`))
}
return dirs
}
const getIntervalKey = (invalidationIntervalDays: number): string => {
const now = new Date()
const secondsSinceEpoch = now.getTime() / 1000
const intervalNumber = Math.floor(secondsSinceEpoch / (invalidationIntervalDays * 86400))
return intervalNumber.toString()
}
async function buildCacheKeys(): Promise<string[]> {
const keys = []
// Periodically invalidate a cache because a new code being added.
// TODO: configure it via inputs.
let cacheKey = `golangci-lint.cache-${getIntervalKey(7)}-`
keys.push(cacheKey)
if (await pathExists(`go.mod`)) {
// Add checksum to key to invalidate a cache when dependencies change.
cacheKey += await checksumFile(`sha1`, `go.mod`)
} else {
cacheKey += `nogomod`
}
keys.push(cacheKey)
return keys
}
export async function restoreCache(): Promise<void> {
if (core.getInput(`skip-cache`, { required: true }).trim() == "true") 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
}
const startedAt = Date.now()
const keys = await buildCacheKeys()
const primaryKey = keys.pop()
const restoreKeys = keys.reverse()
// Tell golangci-lint to use our cache directory.
process.env.GOLANGCI_LINT_CACHE = getLintCacheDir()
if (!primaryKey) {
utils.logWarning(`Invalid primary key`)
return
}
core.saveState(State.CachePrimaryKey, primaryKey)
try {
const cacheKey = await cache.restoreCache(getCacheDirs(), primaryKey, restoreKeys)
if (!cacheKey) {
core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`)
return
}
// Store the matched cache key
utils.setCacheState(cacheKey)
core.info(`Restored cache for golangci-lint from key '${primaryKey}' in ${Date.now() - startedAt}ms`)
} catch (error) {
if (error.name === cache.ValidationError.name) {
throw error
} else {
core.warning(error.message)
}
}
}
export async function saveCache(): Promise<void> {
if (core.getInput(`skip-cache`, { required: true }).trim() == "true") 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 startedAt = Date.now()
const cacheDirs = getCacheDirs()
const primaryKey = core.getState(State.CachePrimaryKey)
if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`)
return
}
const state = utils.getCacheState()
if (utils.isExactKeyMatch(primaryKey, state)) {
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`)
return
}
try {
await cache.saveCache(cacheDirs, primaryKey)
core.info(`Saved cache for golangci-lint from paths '${cacheDirs.join(`, `)}' in ${Date.now() - startedAt}ms`)
} catch (error) {
if (error.name === cache.ValidationError.name) {
throw error
} else if (error.name === cache.ReserveCacheError.name) {
core.info(error.message)
} else {
core.info(`[warning] ${error.message}`)
}
}
}