feat: add version-file input

Resolves the GoReleaser version from a file. Currently supports the
asdf/mise `.tool-versions` format; resolved value takes precedence
over the `version` input.

  # .tool-versions
  goreleaser 2.13.0

  - uses: goreleaser/goreleaser-action@v7
    with:
      version-file: .tool-versions
      args: release --clean

Path is resolved relative to `workdir` unless absolute. Bare semvers
are auto-prefixed with `v`; constraint expressions and `latest` are
returned as-is. Multiple fallback versions per asdf convention are
accepted but only the first is used.

Refs #541
Closes #542

Co-authored-by: Anthony Couvreur <22034450+acouvreur@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker 2026-04-18 16:27:46 -03:00
parent 15fa2a96d4
commit 5fd99f0f14
No known key found for this signature in database
7 changed files with 204 additions and 4 deletions

View file

@ -222,11 +222,28 @@ Following inputs can be used as `step.with` keys
|------------------|---------|--------------|------------------------------------------------------------------|
| `distribution` | String | `goreleaser` | GoReleaser distribution, either `goreleaser` or `goreleaser-pro` |
| `version`**¹** | String | `~> v2` | GoReleaser version |
| `version-file`**²** | String | | Read the GoReleaser version from a file (see below) |
| `args` | String | | Arguments to pass to GoReleaser |
| `workdir` | String | `.` | Working directory (below repository root) |
| `install-only` | Bool | `false` | Just install GoReleaser |
> **¹** Can be a fixed version like `v0.117.0` or a max satisfying semver one like `~> 0.132`. In this case this will return `v0.132.1`.
>
> **²** Path to a file containing the GoReleaser version. Resolved relative
> to `workdir`. Currently only [`.tool-versions`](https://asdf-vm.com/manage/configuration.html#tool-versions)
> (asdf/mise) format is supported. When set, this takes precedence over `version`.
>
> ```yaml
> # .tool-versions
> goreleaser 2.13.0
> ```
>
> ```yaml
> - uses: goreleaser/goreleaser-action@v7
> with:
> version-file: .tool-versions
> args: release --clean
> ```
### outputs

117
__tests__/version.test.ts Normal file
View file

@ -0,0 +1,117 @@
import {describe, expect, it, beforeEach, afterEach} from '@jest/globals';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {getRequestedVersion} from '../src/version';
import {Inputs} from '../src/context';
const baseInputs = (overrides: Partial<Inputs>): Inputs => ({
distribution: 'goreleaser',
version: '~> v2',
versionFile: '',
args: '',
workdir: '.',
installOnly: false,
...overrides
});
describe('getRequestedVersion', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'goreleaser-version-'));
});
afterEach(() => {
fs.rmSync(tmpDir, {recursive: true, force: true});
});
const writeToolVersions = (content: string, name = '.tool-versions'): void => {
fs.writeFileSync(path.join(tmpDir, name), content);
};
describe('without version-file', () => {
it('returns the version input as-is', () => {
expect(getRequestedVersion(baseInputs({version: 'v1.2.3'}))).toBe('v1.2.3');
});
it('returns the default version when none is provided', () => {
expect(getRequestedVersion(baseInputs({version: '~> v2'}))).toBe('~> v2');
});
});
describe('with .tool-versions', () => {
it('parses an unprefixed version and adds the v prefix', () => {
writeToolVersions('goreleaser 1.2.3\n');
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('v1.2.3');
});
it('keeps an existing v prefix without doubling it', () => {
writeToolVersions('goreleaser v1.2.3\n');
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('v1.2.3');
});
it('takes precedence over the version input', () => {
writeToolVersions('goreleaser 1.2.3\n');
expect(getRequestedVersion(baseInputs({version: 'v9.9.9', versionFile: '.tool-versions', workdir: tmpDir}))).toBe(
'v1.2.3'
);
});
it('ignores other tools and picks goreleaser', () => {
writeToolVersions(['nodejs 20.10.0', 'goreleaser 2.13.0', 'python 3.12.1', ''].join('\n'));
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('v2.13.0');
});
it('skips full-line and inline comments', () => {
writeToolVersions(['# pinned for CI', 'goreleaser 2.13.0 # minimum cosign-verifiable', ''].join('\n'));
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('v2.13.0');
});
it('preserves "latest"', () => {
writeToolVersions('goreleaser latest\n');
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('latest');
});
it('uses only the first version when multiple fallbacks are listed', () => {
// asdf supports listing fallback versions; we install the first match.
writeToolVersions('goreleaser 2.13.0 2.12.4\n');
expect(getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toBe('v2.13.0');
});
it('accepts an absolute path and ignores workdir', () => {
const abs = path.join(tmpDir, '.tool-versions');
fs.writeFileSync(abs, 'goreleaser 2.13.0\n');
expect(getRequestedVersion(baseInputs({versionFile: abs, workdir: '/nonexistent'}))).toBe('v2.13.0');
});
it('throws when the file does not exist', () => {
expect(() => getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toThrow(
/version-file not found/
);
});
it('throws when the file has no goreleaser entry', () => {
writeToolVersions(['nodejs 20.10.0', 'python 3.12.1', ''].join('\n'));
expect(() => getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toThrow(
/No goreleaser entry/
);
});
it('throws when the goreleaser entry has no version', () => {
writeToolVersions('goreleaser\n');
expect(() => getRequestedVersion(baseInputs({versionFile: '.tool-versions', workdir: tmpDir}))).toThrow(
/No version specified for goreleaser/
);
});
});
describe('with an unsupported file', () => {
it('throws a clear error', () => {
fs.writeFileSync(path.join(tmpDir, '.go-version'), '1.2.3\n');
expect(() => getRequestedVersion(baseInputs({versionFile: '.go-version', workdir: tmpDir}))).toThrow(
/Unsupported version-file/
);
});
});
});

View file

@ -15,6 +15,12 @@ inputs:
description: 'GoReleaser version'
default: '~> v2'
required: false
version-file:
description: |
Read the GoReleaser version from a file. Path is resolved relative to
`workdir`. Currently only `.tool-versions` (asdf/mise) is supported.
When set, takes precedence over `version`.
required: false
args:
description: 'Arguments to pass to GoReleaser'
required: false

4
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@ export const osArch: string = os.arch();
export interface Inputs {
distribution: string;
version: string;
versionFile: string;
args: string;
workdir: string;
installOnly: boolean;
@ -16,6 +17,7 @@ export async function getInputs(): Promise<Inputs> {
return {
distribution: core.getInput('distribution') || 'goreleaser',
version: core.getInput('version') || '~> v2',
versionFile: core.getInput('version-file'),
args: core.getInput('args'),
workdir: core.getInput('workdir') || '.',
installOnly: core.getBooleanInput('install-only')

View file

@ -4,14 +4,16 @@ import yargs from 'yargs';
import type {Arguments} from 'yargs';
import * as context from './context';
import * as goreleaser from './goreleaser';
import {getRequestedVersion} from './version';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
async function run(): Promise<void> {
try {
const inputs: context.Inputs = await context.getInputs();
const bin = await goreleaser.install(inputs.distribution, inputs.version);
core.info(`GoReleaser ${inputs.version} installed successfully`);
const version = getRequestedVersion(inputs);
const bin = await goreleaser.install(inputs.distribution, version);
core.info(`GoReleaser ${version} installed successfully`);
if (inputs.installOnly) {
const goreleaserDir = path.dirname(bin);

56
src/version.ts Normal file
View file

@ -0,0 +1,56 @@
import * as fs from 'fs';
import * as path from 'path';
import {Inputs} from './context';
// Resolves the GoReleaser version to install.
//
// When `version-file` is set, it is read from disk and parsed; the resolved
// value takes precedence over the `version` input. Otherwise, `version` is
// returned as-is (it always has a default — see context.getInputs).
export function getRequestedVersion(inputs: Inputs): string {
if (!inputs.versionFile) {
return inputs.version;
}
const filePath = path.isAbsolute(inputs.versionFile)
? inputs.versionFile
: path.join(inputs.workdir || '.', inputs.versionFile);
if (!fs.existsSync(filePath)) {
throw new Error(`version-file not found: ${filePath}`);
}
const basename = path.basename(filePath);
const content = fs.readFileSync(filePath, 'utf-8');
switch (basename) {
case '.tool-versions':
return parseToolVersions(content, filePath);
default:
throw new Error(`Unsupported version-file: ${filePath} (only .tool-versions is supported)`);
}
}
// Parses a single `goreleaser <version>` entry out of a `.tool-versions` file
// (asdf/mise format). Full-line `#` comments and inline `# ...` suffixes are
// stripped. When a tool lists multiple fallback versions only the first is
// used. Bare semvers are returned with a leading `v`; constraint expressions
// (`~> v2`, `latest`, ...) are returned as-is.
function parseToolVersions(content: string, filePath: string): string {
for (const rawLine of content.split('\n')) {
const line = rawLine.replace(/#.*$/, '').trim();
if (!line) {
continue;
}
const tokens = line.split(/\s+/);
if (tokens[0] !== 'goreleaser') {
continue;
}
const version = tokens[1];
if (!version) {
throw new Error(`No version specified for goreleaser in ${filePath}`);
}
return /^\d/.test(version) ? `v${version}` : version;
}
throw new Error(`No goreleaser entry found in ${filePath}`);
}