add tool cache option

Signed-off-by: Michi U. <4169888+michemache@users.noreply.github.com>
This commit is contained in:
Michi U. 2025-12-19 14:00:10 +01:00
parent e95ccdd206
commit 3d18cc4a77
4 changed files with 115 additions and 3 deletions

View file

@ -67,6 +67,15 @@ steps:
tofu_wrapper: false
```
Caching can be enabled to reduce download time on subsequent workflow runs by storing the OpenTofu binary in GitHub Actions tool cache:
```yaml
steps:
- uses: opentofu/setup-opentofu@v1
with:
cache: true
```
Subsequent steps can access outputs when the wrapper script is installed:
```yaml
@ -264,6 +273,7 @@ The action supports the following inputs:
- `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`.
- `cache` - (optional) Whether to use GitHub Actions tool cache to store and reuse downloaded OpenTofu binaries, reducing installation time on subsequent workflow runs. Defaults to `false`.
- `github_token` - (optional) Override the GitHub token read from the environment variable. Defaults to the value of the `GITHUB_TOKEN` environment variable unless running on Forgejo or Gitea.
## Outputs

View file

@ -20,6 +20,10 @@ inputs:
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'
required: false
cache:
description: 'Whether to use GitHub Actions tool cache to store and reuse downloaded OpenTofu binaries, reducing installation time on subsequent workflow runs. Defaults to `false`.'
default: 'false'
required: false
github_token:
description: 'API token for GitHub to increase the rate limit. Defaults to the GITHUB_TOKEN environment variable unless running on Forgejo/Gitea.'
default: ''

View file

@ -133,6 +133,7 @@ async function run () {
const credentialsHostname = core.getInput('cli_config_credentials_hostname');
const credentialsToken = core.getInput('cli_config_credentials_token');
const wrapper = core.getInput('tofu_wrapper') === 'true';
const useCache = core.getInput('cache') === 'false';
let githubToken = core.getInput('github_token');
if (githubToken === '' && !(process.env.FORGEJO_ACTIONS || process.env.GITEA_ACTIONS)) {
// Only default to the environment variable when running in GitHub Actions. Don't do this for other CI systems
@ -174,8 +175,22 @@ async function run () {
throw new Error(`OpenTofu version ${version} not available for ${platform} and ${arch}`);
}
// Download requested version
const pathToCLI = await downloadAndExtractCLI(build.url);
// Download requested version if not cached
let pathToCLI;
if (useCache) {
const cachedPath = tc.find('tofu', release.version, arch);
if (cachedPath) {
core.debug(`Using cached OpenTofu version ${release.version} from ${cachedPath}`);
pathToCLI = cachedPath;
} else {
core.debug(`OpenTofu version ${release.version} not found in cache, downloading...`);
const extractedPath = await downloadAndExtractCLI(build.url);
core.debug(`Caching OpenTofu version ${release.version} to tool cache`);
pathToCLI = await tc.cacheDir(extractedPath, 'tofu', release.version, arch);
}
} else {
pathToCLI = await downloadAndExtractCLI(build.url);
}
// Install our wrapper
if (wrapper) {

View file

@ -19,7 +19,9 @@ jest.mock('@actions/io', () => ({
}));
jest.mock('@actions/tool-cache', () => ({
downloadTool: jest.fn(),
extractZip: jest.fn()
extractZip: jest.fn(),
find: jest.fn(),
cacheDir: jest.fn()
}));
// Mock releases.js so setup-tofu.js can be tested in isolation
@ -39,6 +41,8 @@ describe('setup-tofu', () => {
const tc = require('@actions/tool-cache');
tc.downloadTool.mockResolvedValue('/mock/download/path');
tc.extractZip.mockResolvedValue('/mock/extract/path');
tc.find.mockReturnValue(null); // Default to cache miss
tc.cacheDir.mockResolvedValue('/mock/cached/path');
const io = require('@actions/io');
io.mv.mockResolvedValue();
@ -46,6 +50,7 @@ describe('setup-tofu', () => {
io.mkdirP.mockResolvedValue();
const mockRelease = {
version: '1.10.5',
getBuild: jest.fn().mockReturnValue({ url: 'mock-url' })
};
releases.getRelease.mockResolvedValue(mockRelease);
@ -64,6 +69,7 @@ describe('setup-tofu', () => {
cli_config_credentials_hostname: '',
cli_config_credentials_token: '',
tofu_wrapper: 'true',
cache: 'false',
github_token: ''
};
return defaults[name] || '';
@ -173,4 +179,81 @@ describe('setup-tofu', () => {
expect(fs.readFile).not.toHaveBeenCalled();
});
});
describe('caching functionality', () => {
it('should use cached version when cache is enabled and found', async () => {
const tc = require('@actions/tool-cache');
tc.find.mockReturnValue('/mock/cached/path');
core.getInput.mockImplementation((name) => {
const defaults = {
tofu_version: fallbackVersion,
tofu_version_file: '',
cli_config_credentials_hostname: '',
cli_config_credentials_token: '',
tofu_wrapper: 'true',
cache: 'true',
github_token: ''
};
return defaults[name] || '';
});
await setup();
expect(tc.find).toHaveBeenCalledWith('tofu', '1.10.5', expect.any(String));
expect(tc.downloadTool).not.toHaveBeenCalled();
expect(tc.extractZip).not.toHaveBeenCalled();
expect(tc.cacheDir).not.toHaveBeenCalled();
});
it('should download and cache when cache is enabled but not found', async () => {
const tc = require('@actions/tool-cache');
tc.find.mockReturnValue(null); // Cache miss
core.getInput.mockImplementation((name) => {
const defaults = {
tofu_version: fallbackVersion,
tofu_version_file: '',
cli_config_credentials_hostname: '',
cli_config_credentials_token: '',
tofu_wrapper: 'true',
cache: 'true',
github_token: ''
};
return defaults[name] || '';
});
await setup();
expect(tc.find).toHaveBeenCalledWith('tofu', '1.10.5', expect.any(String));
expect(tc.downloadTool).toHaveBeenCalled();
expect(tc.extractZip).toHaveBeenCalled();
expect(tc.cacheDir).toHaveBeenCalledWith('/mock/extract/path', 'tofu', '1.10.5', expect.any(String));
});
it('should not use cache when cache is disabled', async () => {
const tc = require('@actions/tool-cache');
tc.find.mockReturnValue('/mock/cached/path');
core.getInput.mockImplementation((name) => {
const defaults = {
tofu_version: fallbackVersion,
tofu_version_file: '',
cli_config_credentials_hostname: '',
cli_config_credentials_token: '',
tofu_wrapper: 'true',
cache: 'false',
github_token: ''
};
return defaults[name] || '';
});
await setup();
expect(tc.find).not.toHaveBeenCalled();
expect(tc.downloadTool).toHaveBeenCalled();
expect(tc.extractZip).toHaveBeenCalled();
expect(tc.cacheDir).not.toHaveBeenCalled();
});
});
});