mirror of
https://github.com/goreleaser/goreleaser-action
synced 2026-09-03 11:28:28 +00:00
* 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> * 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> * 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> * docs: drop the GitHub Actions cache rationale from the README Review feedback: the paragraph explaining why the action does not use the GitHub Actions cache is not needed. The measurements stay in the pull request for anyone who asks again. Also rebuilds dist for js-yaml 5.4.1, merged from master in #578. 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> * fix(cache): preserve verification guarantees Keep checksum-skipped downloads out of the persistent tool cache and separate checksum-only entries from cosign-verified entries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5abc512e-338c-4459-98ea-d3606438d56a Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> 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 Copilot-Session: 5abc512e-338c-4459-98ea-d3606438d56a
122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
|
|
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
const find = jest.fn<(tool: string, version: string) => string>();
|
|
const downloadTool = jest.fn<(url: string) => Promise<string>>();
|
|
const extractTar = jest.fn<(archive: string) => Promise<string>>();
|
|
const extractZip = jest.fn<(archive: string) => Promise<string>>();
|
|
const cacheDir = jest.fn<(source: string, tool: string, version: string) => Promise<string>>();
|
|
const which = jest.fn<(tool: string, check: boolean) => Promise<string>>();
|
|
const exec = jest.fn<(command: string, args: string[]) => Promise<number>>();
|
|
|
|
jest.unstable_mockModule('@actions/tool-cache', () => ({
|
|
find,
|
|
downloadTool,
|
|
extractTar,
|
|
extractZip,
|
|
cacheDir
|
|
}));
|
|
jest.unstable_mockModule('@actions/io', () => ({which}));
|
|
jest.unstable_mockModule('@actions/exec', () => ({exec}));
|
|
|
|
const goreleaser = await import('../src/goreleaser');
|
|
|
|
describe('runner tool cache verification', () => {
|
|
let tempDir: string;
|
|
let archivePath: string;
|
|
let checksumsPath: string;
|
|
let bundlePath: string;
|
|
let extractedPath: string;
|
|
let cosignPath: string;
|
|
let cache: Map<string, string>;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'goreleaser-cache-'));
|
|
archivePath = path.join(tempDir, 'archive');
|
|
checksumsPath = path.join(tempDir, 'checksums.txt');
|
|
bundlePath = path.join(tempDir, 'checksums.txt.sigstore.json');
|
|
extractedPath = path.join(tempDir, 'extracted');
|
|
fs.writeFileSync(archivePath, 'archive');
|
|
fs.writeFileSync(bundlePath, '{}');
|
|
fs.mkdirSync(extractedPath);
|
|
|
|
const checksum = crypto.createHash('sha256').update('archive').digest('hex');
|
|
fs.writeFileSync(
|
|
checksumsPath,
|
|
[
|
|
`${checksum} goreleaser_Linux_x86_64.tar.gz`,
|
|
`${checksum} goreleaser_Darwin_all.tar.gz`,
|
|
`${checksum} goreleaser_Windows_x86_64.zip`
|
|
].join('\n')
|
|
);
|
|
|
|
cosignPath = '';
|
|
cache = new Map();
|
|
find.mockImplementation((tool, version) => cache.get(`${tool}:${version}`) || '');
|
|
downloadTool.mockImplementation(async url => {
|
|
if (url.endsWith('/checksums.txt')) {
|
|
return checksumsPath;
|
|
}
|
|
if (url.endsWith('/checksums.txt.sigstore.json')) {
|
|
return bundlePath;
|
|
}
|
|
return archivePath;
|
|
});
|
|
extractTar.mockResolvedValue(extractedPath);
|
|
extractZip.mockResolvedValue(extractedPath);
|
|
cacheDir.mockImplementation(async (_source, tool, version) => {
|
|
const destination = path.join(tempDir, tool, version);
|
|
cache.set(`${tool}:${version}`, destination);
|
|
return destination;
|
|
});
|
|
which.mockImplementation(async () => cosignPath);
|
|
exec.mockResolvedValue(0);
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, {recursive: true, force: true});
|
|
});
|
|
|
|
it('does not cache a binary when checksum verification is skipped', async () => {
|
|
downloadTool.mockImplementation(async url => {
|
|
if (url.endsWith('/checksums.txt')) {
|
|
throw new Error('checksums unavailable');
|
|
}
|
|
return archivePath;
|
|
});
|
|
|
|
await goreleaser.install('goreleaser', 'v2.15.3');
|
|
await goreleaser.install('goreleaser', 'v2.15.3');
|
|
|
|
expect(cacheDir).not.toHaveBeenCalled();
|
|
expect(downloadTool.mock.calls.filter(([url]) => !url.endsWith('/checksums.txt'))).toHaveLength(2);
|
|
});
|
|
|
|
it('does not reuse a checksum-only entry when cosign becomes available', async () => {
|
|
await goreleaser.install('goreleaser', 'v2.15.3');
|
|
expect(cacheDir).toHaveBeenLastCalledWith(extractedPath, 'goreleaser-checksum', '2.15.3');
|
|
|
|
cosignPath = '/usr/local/bin/cosign';
|
|
await goreleaser.install('goreleaser', 'v2.15.3');
|
|
await goreleaser.install('goreleaser', 'v2.15.3');
|
|
|
|
expect(exec).toHaveBeenCalledTimes(1);
|
|
expect(cacheDir).toHaveBeenLastCalledWith(extractedPath, 'goreleaser-cosign', '2.15.3');
|
|
expect(downloadTool.mock.calls.filter(([url]) => !url.includes('checksums.txt'))).toHaveLength(2);
|
|
});
|
|
|
|
it('reuses checksum verification for releases without sigstore bundles', async () => {
|
|
cosignPath = '/usr/local/bin/cosign';
|
|
|
|
await goreleaser.install('goreleaser', 'v2.12.4');
|
|
await goreleaser.install('goreleaser', 'v2.12.4');
|
|
|
|
expect(exec).not.toHaveBeenCalled();
|
|
expect(cacheDir).toHaveBeenCalledTimes(1);
|
|
expect(downloadTool.mock.calls.filter(([url]) => !url.includes('checksums.txt'))).toHaveLength(1);
|
|
});
|
|
});
|