feat: cache the goreleaser binary

Look for GoReleaser in the runner tool cache before a download. This
makes a second use of the action in the same job, or any job on a
self-hosted runner, install immediately.

Add an opt-in `cache-binary` input that also stores the binary in the
GitHub Actions cache. When it hits, the action does not download the
release archive, the checksums and the signature bundle again. Cache
errors are not fatal and fall back to a download.

The runner tool cache entry is now keyed by distribution, so a Pro
binary is no longer returned for an OSS install of the same version.

Closes #476

Co-authored-by: timbretimber <105982513+timbretimber@users.noreply.github.com>
Co-authored-by: Akkuman <akkumans@qq.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker
2026-08-29 15:19:02 -03:00
co-authored by timbretimber Akkuman Copilot
parent 59060fdcfd
commit fa7c11c5a3
12 changed files with 1809 additions and 27 deletions
+75 -12
View File
@@ -1,50 +1,113 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as yaml from 'js-yaml';
import * as context from './context';
import * as github from './github';
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
export async function install(distribution: string, version: string): Promise<string> {
export async function install(distribution: string, version: string, cacheBinary = false): Promise<string> {
const release: github.GitHubRelease = await github.getRelease(distribution, version);
const tag = release.tag_name;
const toolVersion = tag.replace(/^v/, '');
const toolPath: string = tc.find(distribution, toolVersion);
if (toolPath) {
core.info(`GoReleaser ${tag} found in the runner tool cache: ${toolPath}`);
return getExePath(toolPath);
}
const filename = getFilename(distribution);
const baseUrl = `https://github.com/goreleaser/${distribution}/releases/download/${release.tag_name}`;
const extPath = path.join(process.env.RUNNER_TEMP || os.tmpdir(), 'goreleaser-action', distribution, toolVersion);
const useCache = cacheBinary && cache.isFeatureAvailable();
// The GitHub Actions cache is shared by every runner of a repository, so the
// key must pin the distribution, platform and architecture. The release
// filename already encodes all three.
const cacheKey = `goreleaser-action-${tag}-${filename}`;
if (useCache && (await restoreCache(cacheKey, extPath))) {
return getExePath(await tc.cacheDir(extPath, distribution, toolVersion));
}
const baseUrl = `https://github.com/goreleaser/${distribution}/releases/download/${tag}`;
const downloadUrl = `${baseUrl}/${filename}`;
core.info(`Downloading ${downloadUrl}`);
const downloadPath: string = await tc.downloadTool(downloadUrl);
core.debug(`Downloaded to ${downloadPath}`);
await verifyChecksum(distribution, release.tag_name, downloadPath, filename);
await verifyChecksum(distribution, tag, downloadPath, filename);
core.info('Extracting GoReleaser');
let extPath: string;
await io.rmRF(extPath);
if (context.osPlat == 'win32') {
if (!downloadPath.endsWith('.zip')) {
const newPath = downloadPath + '.zip';
fs.renameSync(downloadPath, newPath);
extPath = await tc.extractZip(newPath);
await tc.extractZip(newPath, extPath);
} else {
extPath = await tc.extractZip(downloadPath);
await tc.extractZip(downloadPath, extPath);
}
} else {
extPath = await tc.extractTar(downloadPath);
await tc.extractTar(downloadPath, extPath);
}
core.debug(`Extracted to ${extPath}`);
const cachePath: string = await tc.cacheDir(extPath, 'goreleaser-action', release.tag_name.replace(/^v/, ''));
if (useCache) {
await saveCache(cacheKey, extPath);
}
const cachePath: string = await tc.cacheDir(extPath, distribution, toolVersion);
core.debug(`Cached to ${cachePath}`);
const exePath: string = path.join(cachePath, context.osPlat == 'win32' ? 'goreleaser.exe' : 'goreleaser');
core.debug(`Exe path is ${exePath}`);
return exePath;
return getExePath(cachePath);
}
const getExePath = (dir: string): string => {
return path.join(dir, context.osPlat == 'win32' ? 'goreleaser.exe' : 'goreleaser');
};
async function restoreCache(key: string, dest: string): Promise<boolean> {
try {
const hit = await cache.restoreCache([dest], key);
if (!hit) {
core.info(`No GitHub Actions cache entry for ${key}`);
return false;
}
if (!fs.existsSync(getExePath(dest))) {
core.warning(`Ignoring incomplete GitHub Actions cache entry ${hit}`);
await io.rmRF(dest);
return false;
}
core.info(`Restored ${hit} from the GitHub Actions cache`);
return true;
} catch (e) {
logCacheError(`Unable to restore ${key} from the GitHub Actions cache`, e);
return false;
}
}
async function saveCache(key: string, src: string): Promise<void> {
try {
await cache.saveCache([src], key);
core.info(`Saved ${key} to the GitHub Actions cache`);
} catch (e) {
logCacheError(`Unable to save ${key} to the GitHub Actions cache`, e);
}
}
// A read-only cache token (fork pull requests) and a key already reserved by a
// concurrent job are expected, so they must not raise a warning annotation.
const logCacheError = (message: string, e: Error): void => {
const expected = e instanceof cache.ReserveCacheError || e instanceof cache.CacheReadDeniedError;
(expected ? core.info : core.warning)(`${message}: ${e.message}`);
};
export async function verifyChecksum(
distribution: string,
tag: string,