feat: resolve nightly to latest vX.Y.Z-<sha>-nightly release (#558)

* feat: resolve nightly to latest vX.Y.Z-<sha>-nightly release

Query GitHub releases API to resolve the 'nightly' version input to the
latest immutable nightly tag, replacing the moving 'nightly' tag that is
being removed for supply-chain hardening.

Refs goreleaser/goreleaser#6550

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: keep legacy 'nightly' tag working during transition

Fall back to the moving 'nightly' tag when no immutable
vX.Y.Z-<sha>-nightly release is found, so the action keeps working
between this release and the goreleaser nightly switchover.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: assert isNightlyTag accepts legacy fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: accept nightly tags without 'v' prefix

goreleaser-pro publishes nightly releases as e.g. 2.16.0-eaeb08c50-nightly
(no 'v' prefix). Make the nightly tag regex tolerate either form, and
split the integration tests so OSS asserts the legacy fallback while
Pro asserts the new <version>-<sha>-nightly format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "fix: accept nightly tags without 'v' prefix"

The missing 'v' prefix on the goreleaser-pro nightly was a release
mistake; new nightlies will keep the 'v' prefix.

This reverts commit 7673f7f.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: pass GITHUB_TOKEN to tests

The new nightly resolution hits api.github.com/repos/.../releases,
which is rate-limited for unauthenticated requests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: note GITHUB_TOKEN need for nightly resolution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker 2026-04-26 16:39:25 -03:00 committed by GitHub
parent 4f96abf297
commit 4c6ab561ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 74 additions and 9 deletions

View file

@ -39,6 +39,8 @@ jobs:
-
name: Test
run: npm test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0

View file

@ -96,6 +96,11 @@ checksums file against the GoReleaser release workflow's OIDC identity. If
> versions the cosign step is silently skipped — only the `checksums.txt`
> SHA-256 verification runs.
> **Note**: when `version: nightly` is used, the action resolves the
> latest immutable `vX.Y.Z-<sha>-nightly` release from the GitHub
> Releases API. Pass `GITHUB_TOKEN` to the action step (as in the example
> above) to avoid unauthenticated API rate limits.
To enable signature verification, install cosign before running the action:
```yaml

View file

@ -56,16 +56,18 @@ describe('getRelease', () => {
expect(release?.tag_name).not.toEqual('');
});
it('returns nightly GoReleaser GitHub release', async () => {
it('resolves nightly to the legacy nightly tag for OSS GoReleaser', async () => {
// No <version>-<sha>-nightly release exists in goreleaser/goreleaser yet,
// so this should fall back to the legacy moving `nightly` tag.
const release = await github.getRelease('goreleaser', 'nightly');
expect(release).not.toBeNull();
expect(release?.tag_name).not.toEqual('');
expect(release.tag_name).toEqual('nightly');
});
it('returns nightly GoReleaser Pro GitHub release', async () => {
it('resolves nightly to a <version>-<sha>-nightly release for GoReleaser Pro', async () => {
const release = await github.getRelease('goreleaser-pro', 'nightly');
expect(release).not.toBeNull();
expect(release?.tag_name).not.toEqual('');
expect(release.tag_name).toMatch(github.nightlyTagRegex);
});
it('returns v0.182.0 GoReleaser Pro GitHub release', async () => {

View file

@ -104,17 +104,29 @@ describe('getCertificateIdentity', () => {
);
});
it('uses nightly-oss.yml@refs/heads/main for OSS nightly', () => {
it('uses nightly-oss.yml@refs/heads/main for OSS legacy nightly tag', () => {
expect(goreleaser.getCertificateIdentity('goreleaser', 'nightly')).toEqual(
'https://github.com/goreleaser/goreleaser/.github/workflows/nightly-oss.yml@refs/heads/main'
);
});
it('uses nightly-pro.yml@refs/heads/main for Pro nightly', () => {
it('uses nightly-pro.yml@refs/heads/main for Pro legacy nightly tag', () => {
expect(goreleaser.getCertificateIdentity('goreleaser-pro', 'nightly')).toEqual(
'https://github.com/goreleaser/goreleaser-pro-internal/.github/workflows/nightly-pro.yml@refs/heads/main'
);
});
it('uses nightly-oss.yml@refs/heads/main for OSS nightly tag', () => {
expect(goreleaser.getCertificateIdentity('goreleaser', 'v2.16.0-abc1234-nightly')).toEqual(
'https://github.com/goreleaser/goreleaser/.github/workflows/nightly-oss.yml@refs/heads/main'
);
});
it('uses nightly-pro.yml@refs/heads/main for Pro nightly tag', () => {
expect(goreleaser.getCertificateIdentity('goreleaser-pro', 'v2.16.0-eaeb08c50-nightly')).toEqual(
'https://github.com/goreleaser/goreleaser-pro-internal/.github/workflows/nightly-pro.yml@refs/heads/main'
);
});
});
describe('verifyChecksum', () => {

2
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

View file

@ -30,6 +30,13 @@ export interface GitHubRelease {
tag_name: string;
}
// Matches the new-style nightly release tag pattern: vX.Y.Z-<sha>-nightly
export const nightlyTagRegex = /^v\d+\.\d+\.\d+-[0-9a-f]+-nightly$/i;
export const isNightlyTag = (tag: string): boolean => {
return tag === 'nightly' || nightlyTagRegex.test(tag);
};
export const getRelease = async (distribution: string, version: string): Promise<GitHubRelease> => {
if (version === 'latest') {
core.warning("You are using 'latest' as default version. Will lock to '~> v2'.");
@ -40,7 +47,7 @@ export const getRelease = async (distribution: string, version: string): Promise
export const getReleaseTag = async (distribution: string, version: string): Promise<GitHubRelease> => {
if (version === 'nightly') {
return {tag_name: version};
return resolveNightly(distribution);
}
// If version is a specific version (not a range), skip the JSON check
@ -81,6 +88,43 @@ export const getReleaseTag = async (distribution: string, version: string): Prom
throw new Error(`Cannot find GoReleaser release ${version} in ${url}`);
};
// resolveNightly looks up the latest immutable nightly release of the form
// `vX.Y.Z-<sha>-nightly` on the GitHub releases of the given distribution.
const resolveNightly = async (distribution: string): Promise<GitHubRelease> => {
const url = `https://api.github.com/repos/goreleaser/${distribution}/releases?per_page=100`;
core.debug(`Resolving latest nightly release from ${url}`);
const releases = await withRetry(async () => {
const http: httpm.HttpClient = new httpm.HttpClient('goreleaser-action');
const headers: {[name: string]: string} = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};
const token = process.env.GITHUB_TOKEN;
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const resp: httpm.HttpClientResponse = await http.get(url, headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`Failed to list releases from ${url} with status code ${statusCode}: ${body}`);
}
return <Array<GitHubRelease>>JSON.parse(body);
});
const match = releases.find(r => nightlyTagRegex.test(r.tag_name));
if (match) {
core.info(`Resolved nightly to ${match.tag_name}`);
return match;
}
// Fallback to the legacy moving `nightly` tag during the transition period,
// until goreleaser stops publishing it.
core.warning(`No '<version>-<sha>-nightly' release found in ${url}, falling back to 'nightly' tag`);
return {tag_name: 'nightly'};
};
const resolveVersion = async (distribution: string, version: string): Promise<string | null> => {
const allTags: Array<string> | null = await getAllTags(distribution);
if (!allTags) {

View file

@ -120,7 +120,7 @@ async function verifyCosignSignature(
export const getCertificateIdentity = (distribution: string, tag: string): string => {
const pro = isPro(distribution);
if (tag === 'nightly') {
if (github.isNightlyTag(tag)) {
const workflow = pro ? 'nightly-pro.yml' : 'nightly-oss.yml';
const repo = pro ? 'goreleaser-pro-internal' : 'goreleaser';
return `https://github.com/goreleaser/${repo}/.github/workflows/${workflow}@refs/heads/main`;