diff --git a/README.md b/README.md index 47462f86..6980f67a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ and permissions on Google Cloud. configure a Google Cloud Workload Identity Provider. See [setup](#setup) for instructions. +- You must run the `actions/checkout@v2` step _before_ this action. Omitting + the checkout step or putting it after `auth` will cause future steps to be + unable to authenticate. + ## Usage diff --git a/dist/main/index.js b/dist/main/index.js index 43927f69..2b7a1533 100644 --- a/dist/main/index.js +++ b/dist/main/index.js @@ -2198,6 +2198,22 @@ function run() { if (!githubWorkspace) { throw new Error('$GITHUB_WORKSPACE is not set'); } + // There have been a number of issues where users have not used the + // "actions/checkout" step before our action. Our action relies on the + // creation of that directory; worse, if a user puts "actions/checkout" + // after our action, it will delete the exported credential. This + // following code does a small check to see if there are any files in the + // directory. It emits a warning if there are no files, since there may be + // legitimate use cases for authenticating without checking out the + // repository. + const githubWorkspaceIsEmpty = yield (0, utils_1.isEmptyDir)(githubWorkspace); + if (githubWorkspaceIsEmpty) { + (0, core_1.warning)(`The "create_credentials_file" option is true, but the current ` + + `GitHub workspace is empty. Did you forget to use ` + + `"actions/checkout" before this step? If you do not intend to ` + + `share authentication with future steps in this job, set ` + + `"create_credentials_file" to false.`); + } // Create credentials file. const credentialsPath = yield client.createCredentialsFile(githubWorkspace); (0, core_1.info)(`Created credentials file at "${credentialsPath}"`); @@ -2292,12 +2308,22 @@ run(); /***/ }), /***/ 314: -/***/ ((__unused_webpack_module, exports) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.buildDomainWideDelegationJWT = void 0; +exports.isEmptyDir = exports.buildDomainWideDelegationJWT = void 0; +const fs_1 = __nccwpck_require__(147); /** * buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a * DWD exchange. The JWT must be signed and then exchanged with the OAuth @@ -2327,6 +2353,26 @@ function buildDomainWideDelegationJWT(serviceAccount, subject, scopes, lifetime) return JSON.stringify(body); } exports.buildDomainWideDelegationJWT = buildDomainWideDelegationJWT; +/** + * isEmptyDir returns true if the given directory does not exist, or exists but + * contains no files. It also returns true if the current user does not have + * permission to read the directory, since it is effectively empty from the + * viewpoint of the caller. + * + * @param dir Path to a directory. + */ +function isEmptyDir(dir) { + return __awaiter(this, void 0, void 0, function* () { + try { + const files = yield fs_1.promises.readdir(dir); + return files.length <= 0; + } + catch (e) { + return true; + } + }); +} +exports.isEmptyDir = isEmptyDir; /***/ }), diff --git a/src/main.ts b/src/main.ts index cba2680e..37f1c9ad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,7 @@ import { WorkloadIdentityClient } from './client/workload_identity_client'; import { CredentialsJSONClient } from './client/credentials_json_client'; import { AuthClient } from './client/auth_client'; import { BaseClient } from './base'; -import { buildDomainWideDelegationJWT } from './utils'; +import { buildDomainWideDelegationJWT, isEmptyDir } from './utils'; const secretsWarning = `If you are specifying input values via GitHub secrets, ensure the secret ` + @@ -132,6 +132,25 @@ async function run(): Promise { throw new Error('$GITHUB_WORKSPACE is not set'); } + // There have been a number of issues where users have not used the + // "actions/checkout" step before our action. Our action relies on the + // creation of that directory; worse, if a user puts "actions/checkout" + // after our action, it will delete the exported credential. This + // following code does a small check to see if there are any files in the + // directory. It emits a warning if there are no files, since there may be + // legitimate use cases for authenticating without checking out the + // repository. + const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace); + if (githubWorkspaceIsEmpty) { + logWarning( + `The "create_credentials_file" option is true, but the current ` + + `GitHub workspace is empty. Did you forget to use ` + + `"actions/checkout" before this step? If you do not intend to ` + + `share authentication with future steps in this job, set ` + + `"create_credentials_file" to false.`, + ); + } + // Create credentials file. const credentialsPath = await client.createCredentialsFile(githubWorkspace); logInfo(`Created credentials file at "${credentialsPath}"`); diff --git a/src/utils.ts b/src/utils.ts index 49163c1c..1375a177 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ 'use strict'; +import { promises as fs } from 'fs'; + /** * buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a * DWD exchange. The JWT must be signed and then exchanged with the OAuth @@ -35,3 +37,20 @@ export function buildDomainWideDelegationJWT( return JSON.stringify(body); } + +/** + * isEmptyDir returns true if the given directory does not exist, or exists but + * contains no files. It also returns true if the current user does not have + * permission to read the directory, since it is effectively empty from the + * viewpoint of the caller. + * + * @param dir Path to a directory. + */ +export async function isEmptyDir(dir: string): Promise { + try { + const files = await fs.readdir(dir); + return files.length <= 0; + } catch (e) { + return true; + } +} diff --git a/tests/utils.test.ts b/tests/utils.test.ts index fff25157..5ab32a53 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -3,7 +3,9 @@ import 'mocha'; import { expect } from 'chai'; -import { buildDomainWideDelegationJWT } from '../src/utils'; +import { tmpdir } from 'os'; + +import { buildDomainWideDelegationJWT, isEmptyDir } from '../src/utils'; describe('Utils', () => { describe('#buildDomainWideDelegationJWT', () => { @@ -54,4 +56,26 @@ describe('Utils', () => { }); }); }); + + describe('#isEmptyDir', async () => { + const cases = [ + { + name: 'non-existent dir', + dir: '/this/path/definitely/does/not/exist', + exp: true, + }, + { + name: 'exists', + dir: tmpdir(), + exp: false, + }, + ]; + + cases.forEach((tc) => { + it(tc.name, async () => { + const isEmpty = await isEmptyDir(tc.dir); + expect(isEmpty).to.eq(tc.exp); + }); + }); + }); });