diff --git a/README.md b/README.md index 55456e5..dff68d8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/action.yml b/action.yml index 4f9056a..5de48a3 100644 --- a/action.yml +++ b/action.yml @@ -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: '' diff --git a/lib/setup-tofu.js b/lib/setup-tofu.js index 6e4eb62..1810a97 100644 --- a/lib/setup-tofu.js +++ b/lib/setup-tofu.js @@ -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) { diff --git a/lib/test/setup-tofu.test.js b/lib/test/setup-tofu.test.js index ca6d4ab..9e478a1 100644 --- a/lib/test/setup-tofu.test.js +++ b/lib/test/setup-tofu.test.js @@ -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(); + }); + }); });