feat: emit a warning when the workspace is empty (#117)
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.
This commit is contained in:
parent
ac489d50bb
commit
983a037dfb
@ -32,6 +32,10 @@ and permissions on Google Cloud.
|
|||||||
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
|
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
|
||||||
for instructions.
|
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
|
## Usage
|
||||||
|
|
||||||
|
50
dist/main/index.js
vendored
50
dist/main/index.js
vendored
@ -2198,6 +2198,22 @@ function run() {
|
|||||||
if (!githubWorkspace) {
|
if (!githubWorkspace) {
|
||||||
throw new Error('$GITHUB_WORKSPACE is not set');
|
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.
|
// Create credentials file.
|
||||||
const credentialsPath = yield client.createCredentialsFile(githubWorkspace);
|
const credentialsPath = yield client.createCredentialsFile(githubWorkspace);
|
||||||
(0, core_1.info)(`Created credentials file at "${credentialsPath}"`);
|
(0, core_1.info)(`Created credentials file at "${credentialsPath}"`);
|
||||||
@ -2292,12 +2308,22 @@ run();
|
|||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 314:
|
/***/ 314:
|
||||||
/***/ ((__unused_webpack_module, exports) => {
|
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||||
|
|
||||||
"use strict";
|
"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 }));
|
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
|
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
|
||||||
* DWD exchange. The JWT must be signed and then exchanged with the OAuth
|
* 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);
|
return JSON.stringify(body);
|
||||||
}
|
}
|
||||||
exports.buildDomainWideDelegationJWT = buildDomainWideDelegationJWT;
|
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;
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
21
src/main.ts
21
src/main.ts
@ -25,7 +25,7 @@ import { WorkloadIdentityClient } from './client/workload_identity_client';
|
|||||||
import { CredentialsJSONClient } from './client/credentials_json_client';
|
import { CredentialsJSONClient } from './client/credentials_json_client';
|
||||||
import { AuthClient } from './client/auth_client';
|
import { AuthClient } from './client/auth_client';
|
||||||
import { BaseClient } from './base';
|
import { BaseClient } from './base';
|
||||||
import { buildDomainWideDelegationJWT } from './utils';
|
import { buildDomainWideDelegationJWT, isEmptyDir } from './utils';
|
||||||
|
|
||||||
const secretsWarning =
|
const secretsWarning =
|
||||||
`If you are specifying input values via GitHub secrets, ensure the secret ` +
|
`If you are specifying input values via GitHub secrets, ensure the secret ` +
|
||||||
@ -132,6 +132,25 @@ async function run(): Promise<void> {
|
|||||||
throw new Error('$GITHUB_WORKSPACE is not set');
|
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.
|
// Create credentials file.
|
||||||
const credentialsPath = await client.createCredentialsFile(githubWorkspace);
|
const credentialsPath = await client.createCredentialsFile(githubWorkspace);
|
||||||
logInfo(`Created credentials file at "${credentialsPath}"`);
|
logInfo(`Created credentials file at "${credentialsPath}"`);
|
||||||
|
19
src/utils.ts
19
src/utils.ts
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
|
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
|
||||||
* DWD exchange. The JWT must be signed and then exchanged with the OAuth
|
* DWD exchange. The JWT must be signed and then exchanged with the OAuth
|
||||||
@ -35,3 +37,20 @@ export function buildDomainWideDelegationJWT(
|
|||||||
|
|
||||||
return JSON.stringify(body);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import { buildDomainWideDelegationJWT } from '../src/utils';
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
|
import { buildDomainWideDelegationJWT, isEmptyDir } from '../src/utils';
|
||||||
|
|
||||||
describe('Utils', () => {
|
describe('Utils', () => {
|
||||||
describe('#buildDomainWideDelegationJWT', () => {
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user