Compare commits

...
4 Commits
Author SHA1 Message Date
7c44e618e7 fix: drop the GitHub Actions cache layer, keep the tool cache lookup
Review found that the GitHub Actions cache layer cost more than it
saved. Measured on ubuntu-latest, same job, same version:

  cold: download + checksum + cosign + extract   0.93 s
  cache hit: restore the 24 MB entry             1.28 s
  first run also pays a save                    +2.26 s

It is slower than a download in every configuration measured, because a
GitHub-hosted runner reaches the release CDN in about 0.4 s for a 15 MB
archive, and the cached entry is the larger extracted directory.

It also skipped the sha256 and cosign verification on a hit, which is
the control it was supposed to protect, and the only case where it wins
on time is when cosign is installed, which is exactly the case where
skipping is wrong. It cost 875 KB (+120%) of dist/index.js for every
user and 24 MB of repository cache quota per version and platform.

The restore and save wrappers were also dead code: @actions/cache
catches everything except ValidationError internally, so the try/catch
and the ReserveCacheError classification could never run.

What remains is the runner tool cache lookup, which is what #476 asked
for, and the distribution-keyed tool name that stops a Pro binary being
returned for an OSS install.

The tool cache test asserted that two installs return the same path, but
that path is a pure function of the tool, version and architecture: it
passed even with the tool cache wiped between the calls. It now asserts
that the second install reports a tool cache hit and does not download,
and it was verified to fail when the lookup is removed.

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>
Copilot-Session: 4eaf86fa-a85b-41f6-8763-612acc1ccc39
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2026-08-29 17:56:44 -03:00
Carlos Alexandro Becker df0896bb77 Merge remote-tracking branch 'origin/master' into cache
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

# Conflicts:
#	dist/index.js
2026-08-29 15:40:18 -03:00
Carlos Alexandro BeckerandCopilot 52910df325 refactor: collapse the duplicated zip extract branches
Both branches called extractZip with the same destination after the
install rewrite, so only the source path differs now.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4eaf86fa-a85b-41f6-8763-612acc1ccc39
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2026-08-29 15:37:33 -03:00
fa7c11c5a3 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>
2026-08-29 15:19:02 -03:00
4 changed files with 77 additions and 14 deletions
+22
View File
@@ -21,6 +21,7 @@ ___
* [Signing](#signing)
* [Upload artifacts](#upload-artifacts)
* [Install Only](#install-only)
* [Cache the binary](#cache-the-binary)
* [Customizing](#customizing)
* [inputs](#inputs)
* [outputs](#outputs)
@@ -217,6 +218,27 @@ steps:
run: goreleaser -v
```
### Cache the binary
The action looks for GoReleaser in the [runner tool cache][toolcache] before it
downloads. A second use of the action in the same job, or any job on a
self-hosted runner that already has the version, installs immediately.
A binary taken from the tool cache is not verified again, because the checksum
and the cosign signature were verified when it was first written. On a
self-hosted runner the tool cache is kept between jobs, so it must be trusted
like the runner itself. GitHub-hosted runners start with an empty tool cache in
every job, so they always download and verify.
The action does not use the [GitHub Actions cache][ghcache]. It was measured and
it is slower than a download: restoring the 24 MB entry takes about 1.3 s, while
downloading, verifying the checksum, verifying the cosign signature and
extracting the release takes about 0.9 s on a GitHub-hosted runner. It would
also skip the verification it is supposed to protect.
[toolcache]: https://github.com/actions/toolkit/tree/main/packages/tool-cache
[ghcache]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/cache-dependencies
## Customizing
### inputs
+32
View File
@@ -57,6 +57,38 @@ describe('install', () => {
const bin = await goreleaser.install('goreleaser-pro', 'latest');
expect(fs.existsSync(bin)).toBe(true);
}, 100000);
it('reuses the runner tool cache instead of downloading again', async () => {
const first = await goreleaser.install('goreleaser', 'v2.15.3');
const written: string[] = [];
const stdout = process.stdout.write.bind(process.stdout);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.stdout.write = ((chunk: any, ...rest: any[]): boolean => {
written.push(chunk.toString());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (stdout as any)(chunk, ...rest);
}) as typeof process.stdout.write;
let second: string;
try {
second = await goreleaser.install('goreleaser', 'v2.15.3');
} finally {
process.stdout.write = stdout;
}
const logs = written.join('');
expect(logs).toContain('found in the runner tool cache');
expect(logs).not.toContain('Downloading https://github.com/goreleaser');
expect(second).toEqual(first);
expect(fs.existsSync(second)).toBe(true);
}, 100000);
it('does not share the tool cache between distributions', async () => {
const oss = await goreleaser.install('goreleaser', 'v2.15.3');
const pro = await goreleaser.install('goreleaser-pro', 'v2.15.3');
expect(pro).not.toEqual(oss);
expect(fs.existsSync(pro)).toBe(true);
}, 100000);
});
describe('distribSuffix', () => {
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -13
View File
@@ -11,40 +11,49 @@ import * as tc from '@actions/tool-cache';
export async function install(distribution: string, version: string): Promise<string> {
const release: github.GitHubRelease = await github.getRelease(distribution, version);
const tag = release.tag_name;
const toolVersion = tag.replace(/^v/, '');
const toolPath = 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 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;
if (context.osPlat == 'win32') {
if (!downloadPath.endsWith('.zip')) {
const newPath = downloadPath + '.zip';
fs.renameSync(downloadPath, newPath);
extPath = await tc.extractZip(newPath);
} else {
extPath = await tc.extractZip(downloadPath);
let zipPath = downloadPath;
if (!zipPath.endsWith('.zip')) {
zipPath = `${downloadPath}.zip`;
fs.renameSync(downloadPath, zipPath);
}
extPath = await tc.extractZip(zipPath);
} else {
extPath = await tc.extractTar(downloadPath);
}
core.debug(`Extracted to ${extPath}`);
const cachePath: string = await tc.cacheDir(extPath, 'goreleaser-action', release.tag_name.replace(/^v/, ''));
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');
};
export async function verifyChecksum(
distribution: string,
tag: string,