Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ jobs:
You are responsible for ensuring the `gcloud` version matches the features
and components required.

- <a name="__input_version_file"></a><a href="#user-content-__input_version_file"><code>version_file</code></a>: _(Optional)_ Path to a file containing the version or version constraint of the Cloud
SDK (`gcloud`) to install. Both a plain version file (containing only a
version string) and an asdf/mise-style `.tool-versions` file (containing a
`gcloud <version>` line) are supported:

- uses: 'google-github-actions/setup-gcloud@v3'
with:
version_file: '.tool-versions'

The `version` input takes precedence: if `version` is set to anything other
than the default `"latest"`, `version_file` is ignored.

- <a name="__input_project_id"></a><a href="#user-content-__input_project_id"><code>project_id</code></a>: _(Optional)_ ID of the Google Cloud project. If provided, this will configure gcloud to
use this project ID by default for commands. Individual commands can still
override the project using the `--project` flag which takes precedence. If
Expand Down
15 changes: 15 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ inputs:
default: 'latest'
required: false

version_file:
description: |-
Path to a file containing the version or version constraint of the Cloud
SDK (`gcloud`) to install. Both a plain version file (containing only a
version string) and an asdf/mise-style `.tool-versions` file (containing a
`gcloud <version>` line) are supported:

- uses: 'google-github-actions/setup-gcloud@v3'
with:
version_file: '.tool-versions'

The `version` input takes precedence: if `version` is set to anything other
than the default `"latest"`, `version_file` is ignored.
required: false

project_id:
description: |-
ID of the Google Cloud project. If provided, this will configure gcloud to
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,47 @@ import {
parseBoolean,
presence,
} from '@google-github-actions/actions-utils';
import { promises as fs } from 'fs';
import path from 'path';

// Do not listen to the linter - this can NOT be rewritten as an ES6 import
// statement.
const { version: appVersion } = require('../package.json');

/**
* parseVersionFile extracts a gcloud version or version constraint from the
* contents of a version file. It supports asdf/mise-style ".tool-versions"
* files (a "gcloud <version>" line) as well as plain version files containing
* just a version string. Blank lines and "#" comments are ignored.
*
* @param contents The raw contents of the version file.
* @returns The resolved version string, or an empty string if none was found.
*/
export function parseVersionFile(contents: string): string {
const lines = contents.split(/\r?\n/);

// First, look for an asdf/mise ".tool-versions" entry ("gcloud <version>"),
// which may appear on any line. asdf uses the first token as the version.
for (const line of lines) {
const toolVersionsMatch = line.trim().match(/^gcloud\s+(\S+)/i);
if (toolVersionsMatch) {
return toolVersionsMatch[1];
}
}

// Otherwise treat the file as a plain version file and return the first
// non-comment, non-blank line, so that a version constraint such as
// ">= 416.0.0" is preserved.
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
return trimmed;
}
}

return '';
}

export async function run(): Promise<void> {
// Note: unlike the other actions, we actually want to export this as a
// persistent variable for future steps. Other actions should set process.env
Expand Down Expand Up @@ -71,6 +106,28 @@ export async function run(): Promise<void> {
);
}
} else {
// If a version file was provided, resolve the version from it. An
// explicitly-provided "version" (anything other than the default
// "latest") takes precedence over the file.
const versionFile = presence(core.getInput('version_file'));
if (versionFile) {
if (version && version !== 'latest') {
core.warning(
`Both "version" ("${version}") and "version_file" were specified; ` +
`using "version" and ignoring "version_file".`,
);
} else {
const contents = await fs.readFile(versionFile, 'utf8');
version = presence(parseVersionFile(contents));
if (!version) {
throw new Error(
`Failed to resolve a gcloud version from version_file "${versionFile}"`,
);
}
core.debug(`resolved version "${version}" from version_file "${versionFile}"`);
}
}

// Compute the version information. If the version was not specified,
// accept any installed version. If the version was specified as "latest",
// compute the latest version. Otherwise, accept the version/version
Expand Down
72 changes: 70 additions & 2 deletions tests/setup-gcloud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,26 @@
import { mock, test } from 'node:test';
import assert from 'node:assert';

import { promises as fs } from 'fs';
import { promises as fs, writeFileSync } from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as setupGcloud from '@google-github-actions/setup-cloud-sdk';
import { TestToolCache } from '@google-github-actions/setup-cloud-sdk';
import * as core from '@actions/core';
import * as toolCache from '@actions/tool-cache';

import { run } from '../src/main';
import { parseVersionFile, run } from '../src/main';

/**
* writeTempVersionFile writes contents to a uniquely-named file in the OS temp
* directory and returns its path. It uses the synchronous fs API so it is
* unaffected by the mocked promise-based fs.writeFile in the test suite.
*/
const writeTempVersionFile = (name: string, contents: string): string => {
const tmp = path.join(os.tmpdir(), `setup-gcloud-${process.hrtime.bigint()}-${name}`);
writeFileSync(tmp, contents);
return tmp;
};

// These are mock data for github actions inputs, where camel case is expected.
const fakeInputs: { [key: string]: string } = {
Expand Down Expand Up @@ -214,6 +227,61 @@ test('#run', { concurrency: true }, async (suite) => {

assert.deepStrictEqual(mocks.setProject.mock.callCount(), 0);
});

await suite.test('resolves the version from a .tool-versions version_file', async (t) => {
const versionFile = writeTempVersionFile('.tool-versions', 'nodejs 20.0.0\ngcloud 5.6.7\n');
const mocks = defaultMocks(t.mock, {
version: '',
version_file: versionFile,
});
await run();

assert.deepStrictEqual(mocks.installGcloudSDK.mock.calls?.at(0)?.arguments?.at(0), '5.6.7');
});

await suite.test('resolves the version from a plain version_file', async (t) => {
const versionFile = writeTempVersionFile('version', '5.6.7\n');
const mocks = defaultMocks(t.mock, {
version: '',
version_file: versionFile,
});
await run();

assert.deepStrictEqual(mocks.installGcloudSDK.mock.calls?.at(0)?.arguments?.at(0), '5.6.7');
});

await suite.test('prefers an explicit version over version_file', async (t) => {
const versionFile = writeTempVersionFile('version', '5.6.7\n');
const mocks = defaultMocks(t.mock, {
version: '9.9.9',
version_file: versionFile,
});
await run();

assert.deepStrictEqual(mocks.installGcloudSDK.mock.calls?.at(0)?.arguments?.at(0), '9.9.9');
});
});

test('#parseVersionFile', { concurrency: true }, async (suite) => {
await suite.test('extracts the gcloud entry from a .tool-versions file', async () => {
assert.deepStrictEqual(parseVersionFile('nodejs 20.0.0\ngcloud 416.0.0\n'), '416.0.0');
});

await suite.test('reads a plain version file', async () => {
assert.deepStrictEqual(parseVersionFile('416.0.0\n'), '416.0.0');
});

await suite.test('ignores comments and blank lines', async () => {
assert.deepStrictEqual(parseVersionFile('# comment\n\ngcloud 416.0.0\n'), '416.0.0');
});

await suite.test('preserves a version constraint', async () => {
assert.deepStrictEqual(parseVersionFile('>= 416.0.0\n'), '>= 416.0.0');
});

await suite.test('returns an empty string for an empty file', async () => {
assert.deepStrictEqual(parseVersionFile('\n# only a comment\n'), '');
});
});

/**
Expand Down