chore: migrate to biomejs (#1167)

* chore: migrate to biomejs

* chore: migrate to biomejs

* chore: migrate from jest to vitest

* chore: error on lint warnings

* chore: remove obsolete depedencies
This commit is contained in:
Tom Keller
2024-11-04 10:19:12 -08:00
committed by GitHub
parent d90c1f89e1
commit ccbdafa425
17 changed files with 4393 additions and 8410 deletions
-2
View File
@@ -1,2 +0,0 @@
build
dist
-168
View File
@@ -1,168 +0,0 @@
env:
jest: true
node: true
root: true
plugins:
- import
- prettier
parserOptions:
ecmaVersion: 2021
sourceType: module
extends:
- plugin:prettier/recommended
- prettier
rules:
prettier/prettier: [error]
import/no-extraneous-dependencies:
- error
- devDependencies:
- "**/test/**"
- "**/build-tools/**"
optionalDependencies: false
peerDependencies: true
import/no-unresolved: [error]
import/order:
- warn
- groups:
- builtin
- external
alphabetize:
order: asc
caseInsensitive: true
array-callback-return: [warn]
no-await-in-loop: [warn]
no-constant-binary-expression: [error]
no-constructor-return: [error]
no-duplicate-imports: [error]
no-self-compare: [warn]
no-template-curly-in-string: [error]
no-unmodified-loop-condition: [error]
no-unreachable-loop: [error]
no-unused-private-class-members: [error]
no-use-before-define: [error]
require-atomic-updates: [error]
block-scoped-var: [warn]
camelcase: [warn]
class-methods-use-this: [error]
consistent-return: [warn]
consistent-this: [warn]
default-case-last: [warn]
default-param-last: [warn]
dot-notation: [error]
eqeqeq: [error]
guard-for-in: [warn]
logical-assignment-operators:
- error
- always
- enforceForIfStatements: false
no-array-constructor: [error]
no-bitwise: [error]
no-console: [warn]
no-empty-function: [warn]
no-eval: [error]
no-extra-bind: [error]
no-labels: [error]
no-implicit-globals: [error]
no-invalid-this: [error]
key-spacing: [error]
no-multiple-empty-lines: [error]
no-return-await: [warn]
no-trailing-spaces: [error]
no-lonely-if: [error]
no-nested-ternary: [warn]
no-mixed-operators: [warn]
no-proto: [error]
no-sequences: [error]
no-throw-literal: [error]
no-useless-call: [error]
no-useless-concat: [warn]
no-var: [error]
one-var-declaration-per-line: [error]
prefer-const: [warn]
prefer-arrow-callback: [warn]
prefer-regex-literals: [warn]
prefer-promise-reject-errors: [warn]
prefer-spread: [warn]
prefer-template: [warn]
require-await: [error]
overrides:
- files:
- '**/*.ts'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2021
sourceType: module
project: ./tsconfig.json
extends:
- plugin:@typescript-eslint/recommended
- plugin:@typescript-eslint/recommended-requiring-type-checking
- plugin:import/typescript
rules:
'@typescript-eslint/array-type':
- warn
- default: array-simple
'@typescript-eslint/ban-tslint-comment': [error]
'@typescript-eslint/consistent-indexed-object-style': [warn]
'@typescript-eslint/consistent-type-assertions': [warn]
'@typescript-eslint/prefer-includes': [warn]
dot-notation: [off]
'@typescript-eslint/dot-notation': [error]
'@typescript-eslint/no-explicit-any': [off]
'@typescript-eslint/consistent-type-exports': [warn]
'@typescript-eslint/consistent-type-imports': [warn]
'@typescript-eslint/no-base-to-string': [error]
'@typescript-eslint/no-confusing-non-null-assertion': [warn]
'@typescript-eslint/no-invalid-void-type': [error]
'@typescript-eslint/no-meaningless-void-operator': [warn]
'@typescript-eslint/no-redundant-type-constituents': [warn]
'@typescript-eslint/no-unnecessary-boolean-literal-compare': [warn]
'@typescript-eslint/no-unnecessary-condition': [warn]
'@typescript-eslint/no-unnecessary-qualifier': [warn]
'@typescript-eslint/no-unnecessary-type-arguments': [warn]
'@typescript-eslint/non-nullable-type-assertion-style': [warn]
'@typescript-eslint/prefer-for-of': [error]
'@typescript-eslint/prefer-literal-enum-member': [warn]
'@typescript-eslint/prefer-optional-chain': [warn]
'@typescript-eslint/prefer-readonly': [warn]
'@typescript-eslint/prefer-regexp-exec': [warn]
'@typescript-eslint/prefer-string-starts-ends-with': [warn]
'@typescript-eslint/prefer-ts-expect-error': [error]
'@typescript-eslint/promise-function-async': [warn]
'@typescript-eslint/require-array-sort-compare': [error]
default-param-last: [off]
'@typescript-eslint/default-param-last': [warn]
no-array-constructor: [off]
'@typescript-eslint/no-array-constructor': [error]
no-dupe-class-members: [off]
'@typescript-eslint/no-dupe-class-members': [warn]
no-invalid-this: [off]
'@typescript-eslint/no-invalid-this': [warn]
no-unused-vars: [off]
'@typescript-eslint/no-unused-vars':
- error
- varsIgnorePattern: '^_'
argsIgnorePattern: '^_'
caughtErrorsIgnorePattern: '^_'
'@typescript-eslint/no-non-null-assertion': [off]
'@typescript-eslint/no-require-imports':
- error
no-return-await: [off]
'@typescript-eslint/return-await': [error]
no-shadow: [off]
'@typescript-eslint/no-shadow': [error]
'@typescript-eslint/no-floating-promises': [error]
"@typescript-eslint/member-ordering":
- error
- default:
- public-static-field
- public-static-method
- protected-static-field
- protected-static-method
- private-static-field
- private-static-method
- field
- constructor
- method
no-use-before-define: [off]
'@typescript-eslint/no-use-before-define': [error]
no-duplicate-imports: [off]
-8
View File
@@ -1,8 +0,0 @@
{
"printWidth": 120,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"overrides": []
}
+2
View File
@@ -0,0 +1,2 @@
const { fs } = require('memfs')
module.exports = fs
+34
View File
@@ -0,0 +1,34 @@
{
"formatter": {
"indentStyle": "space",
"lineWidth": 120,
"indentWidth": 2,
"lineEnding": "lf",
"enabled": true,
},
"linter": {
"enabled": true,
"rules": {
"performance": {
"noDelete": "off",
},
"complexity": {
"noExtraBooleanCast": "off",
}
},
},
"javascript": {
"formatter": {
"trailingCommas": "all",
"jsxQuoteStyle": "double",
"quoteStyle": "single",
"bracketSpacing": true,
"arrowParentheses": "always",
},
},
"json": {
"formatter": {
"trailingCommas": "all",
},
},
}
+3848 -7296
View File
File diff suppressed because it is too large Load Diff
+15 -25
View File
@@ -3,9 +3,9 @@
"description": "A GitHub Action to configure AWS credentials", "description": "A GitHub Action to configure AWS credentials",
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
"lint": "eslint .", "lint": "biome check --error-on-warnings ./src",
"package": "npm run build && ncc build --license THIRD-PARTY -o dist && ncc build src/cleanup/index.ts -o dist/cleanup && copyup -E dist/THIRD-PARTY . && del-cli dist/THIRD-PARTY", "package": "npm run build && ncc build --license THIRD-PARTY -o dist && ncc build src/cleanup/index.ts -o dist/cleanup && cpy -E dist/THIRD-PARTY . && del-cli dist/THIRD-PARTY",
"test": "npm run lint && jest --verbose" "test": "npm run lint && vitest run"
}, },
"author": { "author": {
"name": "Amazon.com, Inc. or its affiliates", "name": "Amazon.com, Inc. or its affiliates",
@@ -14,35 +14,25 @@
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/credential-provider-env": "^3.515.0", "@aws-sdk/credential-provider-env": "^3.515.0",
"@smithy/property-provider": "^3.1.8", "@biomejs/biome": "1.9.4",
"@jest/globals": "^29.7.0", "@smithy/property-provider": "^3.1.7",
"@types/jest": "^29.5.14", "@types/node": "^22.7.7",
"@types/node": "^22",
"@typescript-eslint/eslint-plugin": "<=5.62.0",
"@typescript-eslint/parser": "<=5.62.0",
"@vercel/ncc": "^0.38.2", "@vercel/ncc": "^0.38.2",
"@vitest/coverage-v8": "^2.1.3",
"aws-sdk-client-mock": "^4.1.0", "aws-sdk-client-mock": "^4.1.0",
"copyfiles": "^2.4.1", "cpy-cli": "^5.0.0",
"del-cli": "^6.0.0", "del-cli": "^6.0.0",
"eslint": "^8",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"jest-junit": "^16",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"prettier": "^3.3.3", "memfs": "^4.14.0",
"standard-version": "^9", "standard-version": "^9.5.0",
"ts-jest": "^29.2.5", "typescript": "^5.6.3",
"typescript": "^5.6.3" "vitest": "^2.1.3"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@aws-sdk/client-sts": "^3", "@aws-sdk/client-sts": "^3.675.0",
"@smithy/node-http-handler": "^3.2.5", "@smithy/node-http-handler": "^3.2.4",
"https-proxy-agent": "^5.0.0" "https-proxy-agent": "^5.0.1"
}, },
"keywords": [ "keywords": [
"aws", "aws",
+3 -2
View File
@@ -1,5 +1,6 @@
import { info } from '@actions/core'; import { info } from '@actions/core';
import { STSClient } from '@aws-sdk/client-sts'; import { STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler'; import { NodeHttpHandler } from '@smithy/node-http-handler';
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { errorMessage } from './helpers'; import { errorMessage } from './helpers';
@@ -40,7 +41,7 @@ export class CredentialsClient {
} }
public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) { public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) {
let credentials; let credentials: AwsCredentialIdentity;
try { try {
credentials = await this.loadCredentials(); credentials = await this.loadCredentials();
if (!credentials.accessKeyId) { if (!credentials.accessKeyId) {
@@ -55,7 +56,7 @@ export class CredentialsClient {
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) { if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
throw new Error( throw new Error(
'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action' 'Unexpected failure: Credentials loaded by the SDK do not match the access key ID configured by the action',
); );
} }
} }
+19 -15
View File
@@ -1,6 +1,6 @@
import assert from 'assert'; import assert from 'node:assert';
import fs from 'fs'; import fs from 'node:fs';
import path from 'path'; import path from 'node:path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts'; import type { AssumeRoleCommandInput, STSClient, Tag } from '@aws-sdk/client-sts';
import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts'; import { AssumeRoleCommand, AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
@@ -15,7 +15,7 @@ async function assumeRoleWithOIDC(params: AssumeRoleCommandInput, client: STSCli
new AssumeRoleWithWebIdentityCommand({ new AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
}) }),
); );
return creds; return creds;
} catch (error) { } catch (error) {
@@ -27,10 +27,10 @@ async function assumeRoleWithWebIdentityTokenFile(
params: AssumeRoleCommandInput, params: AssumeRoleCommandInput,
client: STSClient, client: STSClient,
webIdentityTokenFile: string, webIdentityTokenFile: string,
workspace: string workspace: string,
) { ) {
core.debug( core.debug(
'webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.' 'webIdentityTokenFile provided. Will call sts:AssumeRoleWithWebIdentity and take session tags from token contents.',
); );
const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile) const webIdentityTokenFilePath = path.isAbsolute(webIdentityTokenFile)
? webIdentityTokenFile ? webIdentityTokenFile
@@ -46,7 +46,7 @@ async function assumeRoleWithWebIdentityTokenFile(
new AssumeRoleWithWebIdentityCommand({ new AssumeRoleWithWebIdentityCommand({
...params, ...params,
WebIdentityToken: webIdentityToken, WebIdentityToken: webIdentityToken,
}) }),
); );
return creds; return creds;
} catch (error) { } catch (error) {
@@ -75,7 +75,7 @@ export interface assumeRoleParams {
webIdentityTokenFile?: string; webIdentityTokenFile?: string;
webIdentityToken?: string; webIdentityToken?: string;
inlineSessionPolicy?: string; inlineSessionPolicy?: string;
managedSessionPolicies?: any[]; managedSessionPolicies?: { arn: string }[];
} }
export async function assumeRole(params: assumeRoleParams) { export async function assumeRole(params: assumeRoleParams) {
@@ -108,8 +108,11 @@ export async function assumeRole(params: assumeRoleParams) {
{ Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) }, { Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) },
{ Key: 'Commit', Value: GITHUB_SHA }, { Key: 'Commit', Value: GITHUB_SHA },
]; ];
if (process.env['GITHUB_REF']) { if (process.env.GITHUB_REF) {
tagArray.push({ Key: 'Branch', Value: sanitizeGitHubVariables(process.env['GITHUB_REF']) }); tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
} }
const tags = roleSkipSessionTagging ? undefined : tagArray; const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) { if (!tags) {
@@ -123,7 +126,7 @@ export async function assumeRole(params: assumeRoleParams) {
if (!roleArn.startsWith('arn:aws')) { if (!roleArn.startsWith('arn:aws')) {
assert( assert(
isDefined(sourceAccountId), isDefined(sourceAccountId),
'Source Account ID is needed if the Role Name is provided and not the Role Arn.' 'Source Account ID is needed if the Role Name is provided and not the Role Arn.',
); );
roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`; roleArn = `arn:aws:iam::${sourceAccountId}:role/${roleArn}`;
} }
@@ -139,6 +142,7 @@ export async function assumeRole(params: assumeRoleParams) {
PolicyArns: managedSessionPolicies?.length ? managedSessionPolicies : undefined, PolicyArns: managedSessionPolicies?.length ? managedSessionPolicies : undefined,
}; };
const keys = Object.keys(commonAssumeRoleParams) as Array<keyof typeof commonAssumeRoleParams>; const keys = Object.keys(commonAssumeRoleParams) as Array<keyof typeof commonAssumeRoleParams>;
// biome-ignore lint/complexity/noForEach: Legacy code
keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]); keys.forEach((k) => commonAssumeRoleParams[k] === undefined && delete commonAssumeRoleParams[k]);
// Instantiate STS client // Instantiate STS client
@@ -147,14 +151,14 @@ export async function assumeRole(params: assumeRoleParams) {
// Assume role using one of three methods // Assume role using one of three methods
if (!!webIdentityToken) { if (!!webIdentityToken) {
return assumeRoleWithOIDC(commonAssumeRoleParams, stsClient, webIdentityToken); return assumeRoleWithOIDC(commonAssumeRoleParams, stsClient, webIdentityToken);
} else if (!!webIdentityTokenFile) { }
if (!!webIdentityTokenFile) {
return assumeRoleWithWebIdentityTokenFile( return assumeRoleWithWebIdentityTokenFile(
commonAssumeRoleParams, commonAssumeRoleParams,
stsClient, stsClient,
webIdentityTokenFile, webIdentityTokenFile,
GITHUB_WORKSPACE GITHUB_WORKSPACE,
); );
} else {
return assumeRoleWithCredentials(commonAssumeRoleParams, stsClient);
} }
return assumeRoleWithCredentials(commonAssumeRoleParams, stsClient);
} }
+4 -3
View File
@@ -23,7 +23,7 @@ export function exportCredentials(creds?: Partial<Credentials>, outputCredential
if (creds?.SessionToken) { if (creds?.SessionToken) {
core.setSecret(creds.SessionToken); core.setSecret(creds.SessionToken);
core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken); core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken);
} else if (process.env['AWS_SESSION_TOKEN']) { } else if (process.env.AWS_SESSION_TOKEN) {
// clear session token from previous credentials action // clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', ''); core.exportVariable('AWS_SESSION_TOKEN', '');
} }
@@ -116,7 +116,7 @@ export async function retryAndBackoff<T>(
isRetryable: boolean, isRetryable: boolean,
maxRetries = 12, maxRetries = 12,
retries = 0, retries = 0,
base = 50 base = 50,
): Promise<T> { ): Promise<T> {
try { try {
return await fn(); return await fn();
@@ -125,7 +125,8 @@ export async function retryAndBackoff<T>(
throw err; throw err;
} }
// It's retryable, so sleep and retry. // It's retryable, so sleep and retry.
await sleep(Math.random() * (Math.pow(2, retries) * base)); await sleep(Math.random() * (2 ** retries * base));
// biome-ignore lint/style/noParameterAssign: This is a loop variable
retries += 1; retries += 1;
if (retries >= maxRetries) { if (retries >= maxRetries) {
throw err; throw err;
+31 -22
View File
@@ -1,13 +1,13 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts'; import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts';
import { assumeRole } from './assumeRole';
import { CredentialsClient } from './CredentialsClient'; import { CredentialsClient } from './CredentialsClient';
import { assumeRole } from './assumeRole';
import { import {
errorMessage, errorMessage,
retryAndBackoff,
exportRegion,
exportCredentials,
exportAccountId, exportAccountId,
exportCredentials,
exportRegion,
retryAndBackoff,
unsetCredentials, unsetCredentials,
verifyKeys, verifyKeys,
} from './helpers'; } from './helpers';
@@ -20,24 +20,35 @@ export async function run() {
try { try {
// Get inputs // Get inputs
const AccessKeyId = core.getInput('aws-access-key-id', { required: false }); const AccessKeyId = core.getInput('aws-access-key-id', { required: false });
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false }); const SecretAccessKey = core.getInput('aws-secret-access-key', {
const sessionTokenInput = core.getInput('aws-session-token', { required: false }); required: false,
});
const sessionTokenInput = core.getInput('aws-session-token', {
required: false,
});
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput; const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: true }); const region = core.getInput('aws-region', { required: true });
const roleToAssume = core.getInput('role-to-assume', { required: false }); const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false }); const audience = core.getInput('audience', { required: false });
const maskAccountIdInput = core.getInput('mask-aws-account-id', { required: false }) || 'false'; const maskAccountIdInput = core.getInput('mask-aws-account-id', { required: false }) || 'false';
const maskAccountId = maskAccountIdInput.toLowerCase() === 'true'; const maskAccountId = maskAccountIdInput.toLowerCase() === 'true';
const roleExternalId = core.getInput('role-external-id', { required: false }); const roleExternalId = core.getInput('role-external-id', {
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false }); required: false,
const roleDuration = parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION; });
const webIdentityTokenFile = core.getInput('web-identity-token-file', {
required: false,
});
const roleDuration =
Number.parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION;
const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME; const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false'; const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true'; const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
const proxyServer = core.getInput('http-proxy', { required: false }); const proxyServer = core.getInput('http-proxy', { required: false });
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false }); const inlineSessionPolicy = core.getInput('inline-session-policy', {
required: false,
});
const managedSessionPoliciesInput = core.getMultilineInput('managed-session-policies', { required: false }); const managedSessionPoliciesInput = core.getMultilineInput('managed-session-policies', { required: false });
const managedSessionPolicies: any[] = []; const managedSessionPolicies: { arn: string }[] = [];
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false'; const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true'; const roleChaining = roleChainingInput.toLowerCase() === 'true';
const outputCredentialsInput = core.getInput('output-credentials', { required: false }) || 'false'; const outputCredentialsInput = core.getInput('output-credentials', { required: false }) || 'false';
@@ -49,7 +60,7 @@ export async function run() {
const specialCharacterWorkaroundInput = const specialCharacterWorkaroundInput =
core.getInput('special-characters-workaround', { required: false }) || 'false'; core.getInput('special-characters-workaround', { required: false }) || 'false';
const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true'; const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true';
let maxRetries = parseInt(core.getInput('retry-max-attempts', { required: false })) || 12; let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
switch (true) { switch (true) {
case specialCharacterWorkaround: case specialCharacterWorkaround:
// 😳 // 😳
@@ -74,17 +85,17 @@ export async function run() {
!!roleToAssume && !!roleToAssume &&
!webIdentityTokenFile && !webIdentityTokenFile &&
!AccessKeyId && !AccessKeyId &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!roleChaining !roleChaining
) { ) {
core.info( core.info(
'It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission? ' + 'It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission? ' +
'If you are not trying to authenticate with OIDC and the action is working successfully, you can ignore this message.' 'If you are not trying to authenticate with OIDC and the action is working successfully, you can ignore this message.',
); );
} }
return ( return (
!!roleToAssume && !!roleToAssume &&
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] && !!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!AccessKeyId && !AccessKeyId &&
!webIdentityTokenFile && !webIdentityTokenFile &&
!roleChaining !roleChaining
@@ -114,7 +125,7 @@ export async function run() {
return core.getIDToken(audience); return core.getIDToken(audience);
}, },
!disableRetry, !disableRetry,
maxRetries maxRetries,
); );
} catch (error) { } catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`); throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
@@ -146,7 +157,6 @@ export async function run() {
if (roleToAssume) { if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput; let roleCredentials: AssumeRoleCommandOutput;
do { do {
// eslint-disable-next-line no-await-in-loop
roleCredentials = await retryAndBackoff( roleCredentials = await retryAndBackoff(
async () => { async () => {
return assumeRole({ return assumeRole({
@@ -164,17 +174,16 @@ export async function run() {
}); });
}, },
!disableRetry, !disableRetry,
maxRetries maxRetries,
); );
// eslint-disable-next-line no-unmodified-loop-condition
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials)); } while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser!.AssumedRoleId!}`); core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser?.AssumedRoleId}`);
exportCredentials(roleCredentials.Credentials, outputCredentials); exportCredentials(roleCredentials.Credentials, outputCredentials);
// We need to validate the credentials in 2 of our use-cases // We need to validate the credentials in 2 of our use-cases
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable // First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// is set to `true` then we are NOT in a self-hosted runner. // is set to `true` then we are NOT in a self-hosted runner.
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets) // Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
if (!process.env['GITHUB_ACTIONS'] || AccessKeyId) { if (!process.env.GITHUB_ACTIONS || AccessKeyId) {
await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId); await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId);
} }
await exportAccountId(credentialsClient, maskAccountId); await exportAccountId(credentialsClient, maskAccountId);
@@ -184,7 +193,7 @@ export async function run() {
} catch (error) { } catch (error) {
core.setFailed(errorMessage(error)); core.setFailed(errorMessage(error));
const showStackTrace = process.env['SHOW_STACK_TRACE']; const showStackTrace = process.env.SHOW_STACK_TRACE;
if (showStackTrace === 'true') { if (showStackTrace === 'true') {
throw error; throw error;
+29 -33
View File
@@ -1,35 +1,34 @@
import * as core from '@actions/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup } from '../src/cleanup'; import { cleanup } from '../src/cleanup';
import * as core from '@actions/core';
import { mockClient } from 'aws-sdk-client-mock';
import { STSClient } from '@aws-sdk/client-sts';
import mocks from './mockinputs.test';
const FAKE_ACCESS_KEY_ID = 'MY-AWS-ACCESS-KEY-ID'; const mockedSTSClient = mockClient(STSClient);
const FAKE_SECRET_ACCESS_KEY = 'MY-AWS-SECRET-ACCESS-KEY';
const FAKE_SESSION_TOKEN = 'MY-AWS-SESSION-TOKEN';
const FAKE_REGION = 'fake-region-1';
const ACTION_ENVIRONMENT_VARIABLES = {
AWS_ACCESS_KEY_ID: FAKE_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: FAKE_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN: FAKE_SESSION_TOKEN,
AWS_DEFAULT_REGION: FAKE_REGION,
AWS_REGION: FAKE_REGION,
};
describe('Configure AWS Credentials', () => {
const OLD_ENV = process.env;
describe('Configure AWS Credentials cleanup', {}, () => {
beforeEach(() => { beforeEach(() => {
jest.resetModules(); // Reset mock state
jest.spyOn(core, 'exportVariable').mockImplementation(); vi.restoreAllMocks();
jest.spyOn(core, 'setSecret').mockImplementation(); mockedSTSClient.reset();
jest.spyOn(core, 'setOutput').mockImplementation(); // Mock GitHub Actions core functions
jest.spyOn(core, 'setFailed').mockImplementation(); vi.spyOn(core, 'exportVariable').mockImplementation((_n, _v) => {});
process.env = { ...OLD_ENV, ...ACTION_ENVIRONMENT_VARIABLES }; vi.spyOn(core, 'setSecret').mockImplementation((_s) => {});
vi.spyOn(core, 'setFailed').mockImplementation((_m) => {});
vi.spyOn(core, 'setOutput').mockImplementation((_n, _v) => {});
vi.spyOn(core, 'debug').mockImplementation((_m) => {});
vi.spyOn(core, 'info').mockImplementation((_m) => {});
process.env = {
...mocks.envs,
AWS_ACCESS_KEY_ID: 'CLEANUPTEST',
AWS_SECRET_ACCESS_KEY: 'CLEANUPTEST',
AWS_SESSION_TOKEN: 'CLEANUPTEST',
AWS_REGION: 'CLEANUPTEST',
AWS_DEFAULT_REGION: 'CLEANUPTEST',
};
}); });
it('replaces AWS credential and region environment variables with empty strings', {}, () => {
afterEach(() => {
process.env = OLD_ENV;
});
test('replaces AWS credential and region env vars with empty strings', () => {
cleanup(); cleanup();
expect(core.setFailed).toHaveBeenCalledTimes(0); expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.exportVariable).toHaveBeenCalledTimes(5); expect(core.exportVariable).toHaveBeenCalledTimes(5);
@@ -39,14 +38,11 @@ describe('Configure AWS Credentials', () => {
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', ''); expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', '');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', ''); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', '');
}); });
it('handles errors', {}, () => {
test('error is caught and fails the action', () => { vi.spyOn(core, 'exportVariable').mockImplementationOnce(() => {
jest.spyOn(core, 'exportVariable').mockImplementation(() => { throw new Error('Test error');
throw new Error();
}); });
cleanup(); cleanup();
expect(core.setFailed).toHaveBeenCalled(); expect(core.setFailed).toHaveBeenCalled();
}); });
}); });
+35 -16
View File
@@ -1,26 +1,45 @@
import { describe, it, expect, vi } from 'vitest';
import * as helpers from '../src/helpers'; import * as helpers from '../src/helpers';
describe('helpers', () => { import * as core from '@actions/core';
import { before, beforeEach } from 'node:test';
describe('Configure AWS Credentials helpers', {}, () => {
beforeEach(() => { beforeEach(() => {
jest.resetModules(); vi.restoreAllMocks();
jest.clearAllMocks();
}); });
it('removes brackets from GitHub Actor', {}, () => {
test('removes brackets from GitHub Actor', () => { const actor = 'actor[bot]';
expect(helpers.sanitizeGitHubVariables('foo[bot]')).toEqual('foo_bot_'); expect(helpers.sanitizeGitHubVariables(actor)).toBe('actor_bot_');
}); });
it('can sleep', {}, () => {
test('removes special characters from worflow names', () => { const sleep = helpers.defaultSleep(10);
expect(Promise.race([sleep, new Promise((_, reject) => setTimeout(reject, 20))])).resolves.toBe(undefined);
});
it('removes special characters from workflow names', {}, () => {
expect(helpers.sanitizeGitHubVariables('sdf234@#$%$^&*()_+{}|:"<>?')).toEqual('sdf234@__________+___:____'); expect(helpers.sanitizeGitHubVariables('sdf234@#$%$^&*()_+{}|:"<>?')).toEqual('sdf234@__________+___:____');
}); });
it("doesn't retry non-retryable errors", {}, async () => {
test('can sleep', () => { const fn = vi.fn().mockRejectedValue('i am not retryable');
const sleep = helpers.defaultSleep(10);
expect(Promise.race([sleep, new Promise((_res, rej) => setTimeout(rej, 20))])).resolves;
});
test("backoff function doesn't retry non-retryable errors", async () => {
const fn = jest.fn().mockRejectedValue('i am not retryable');
await expect(helpers.retryAndBackoff(fn, false)).rejects.toMatch('i am not retryable'); await expect(helpers.retryAndBackoff(fn, false)).rejects.toMatch('i am not retryable');
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
it('can output creds when told to', {}, () => {
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test' }, true);
expect(core.setOutput).toHaveBeenCalledTimes(3);
expect(core.setSecret).toHaveBeenCalledTimes(3);
expect(core.exportVariable).toHaveBeenCalledTimes(3);
});
it('can unset credentials', {}, () => {
const env = process.env;
helpers.unsetCredentials();
expect(process.env.AWS_ACCESS_KEY_ID).toBeUndefined;
expect(process.env.AWS_SECRET_ACCESS_KEY).toBeUndefined;
expect(process.env.AWS_SESSION_TOKEN).toBeUndefined;
expect(process.env.AWS_REGION).toBeUndefined;
expect(process.env.AWS_DEFAULT_REGION).toBeUndefined;
process.env = env;
});
}); });
+264 -817
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
import type * as core from '@actions/core';
const inputs = {
GH_OIDC_INPUTS: {
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'special-characters-workaround': 'true',
},
IAM_USER_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'aws-region': 'fake-region-1',
},
IAM_ASSUMEROLE_INPUTS: {
'aws-access-key-id': 'MYAWSACCESSKEYID',
'aws-secret-access-key': 'MYAWSSECRETACCESSKEY',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
},
WEBIDENTITY_TOKEN_FILE_INPUTS: {
'web-identity-token-file': 'file.txt',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
},
EXISTING_ROLE_INPUTS: {
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'role-chaining': 'true',
'aws-region': 'fake-region-1',
},
};
const envs = {
GITHUB_REPOSITORY: 'MY-REPOSITORY-NAME',
GITHUB_WORKFLOW: 'MY-WORKFLOW-ID',
GITHUB_ACTION: 'MY-ACTION-NAME',
GITHUB_ACTOR: 'MY-USERNAME[bot]',
GITHUB_SHA: 'MY-COMMIT-ID',
GITHUB_WORKSPACE: '/home/github',
GITHUB_ACTIONS: 'true',
};
const outputs = {
STS_CREDENTIALS: {
Credentials: {
AccessKeyId: 'STSAWSACCESSKEYID',
SecretAccessKey: 'STSAWSSECRETACCESSKEY',
SessionToken: 'STSAWSSESSIONTOKEN',
Expiration: new Date(8640000000000000),
},
AssumedRoleUser: {
Arn: 'arn:aws:sts::111111111111:assumed-role/MY-ROLE/',
AssumedRoleId: 'AROAFAKEASSUMEDROLEID',
},
},
GET_CALLER_IDENTITY: {
Account: '111111111111',
Arn: 'arn:aws:iam::111111111111:role/MY-ROLE',
},
FAKE_STS_ACCESS_KEY_ID: 'STSAWSACCESSKEYID',
FAKE_STS_SECRET_ACCESS_KEY: 'STSAWSSECRETACCESSKEY',
FAKE_STS_SESSION_TOKEN: 'STSAWSSESSIONTOKEN',
ODD_CHARACTER_CREDENTIALS: {
Credentials: {
AccessKeyId: 'STSA#$%^&',
SecretAccessKey: 'STSA#$%^&Key',
SessionToken: 'STSA#$%^',
Expiration: new Date(8640000000000000),
},
AssumedRoleUser: {
Arn: 'arn:aws:sts::111111111111:assumed-role/MY-ROLE/',
AssumedRoleId: 'AROAFAKEASSUMEDROLEID',
},
},
};
export default {
getInput: (fakeEnv: Record<string, string>) => {
return (name: string, options?: core.InputOptions): string => {
if (!fakeEnv[name]) {
if (options?.required) throw new Error(`Input ${name} not found`);
return '';
}
return fakeEnv[name];
};
},
getMultilineInput: (fakeEnv: Record<string, string[]>) => {
return (name: string, options?: core.InputOptions): string[] => {
if (!fakeEnv[name]) {
if (options?.required) throw new Error(`Input ${name} not found`);
return [];
}
return fakeEnv[name];
};
},
...inputs,
outputs,
envs,
} as const;
+2 -3
View File
@@ -7,9 +7,8 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true, "noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"module": "CommonJS", "module": "CommonJS",
"resolveJsonModule": true, "resolveJsonModule": true,
@@ -27,7 +26,7 @@
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"test/**/*.ts" "test/**/*.test.ts"
], ],
"exclude": [], "exclude": [],
} }
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
passWithNoTests: true,
include: ['test/**/*.test.ts'],
coverage: { enabled: true },
},
});