This commit is contained in:
Victor Martinez 2025-12-16 15:56:42 +00:00 committed by GitHub
commit eec3116157
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 314 additions and 2 deletions

View file

@ -31,6 +31,15 @@ steps:
terraform_version: "1.1.7"
```
A specific version of Terraform CLI can be installed using a version file:
```yaml
steps:
- uses: hashicorp/setup-terraform@v3
with:
terraform_version_file: ".tool-versions"
```
Credentials for HCP Terraform ([app.terraform.io](https://app.terraform.io/)) can be configured:
```yaml
@ -251,10 +260,24 @@ The action supports the following inputs:
for available range specifications). Examples are: `"<1.2.0"`, `"~1.1.0"`, `"1.1.7"` (all three installing
the latest available `1.1` 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`.
- `terraform_version_file` - (optional) The path to a file containing terraform version. Supported file types are `.tool-versions` or `.terraform-version`. See more details in [about version-file](#Terraform-version-file).
- `terraform_wrapper` - (optional) Whether to install a wrapper to wrap subsequent calls of
the `terraform` binary and expose its STDOUT, STDERR, and exit code as outputs
named `stdout`, `stderr`, and `exitcode` respectively. Defaults to `true`.
### Terraform version file
If the `terraform_version_file` input is specified, the action will extract the version from the file and install it.
Supported files names are `.tool-versions` or `.terraform-version`.
In `.tool-versions` file, terraform version should be preceded by the terraform keyword (e.g., `terraform 1.13.0`).
The `.tool-versions` file supports version specifications in accordance with Semantic Versioning ([semver](https://semver.org/)) and [Semver Ranges](https://www.npmjs.com/package/semver#ranges).
The `.terraform-version` file supports version specifications as explained in the `terraform_version` input.
If both `terraform_version` and `terraform_version_file` inputs are provided, the `terraform_version` input will be used.
If the file contains multiple versions, only the first one will be recognized.
## Outputs
This action does not configure any outputs directly. However, when you set the `terraform_wrapper` input

View file

@ -11,7 +11,9 @@ inputs:
required: false
terraform_version:
description: 'The version of Terraform CLI to install. Instead of full version string you can also specify constraint string starting with "<" (for example `<1.13.0`) to install the latest version satisfying the constraint. A value of `latest` will install the latest version of Terraform CLI. Defaults to `latest`.'
default: 'latest'
required: false
terraform_version_file:
description: 'The path to the `.terraform-version` file. See examples of supported syntax in README file'
required: false
terraform_wrapper:
description: 'Whether or not to install a wrapper to wrap subsequent calls of the `terraform` binary and expose its STDOUT, STDERR, and exit code as outputs named `stdout`, `stderr`, and `exitcode` respectively. Defaults to `true`.'

View file

@ -121,14 +121,70 @@ credentials "${credentialsHostname}" {
await fs.writeFile(credsFile, creds);
}
async function getVersionFromFileContent (versionFile) {
if (!versionFile) {
return;
}
let versionRegExp;
const versionFileName = path.basename(versionFile);
if (versionFileName === '.tool-versions') {
versionRegExp = /^(terraform\s+)(?<version>[^\s]+)$/m;
} else if (versionFileName === '.terraform-version') {
versionRegExp = /(?<version>[^\s]+)/;
} else {
return;
}
try {
const content = fs.readFileSync(versionFile).toString().trim();
let fileContent = '';
if (content.match(versionRegExp)?.groups?.version) {
fileContent = content.match(versionRegExp)?.groups?.version;
}
if (!fileContent) {
return;
}
core.debug(`Version from file '${fileContent}'`);
return fileContent;
} catch (error) {
if (error.code === 'ENOENT') {
return;
}
throw error;
}
}
// get the Terraform version from the action inputs
async function getTerraformVersion (versionInput, versionFile) {
const DEFAULT_VERSION = 'latest';
let version = versionInput;
if (!versionInput && !versionFile) {
core.info(`Set default value for version to ${DEFAULT_VERSION}`);
version = DEFAULT_VERSION;
}
if (!version && versionFile) {
version = await getVersionFromFileContent(versionFile);
if (!version) {
throw new Error(`No supported version was found in file ${versionFile}`);
}
}
return version;
}
async function run () {
try {
// Gather GitHub Actions inputs
const version = core.getInput('terraform_version');
const versionInput = core.getInput('terraform_version', { required: false });
const versionFile = core.getInput('terraform_version_file', { required: false });
const credentialsHostname = core.getInput('cli_config_credentials_hostname');
const credentialsToken = core.getInput('cli_config_credentials_token');
const wrapper = core.getInput('terraform_wrapper') === 'true';
const version = await getTerraformVersion(versionInput, versionFile);
// Gather OS details
const osPlatform = os.platform();
const osArch = os.arch();

View file

@ -44,12 +44,14 @@ describe('Setup Terraform', () => {
test('gets specific version and adds token and hostname on linux, amd64', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -86,12 +88,14 @@ describe('Setup Terraform', () => {
test('gets specific version and adds token and hostname on windows, 386', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -131,12 +135,14 @@ describe('Setup Terraform', () => {
test('gets latest version and adds token and hostname on linux, amd64', async () => {
const version = 'latest';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -174,12 +180,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching specification adds token and hostname on linux, amd64', async () => {
const version = '<0.10.0';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -217,12 +225,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching tilde range patch', async () => {
const version = '~0.1.0';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -259,12 +269,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching tilde range minor', async () => {
const version = '~0.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -301,12 +313,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching tilde range minor', async () => {
const version = '~0';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -343,12 +357,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching .X range ', async () => {
const version = '0.1.x';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -385,12 +401,14 @@ describe('Setup Terraform', () => {
test('gets latest version matching - range ', async () => {
const version = '0.1.0 - 0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -427,12 +445,14 @@ describe('Setup Terraform', () => {
test('fails when metadata cannot be downloaded', async () => {
const version = 'latest';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -449,12 +469,14 @@ describe('Setup Terraform', () => {
test('fails when specific version cannot be found', async () => {
const version = '0.9.9';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -471,12 +493,14 @@ describe('Setup Terraform', () => {
test('fails when CLI for os and architecture cannot be found', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -509,12 +533,14 @@ describe('Setup Terraform', () => {
test('fails when CLI cannot be downloaded', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
@ -547,6 +573,7 @@ describe('Setup Terraform', () => {
test('installs wrapper on linux', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
const wrapperPath = path.resolve([__dirname, '..', 'wrapper', 'dist', 'index.js'].join(path.sep));
@ -559,6 +586,7 @@ describe('Setup Terraform', () => {
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken)
.mockReturnValueOnce('true');
@ -591,6 +619,7 @@ describe('Setup Terraform', () => {
test('installs wrapper on windows', async () => {
const version = '0.1.1';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
const wrapperPath = path.resolve([__dirname, '..', 'wrapper', 'dist', 'index.js'].join(path.sep));
@ -603,6 +632,7 @@ describe('Setup Terraform', () => {
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken)
.mockReturnValueOnce('true');
@ -632,4 +662,205 @@ describe('Setup Terraform', () => {
expect(ioMv).toHaveBeenCalledWith(`file${path.sep}terraform.exe`, `file${path.sep}terraform-bin.exe`);
expect(ioCp).toHaveBeenCalledWith(wrapperPath, `file${path.sep}terraform`);
});
test('gets version from .tool-versions file', async () => {
const version = '';
const versionFile = '.tool-versions';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
tc.downloadTool = jest
.fn()
.mockReturnValueOnce('file.zip');
tc.extractZip = jest
.fn()
.mockReturnValueOnce('file');
os.platform = jest
.fn()
.mockReturnValue('linux');
os.arch = jest
.fn()
.mockReturnValue('amd64');
nock('https://releases.hashicorp.com')
.get('/terraform/index.json')
.reply(200, json);
fs.readFileSync = jest
.fn()
.mockReturnValueOnce('terraform 0.1.1');
const versionObj = await setup();
expect(versionObj.version).toEqual('0.1.1');
});
test('gets version from version if both (version and .tool-versions file) are set', async () => {
const version = '0.1.0';
const versionFile = '.tool-versions';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
tc.downloadTool = jest
.fn()
.mockReturnValueOnce('file.zip');
tc.extractZip = jest
.fn()
.mockReturnValueOnce('file');
os.platform = jest
.fn()
.mockReturnValue('linux');
os.arch = jest
.fn()
.mockReturnValue('amd64');
nock('https://releases.hashicorp.com')
.get('/terraform/index.json')
.reply(200, json);
fs.readFileSync = jest
.fn()
.mockReturnValueOnce('terraform 0.10.0');
const versionObj = await setup();
expect(versionObj.version).toEqual('0.1.0');
});
test('gets latest version using empty inputs for versions', async () => {
const version = '';
const versionFile = '';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
tc.downloadTool = jest
.fn()
.mockReturnValueOnce('file.zip');
tc.extractZip = jest
.fn()
.mockReturnValueOnce('file');
os.platform = jest
.fn()
.mockReturnValue('linux');
os.arch = jest
.fn()
.mockReturnValue('amd64');
nock('https://releases.hashicorp.com')
.get('/terraform/index.json')
.reply(200, json);
const versionObj = await setup();
expect(versionObj.version).toEqual('0.10.0');
});
test('gets version from .terraform-version file', async () => {
const version = '';
const versionFile = '.terraform-version';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
tc.downloadTool = jest
.fn()
.mockReturnValueOnce('file.zip');
tc.extractZip = jest
.fn()
.mockReturnValueOnce('file');
os.platform = jest
.fn()
.mockReturnValue('linux');
os.arch = jest
.fn()
.mockReturnValue('amd64');
nock('https://releases.hashicorp.com')
.get('/terraform/index.json')
.reply(200, json);
fs.readFileSync = jest
.fn()
.mockReturnValueOnce('0.1.1');
const versionObj = await setup();
expect(versionObj.version).toEqual('0.1.1');
});
test('fails when unsupported terraform version file', async () => {
const version = '';
const versionFile = 'unsupported-file';
const credentialsHostname = 'app.terraform.io';
const credentialsToken = 'asdfjkl';
core.getInput = jest
.fn()
.mockReturnValueOnce(version)
.mockReturnValueOnce(versionFile)
.mockReturnValueOnce(credentialsHostname)
.mockReturnValueOnce(credentialsToken);
tc.downloadTool = jest
.fn()
.mockReturnValueOnce('file.zip');
tc.extractZip = jest
.fn()
.mockReturnValueOnce('file');
os.platform = jest
.fn()
.mockReturnValue('linux');
os.arch = jest
.fn()
.mockReturnValue('amd64');
nock('https://releases.hashicorp.com')
.get('/terraform/index.json')
.reply(200, json);
fs.readFileSync = jest
.fn()
.mockReturnValueOnce('0.10.0');
try {
await setup();
} catch (e) {
expect(core.error).toHaveBeenCalled();
}
});
});