Add OpenTofu version file support (#59)
Some checks failed
Continuous Integration / Check dist/ directory (push) Has been cancelled
Continuous Integration / Test (push) Has been cancelled
Setup OpenTofu / OpenTofu Version Files (push) Has been cancelled
Setup OpenTofu / OpenTofu Versions (push) Has been cancelled
Setup OpenTofu / OpenTofu Arguments (push) Has been cancelled
Setup OpenTofu / OpenTofu Run Local (push) Has been cancelled
Setup OpenTofu / OpenTofu Cloud Credentials (push) Has been cancelled
Setup OpenTofu / OpenTofu Enterprise Credentials (push) Has been cancelled
Setup OpenTofu / OpenTofu No Credentials (push) Has been cancelled

Signed-off-by: Brendon Smith <bws@bws.bio>
This commit is contained in:
Brendon Smith 2025-08-04 08:32:54 -04:00 committed by GitHub
parent 4a98c1282d
commit 000eeb8522
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 258 additions and 2 deletions

View file

@ -0,0 +1 @@
latest

View file

@ -14,6 +14,25 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
tofu-version-files:
name: 'OpenTofu Version Files'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
tofu-version-files: ['./.github/workflows/data/local/.opentofu-version']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup OpenTofu - ${{ matrix['tofu-version-files'] }}
uses: ./
with:
tofu_version_file: ${{ matrix['tofu-version-files'] }}
tofu_wrapper: false
- name: Validate that OpenTofu was installed
run: tofu version | grep 'OpenTofu v'
tofu-versions:
name: 'OpenTofu Versions'
runs-on: ${{ matrix.os }}

View file

@ -28,6 +28,17 @@ steps:
tofu_version: 1.6.0
```
You can also specify the version in a file (e.g., `.opentofu-version`):
```yaml
steps:
- uses: opentofu/setup-opentofu@v1
with:
tofu_version_file: .opentofu-version
```
Supported version syntax is the same as for the `tofu_version` input. If both `tofu_version` and `tofu_version_file` are provided, the version number in the file takes precedence.
Credentials for Terraform Cloud ([app.terraform.io](https://app.terraform.io/)) can be configured:
```yaml
@ -248,6 +259,8 @@ The action supports the following inputs:
for available range specifications). Examples are: `<1.6.0-beta`, `~1.6.0-alpha`, `1.6.0-alpha2` (all three installing
the latest available `1.6.0-alpha2` version). Prerelease versions can be specified and a range will stay within the
given tag such as `beta` or `rc`. If no version is given, it will default to `latest`.
- `tofu_version_file` - (optional) Path to a file containing the OpenTofu version to install. Supported version syntax
is the same as for the `tofu_version` input. Takes precedence over `tofu_version` if both are provided.
- `tofu_wrapper` - (optional) Whether to install a wrapper to wrap subsequent calls of
the `tofu` binary and expose its STDOUT, STDERR, and exit code as outputs
named `stdout`, `stderr`, and `exitcode` respectively. Defaults to `true`.

View file

@ -13,6 +13,9 @@ inputs:
description: 'The version of OpenTofu CLI to install. If no version is given, it will default to `latest`.'
default: 'latest'
required: false
tofu_version_file:
description: 'Path to a file containing the OpenTofu version to install. Takes precedence over `tofu_version` if both are provided.'
required: false
tofu_wrapper:
description: 'Whether or not to install a wrapper to wrap subsequent calls of the `tofu` binary and expose its STDOUT, STDERR, and exit code as outputs named `stdout`, `stderr`, and `exitcode` respectively. Defaults to `true`.'
default: 'true'

24
dist/index.js vendored
View file

@ -247,7 +247,8 @@ credentials "${credentialsHostname}" {
async function run () {
try {
// Gather GitHub Actions inputs
const version = core.getInput('tofu_version');
let version = core.getInput('tofu_version');
const versionFile = core.getInput('tofu_version_file');
const credentialsHostname = core.getInput('cli_config_credentials_hostname');
const credentialsToken = core.getInput('cli_config_credentials_token');
const wrapper = core.getInput('tofu_wrapper') === 'true';
@ -258,6 +259,27 @@ async function run () {
githubToken = process.env.GITHUB_TOKEN;
}
// If tofu_version_file is provided, read the version from the file
if (versionFile) {
try {
core.debug(`Reading OpenTofu version from file: ${versionFile}`);
const fileVersion = await fs.readFile(versionFile, 'utf8');
const trimmedVersion = fileVersion.trim();
if (trimmedVersion) {
version = trimmedVersion;
core.debug(`Using version from file: ${version}`);
} else {
core.warning(
`Version file ${versionFile} is empty, using tofu_version input: ${version}`
);
}
} catch (error) {
core.warning(
`Failed to read version from file ${versionFile}: ${error.message}. Using tofu_version input: ${version}`
);
}
}
// Gather OS details
const osPlatform = os.platform();
const osArch = os.arch();

View file

@ -128,7 +128,8 @@ credentials "${credentialsHostname}" {
async function run () {
try {
// Gather GitHub Actions inputs
const version = core.getInput('tofu_version');
let version = core.getInput('tofu_version');
const versionFile = core.getInput('tofu_version_file');
const credentialsHostname = core.getInput('cli_config_credentials_hostname');
const credentialsToken = core.getInput('cli_config_credentials_token');
const wrapper = core.getInput('tofu_wrapper') === 'true';
@ -139,6 +140,27 @@ async function run () {
githubToken = process.env.GITHUB_TOKEN;
}
// If tofu_version_file is provided, read the version from the file
if (versionFile) {
try {
core.debug(`Reading OpenTofu version from file: ${versionFile}`);
const fileVersion = await fs.readFile(versionFile, 'utf8');
const trimmedVersion = fileVersion.trim();
if (trimmedVersion) {
version = trimmedVersion;
core.debug(`Using version from file: ${version}`);
} else {
core.warning(
`Version file ${versionFile} is empty, using tofu_version input: ${version}`
);
}
} catch (error) {
core.warning(
`Failed to read version from file ${versionFile}: ${error.message}. Using tofu_version input: ${version}`
);
}
}
// Gather OS details
const osPlatform = os.platform();
const osArch = os.arch();

176
lib/test/setup-tofu.test.js Normal file
View file

@ -0,0 +1,176 @@
// Node.js core
const fs = require('fs').promises;
const os = require('os');
const path = require('path');
// External
const core = require('@actions/core');
// First party
const releases = require('../releases');
const setup = require('../setup-tofu');
// Mock dependencies
jest.mock('@actions/core');
jest.mock('@actions/io', () => ({
mv: jest.fn(),
cp: jest.fn(),
mkdirP: jest.fn()
}));
jest.mock('@actions/tool-cache', () => ({
downloadTool: jest.fn(),
extractZip: jest.fn()
}));
// Mock releases.js so setup-tofu.js can be tested in isolation
jest.mock('../releases');
// Set up global test fixtures
const fallbackVersion = 'latest';
let tempDir;
let tempDirPath;
let version = '1.10.5';
let versionFile;
let versionFileName = '.opentofu-version';
describe('setup-tofu', () => {
beforeAll(async () => {
// Mock dependencies
const tc = require('@actions/tool-cache');
tc.downloadTool.mockResolvedValue('/mock/download/path');
tc.extractZip.mockResolvedValue('/mock/extract/path');
const io = require('@actions/io');
io.mv.mockResolvedValue();
io.cp.mockResolvedValue();
io.mkdirP.mockResolvedValue();
const mockRelease = {
getBuild: jest.fn().mockReturnValue({ url: 'mock-url' })
};
releases.getRelease.mockResolvedValue(mockRelease);
// Write version file to temporary directory
tempDirPath = path.join(os.tmpdir(), 'setup-tofu-');
tempDir = await fs.mkdtemp(tempDirPath);
versionFile = path.join(tempDir, versionFileName);
await fs.writeFile(versionFile, `${version}\n`);
// Mock action inputs to return default values
core.getInput.mockImplementation((name) => {
const defaults = {
tofu_version: fallbackVersion,
tofu_version_file: versionFile,
cli_config_credentials_hostname: '',
cli_config_credentials_token: '',
tofu_wrapper: 'true',
github_token: ''
};
return defaults[name] || '';
});
// Mock environment variables
process.env.GITHUB_TOKEN = 'mock-github-token';
});
beforeEach(() => {
jest.clearAllMocks();
});
afterAll(async () => {
delete process.env.GITHUB_TOKEN;
});
describe('tofu_version_file functionality', () => {
it('should read version from file when tofu_version_file is provided', async () => {
jest.spyOn(fs, 'readFile');
await setup();
expect(releases.getRelease).toHaveBeenCalledWith(
version, process.env.GITHUB_TOKEN
);
expect(fs.readFile).toHaveBeenCalled();
});
it('should handle empty version file gracefully', async () => {
jest.spyOn(fs, 'readFile');
version = ' ';
versionFileName = '.opentofu-version-empty';
versionFile = path.join(tempDir, versionFileName);
await fs.writeFile(versionFile, `${version}\n`);
core.getInput.mockImplementation((name) => {
if (name === 'tofu_version_file') {
return versionFile;
}
if (name === 'tofu_version') {
return fallbackVersion;
}
return '';
});
await setup();
expect(releases.getRelease).toHaveBeenCalledWith(
fallbackVersion, process.env.GITHUB_TOKEN
);
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining(`Version file ${versionFile} is empty`)
);
expect(fs.readFile).toHaveBeenCalled();
});
it('should handle file read errors gracefully', async () => {
jest.spyOn(fs, 'readFile');
versionFileName = '.opentofu-version-file-does-not-exist';
versionFile = path.join(tempDir, versionFileName);
core.getInput.mockImplementation((name) => {
if (name === 'tofu_version_file') {
return versionFile;
}
if (name === 'tofu_version') {
return fallbackVersion;
}
return '';
});
await setup();
expect(releases.getRelease).toHaveBeenCalledWith(
fallbackVersion, process.env.GITHUB_TOKEN
);
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining(`Failed to read version from file ${versionFile}`)
);
expect(fs.readFile).toHaveBeenCalled();
});
it('should not read file when tofu_version_file is not provided', async () => {
jest.spyOn(fs, 'readFile');
core.getInput.mockImplementation((name) => {
if (name === 'tofu_version') {
return fallbackVersion;
}
return '';
});
await setup();
expect(releases.getRelease).toHaveBeenCalledWith(
fallbackVersion, process.env.GITHUB_TOKEN
);
expect(fs.readFile).not.toHaveBeenCalled();
});
});
});