Skip to content

Commit

Permalink
feat: emit a warning when the workspace is empty (#117)
Browse files Browse the repository at this point in the history
There have been a number of GitHub issues recently due to users not adding actions/checkout before calling "auth", which makes the credentials unavailable to future steps. Worse, some people are putting checkout _after_ auth, which overwrites the generated credentials with a checkout of the repo.

This adds a feature that emits a warning with the workspace is empty.
  • Loading branch information
sethvargo committed Jan 26, 2022
1 parent ac489d5 commit 983a037
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -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

Expand Down
50 changes: 48 additions & 2 deletions dist/main/index.js
Expand Up @@ -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}"`);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;


/***/ }),
Expand Down
21 changes: 20 additions & 1 deletion src/main.ts
Expand Up @@ -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 ` +
Expand Down Expand Up @@ -132,6 +132,25 @@ async function run(): Promise<void> {
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}"`);
Expand Down
19 changes: 19 additions & 0 deletions 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
Expand Down Expand Up @@ -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<boolean> {
try {
const files = await fs.readdir(dir);
return files.length <= 0;
} catch (e) {
return true;
}
}
26 changes: 25 additions & 1 deletion tests/utils.test.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
});

0 comments on commit 983a037

Please sign in to comment.