Add support for specifying version constraints (#612)

This adds support for specifying a version _constraint_ for the version
of gcloud. Since the GitHub Actions runners pre-install a version of
gcloud, this can dramatically reduce CI times.
This commit is contained in:
Seth Vargo 2023-02-02 19:21:19 -05:00 committed by GitHub
parent fb149a7b94
commit 10b5bd2b6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 338 additions and 2152 deletions

View file

@ -38,6 +38,18 @@ jobs:
- name: 'Build'
run: 'npm ci && npm run build'
# No installation
- name: 'Install constraint'
uses: './'
with:
skip_install: true
# Constraint installation
- name: 'Install constraint'
uses: './'
with:
version: '>= 1.0.0'
# Default installation
- name: 'Install version'
uses: './'

View file

@ -41,6 +41,8 @@ jobs:
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
with:
version: '>= 363.0.0'
- name: 'Use gcloud CLI'
run: 'gcloud info'
@ -50,13 +52,42 @@ jobs:
### Cloud SDK inputs
- `version`: (Optional) A string representing the version of the Cloud SDK
(`gcloud`) to install (e.g. `"290.0.1"`). The default value is "latest",
which will install the latest available Cloud SDK version.
- `skip_install`: (Optional) Skip the `gcloud` installation and use the
[system-installed gcloud][github-runners] instead. This can dramatically
improve workflow speeds at the expense of a slightly older gcloud version.
Setting this to `true` ignores any value for the `version` input. Even if
installation is skipped, this GitHub Action will still configure any
components or project metadata. The default value is `false`.
- `version`: (Optional) A string representing the version or version
constraint of the Cloud SDK (`gcloud`) to install (e.g. `"290.0.1"` or `">=
197.0.1"`). The default value is `"latest"`, which will always download and
install the latest available Cloud SDK version.
```yaml
- uses: 'google-github-actions/setup-gcloud@v1'
with:
version: '>= 416.0.0'
```
If there is no installed `gcloud` version that matches the given constraint,
this GitHub Action will download and install the latest available version
that still matches the constraint.
**Warning!** Workload Identity Federation requires version
[363.0.0](https://cloud.google.com/sdk/docs/release-notes#36300_2021-11-02)
or newer.
or newer. If you need support for Workload Identity Federation, specify your
version constraint as such:
```yaml
- uses: 'google-github-actions/setup-gcloud@v1'
with:
version: '>= 363.0.0'
```
You are responsible for ensuring the `gcloud` version matches the features
and components required. See the [gcloud release
notes][gcloud-release-notes] for a full list of versions.
- `project_id`: (Optional) Project ID (**not** project _number_) of the Google
Cloud project. If provided, this will configure the `gcloud` CLI to use that
@ -233,3 +264,5 @@ See [LICENSE](LICENSE).
[sa-iam-docs]: https://cloud.google.com/iam/docs/service-accounts
[sa]: https://cloud.google.com/iam/docs/creating-managing-service-accounts
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation
[github-runners]: https://github.com/actions/runner-images
[gcloud-release-notes]: https://cloud.google.com/sdk/docs/release-notes

View file

@ -19,12 +19,21 @@ description: |-
Adds the `gcloud` CLI command to the $PATH.
inputs:
skip_install:
description: |-
Skip installation of the gcloud SDK and use the system-supplied version
instead. The "version" input will be ignored.
default: false
required: false
version:
description: |-
Version of the gcloud SDK to install. If unspecified or set to "latest",
the latest available gcloud SDK version for the target platform will be
installed. Example: "290.0.1".
default: latest
Version or version constraint of the gcloud SDK to install. If
unspecified, it will accept any installed version of the gcloud SDK. If
set to "latest", it will download the latest available SDK. If set to a
version constraint, it will download the latest available version that
matches the constraint. Examples: "290.0.1" or ">= 197.0.1".
default: 'latest'
required: false
project_id:
@ -32,7 +41,6 @@ inputs:
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.
default: null
required: false
install_components:

2
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

2182
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -28,24 +28,24 @@
"@actions/core": "^1.10.0",
"@actions/tool-cache": "^2.0.1",
"@google-github-actions/actions-utils": "^0.4.6",
"@google-github-actions/setup-cloud-sdk": "^1.0.1"
"@google-github-actions/setup-cloud-sdk": "^1.1.1"
},
"devDependencies": {
"@types/chai": "^4.3.4",
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.15",
"@types/node": "^18.11.18",
"@types/sinon": "^10.0.13",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"@vercel/ncc": "^0.36.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vercel/ncc": "^0.36.1",
"chai": "^4.3.7",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"mocha": "^10.2.0",
"prettier": "^2.8.1",
"prettier": "^2.8.3",
"sinon": "^15.0.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
"typescript": "^4.9.5"
}
}

View file

@ -18,16 +18,17 @@ import * as core from '@actions/core';
import * as toolCache from '@actions/tool-cache';
import {
authenticateGcloudSDK,
getLatestGcloudSDKVersion,
bestVersion,
installComponent,
installGcloudSDK,
isInstalled,
setProject,
} from '@google-github-actions/setup-cloud-sdk';
import {
errorMessage,
isPinnedToHead,
pinnedToHeadWarning,
parseBoolean,
presence,
} from '@google-github-actions/actions-utils';
import path from 'path';
@ -49,17 +50,33 @@ export async function run(): Promise<void> {
}
try {
let version = core.getInput('version');
if (!version || version == 'latest') {
version = await getLatestGcloudSDKVersion();
}
// Install the gcloud if not already present
if (!isInstalled(version)) {
await installGcloudSDK(version);
const skipInstall = parseBoolean(core.getInput('skip_install'));
if (skipInstall) {
core.info(`Skipping installation ("skip_install" was true)`);
} else {
// 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
// constraint as-is.
let version = presence(core.getInput('version'));
if (!version) {
core.debug(`version was unset, defaulting to any version`);
version = '> 0.0.0';
}
if (version === 'latest') {
core.debug(`resolving latest version`);
version = await bestVersion('> 0.0.0');
core.debug(`resolved latest version to ${version}`);
}
// Install the gcloud if not already present
const toolPath = toolCache.find('gcloud', version);
core.addPath(path.join(toolPath, 'bin'));
if (toolPath !== '') {
core.addPath(path.join(toolPath, 'bin'));
} else {
core.debug(`no version of gcloud matching "${version}" is installed`);
await installGcloudSDK(version);
}
}
// Install additional components

View file

@ -23,8 +23,10 @@ import * as sinon from 'sinon';
import { promises as fs } from 'fs';
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 { clearEnv } from '@google-github-actions/actions-utils';
import { run } from '../src/main';
@ -44,84 +46,138 @@ describe('#run', function () {
getInput: sinon.stub(core, 'getInput').callsFake(getInputMock),
getBooleanInput: sinon.stub(core, 'getBooleanInput').returns(false),
exportVariable: sinon.stub(core, 'exportVariable'),
setFailed: sinon.stub(core, 'setFailed'),
warning: sinon.stub(core, 'warning'),
authenticateGcloudSDK: sinon.stub(setupGcloud, 'authenticateGcloudSDK'),
installGcloudSDK: sinon.stub(setupGcloud, 'installGcloudSDK'),
isInstalled: sinon.stub(setupGcloud, 'isInstalled').returns(false),
setProject: sinon.stub(setupGcloud, 'setProject'),
installComponent: sinon.stub(setupGcloud, 'installComponent'),
toolCacheFind: sinon.stub(toolCache, 'find').returns('/'),
writeFile: sinon.stub(fs, 'writeFile'),
env: sinon.stub(process, 'env').value({ GITHUB_PATH: '/' }),
};
process.env.GITHUB_PATH = '/';
sinon.stub(core, 'setFailed').throwsArg(0); // make setFailed throw exceptions
sinon.stub(core, 'addPath').callsFake(sinon.fake());
sinon.stub(core, 'debug').callsFake(sinon.fake());
sinon.stub(core, 'endGroup').callsFake(sinon.fake());
sinon.stub(core, 'info').callsFake(sinon.fake());
sinon.stub(core, 'startGroup').callsFake(sinon.fake());
sinon.stub(core, 'warning').callsFake(sinon.fake());
await TestToolCache.start();
});
afterEach(function () {
afterEach(async function () {
Object.keys(this.stubs).forEach((k) => this.stubs[k].restore());
sinon.restore();
clearEnv((key: string) => key.startsWith(`GITHUB_`));
await TestToolCache.stop();
});
it('downloads the latest gcloud SDK if version is not provided', async function () {
this.stubs.getInput.withArgs('version').returns('');
await run();
// getLatestGcloudSDKVersion is implemented as a getter on on exports and so stubbing doesn't work.
// Instead, make sure that installGcloudSDK was called with an expected version string.
expect(this.stubs.installGcloudSDK.withArgs(sinon.match(/\d+\.\d+\.\d+/)).callCount).to.eq(1);
describe('download', () => {
it('downloads when no version is provided', async function () {
this.stubs.getInput.withArgs('version').returns('');
await run();
const call = this.stubs.installGcloudSDK.firstCall;
expect(call.firstArg).to.match(/\d+\.\d+\.\d+/);
});
it('downloads when version is "latest"', async function () {
this.stubs.getInput.withArgs('version').returns('latest');
await run();
const call = this.stubs.installGcloudSDK.firstCall;
expect(call.firstArg).to.match(/\d+\.\d+\.\d+/);
});
it('downloads when version is not installed', async function () {
this.stubs.getInput.withArgs('version').returns('5.6.7');
await run();
const call = this.stubs.installGcloudSDK.firstCall;
expect(call.firstArg).to.eql('5.6.7');
});
it('downloads when version constraint is not satisfied', async function () {
this.stubs.getInput.withArgs('version').returns('>= 10.0.0');
await run();
const call = this.stubs.installGcloudSDK.firstCall;
expect(call.firstArg).to.eql('>= 10.0.0');
});
it('downloads when a version is installed but the version constraint is not satisfied', async function () {
this.stubs.getInput.withArgs('version').returns('>= 10.0.0');
await installFakeGcloud('9.8.7');
await run();
const call = this.stubs.installGcloudSDK.firstCall;
expect(call.firstArg).to.eql('>= 10.0.0');
});
it('does not download when version is installed', async function () {
this.stubs.getInput.withArgs('version').returns('1.2.3');
await installFakeGcloud('1.2.3');
await run();
expect(this.stubs.installGcloudSDK.callCount).to.eq(0);
});
it('does not download when a version constraint is satisfied', async function () {
this.stubs.getInput.withArgs('version').returns('>= 0.0.0');
await installFakeGcloud('1.2.3');
await run();
expect(this.stubs.installGcloudSDK.callCount).to.eq(0);
});
});
it('downloads the latest gcloud SDK if version is "latest"', async function () {
this.stubs.getInput.withArgs('version').returns('latest');
await run();
expect(this.stubs.installGcloudSDK.withArgs(sinon.match(/\d+\.\d+\.\d+/)).callCount).to.eq(1);
describe('component installation', () => {
it('installs 1 additional component', async function () {
this.stubs.getInput.withArgs('install_components').returns('beta');
await run();
const call = this.stubs.installComponent.firstCall;
expect(call.firstArg).to.eql(['beta']);
});
it('installs additional components', async function () {
this.stubs.getInput.withArgs('install_components').returns('beta, alpha');
await run();
const call = this.stubs.installComponent.firstCall;
expect(call.firstArg).to.eql(['beta', 'alpha']);
});
});
it('does not download the SDK if version is provided', async function () {
this.stubs.getInput.withArgs('version').returns('999');
await run();
expect(this.stubs.installGcloudSDK.withArgs('999').callCount).to.eq(1);
describe('authentication', () => {
it('authenticates if GOOGLE_GHA_CREDS is set', async function () {
process.env.GOOGLE_GHA_CREDS_PATH = '/foo/bar/path.json';
await run();
expect(this.stubs.authenticateGcloudSDK.callCount).to.eq(1);
});
});
it('installs the gcloud SDK if it is not already installed', async function () {
this.stubs.isInstalled.returns(false);
this.stubs.getInput.withArgs('version').returns('888');
await run();
expect(this.stubs.installGcloudSDK.withArgs('888').callCount).to.eq(1);
});
describe('configuration', () => {
it('sets the project ID if provided', async function () {
this.stubs.getInput.withArgs('project_id').returns('test');
await run();
expect(this.stubs.setProject.withArgs('test').callCount).to.eq(1);
});
it('uses the cached gcloud SDK if it was already installed', async function () {
this.stubs.isInstalled.returns(true);
this.stubs.getInput.withArgs('version').returns('777');
await run();
expect(this.stubs.toolCacheFind.withArgs('gcloud', '777').callCount).to.eq(1);
});
it('installs 1 additional component', async function () {
this.stubs.getInput.withArgs('install_components').returns('beta');
await run();
expect(this.stubs.installComponent.callCount).to.eq(1);
});
it('installs additional components', async function () {
this.stubs.getInput.withArgs('install_components').returns('beta, alpha');
await run();
expect(this.stubs.installComponent.callCount).to.eq(1);
});
it('authenticates if GOOGLE_GHA_CREDS is set', async function () {
this.stubs.env.value({ GOOGLE_GHA_CREDS_PATH: 'foo/bar/path.json' }).returns('{}');
await run();
expect(this.stubs.authenticateGcloudSDK.callCount).to.eq(1);
});
it('sets the project ID if provided', async function () {
this.stubs.getInput.withArgs('project_id').returns('test');
await run();
expect(this.stubs.setProject.withArgs('test').callCount).to.eq(1);
});
it('does not set the project ID if not provided', async function () {
this.stubs.getInput.withArgs('project_id').returns('');
await run();
expect(this.stubs.setProject.callCount).to.eq(0);
it('does not set the project ID if not provided', async function () {
this.stubs.getInput.withArgs('project_id').returns('');
await run();
expect(this.stubs.setProject.callCount).to.eq(0);
});
});
});
/**
* installFakeGcloud puts a fake gcloud version into the temporary toolcache.
* It's not actually gcloud and not actually executable.
*/
async function installFakeGcloud(version: string): Promise<string> {
return await toolCache.cacheFile('action.yml', 'action.yml', 'gcloud', version);
}