chore: replay 6.2 devel changes onto main (#1807)

* chore(deps-dev): bump vitest from 3.2.4 to 4.1.5 (#1748)

* chore(deps-dev): bump vitest from 3.2.4 to 4.1.5

Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 4.1.5.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...



* chore(deps-dev): update @vitest/coverage-v8

Bump @vitest/coverage-v8 to ^4.1.5 to match peer dependency, and move
vi.mock('node:fs') calls to the top level of test files to reflect
actual hoisting semantics required by vitest 4.x.

---------




(cherry picked from commit 78f374f6d1)

* chore(deps): bump @actions/core from 2.0.3 to 3.0.1 (#1746)

* chore(deps): bump @actions/core from 2.0.3 to 3.0.1

Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 2.0.3 to 3.0.1.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...



* chore: update test mocks for @actions/core ESM

@actions/core v3 ships as an ESM module with non-configurable exports,
breaking vi.spyOn(). Switch to vi.mock('@actions/core') which
intercepts at the module loader level.

---------




(cherry picked from commit 64d8e82527)

* chore(deps): bump @aws-sdk/client-sts from 3.1043.0 to 3.1044.0 (#1754)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1043.0 to 3.1044.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1044.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1042.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



(cherry picked from commit 4cfda40a13)

* chore: bump unit test node version (#1758)

(cherry picked from commit 39d1702721)

* chore: automatic major version tagging (#1565)

* Update release-please.yml to auto-update version tag

* chore: configure release-please auto floating tag

---------


(cherry picked from commit c36525a567)

* feat: Allow custom session tags to be passed when assuming a role (#1759)

* Add possibility to input custom session tags

* Use json for input to custom-tags, add documentation for custom-tags

* Add more examples

* Simplify example to avoid parse error

* Add input validation for custom tags

* Fix unit tests for custom-tags

* Add debugging message

* Skip failing test for now

* Build package

* Remove some unused validation for custom tags

* feat: add validation for custom session tags

Harden the custom-tags feature against misuse and
misconfiguration:

- Validate input is a JSON object (reject arrays, primitives, null)
- Enforce STS tag constraints: key length (128), value length (256),
  allowed characters
- Reject nested object/array values that would silently stringify to
  '[object Object]'
- Block overriding default session tags (GitHub, Repository,
  Workflow, etc.)
- Enforce 50-tag session limit
- Warn when custom-tags used with OIDC or web identity
- Fix missing await on helpers test assertion
- Remove unused CUSTOM_TAGS_JSON_INPUTS fixture
- Normalize test mocking to vi.mocked() pattern

---------


(cherry picked from commit 61f50f630f)

* chore: configure codeql to ignore generated code (#1760)

(cherry picked from commit dc2353e57a)

* feat: support custom STS endpoints (#1762)

Closes #1067. This is a advanced option
and is not needed for most deployments.

(cherry picked from commit 8d52d05d7a)

* chore: automate README version bumping (#1763)

Closes #1420.

(cherry picked from commit 07ada0fe07)

* feat: add more retry logic and better logging (#1764)

Wraps exportAccountId and validateCredentials calls in retryAndBackoff.
Closes #1681. Adds a label parameter to retryAndBackoff for better
info-level log messages.

(cherry picked from commit 540d0c13ae)

* feat: add regex validation to role-session-name (#1765)

Previously invalid role session names would get errors from the STS
API instead of this action rejecting them, causing unnecessary retries.
Now we check them and fail early.

Closes #1656. That FR recommended that we sanitize the name before
sending to STS, but instead we error to not silently change the user's
selected session name (avoiding the potential security sharp edge)

(cherry picked from commit e35449909c)

* chore: update documentation for environment workflows (#1766)

Closes #1238.

(cherry picked from commit 3f7e1b63d7)

* chore(deps): bump @aws-sdk/client-sts from 3.1044.0 to 3.1045.0 (#1767)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1044.0 to 3.1045.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1045.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1045.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



(cherry picked from commit a388f23f7d)

* chore(deps-dev): bump @vitest/coverage-v8 from 4.1.5 to 4.1.6 (#1768)

Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...



(cherry picked from commit 1fb495c4b2)

* chore(deps-dev): bump @smithy/property-provider from 4.2.14 to 4.3.1 (#1771)

Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.2.14 to 4.3.1.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.1/packages/property-provider)

---
updated-dependencies:
- dependency-name: "@smithy/property-provider"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...



(cherry picked from commit 1ab31502aa)

* chore(deps): bump @smithy/node-http-handler from 4.6.1 to 4.7.1 (#1770)

Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.6.1 to 4.7.1.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.1/packages/node-http-handler)

---
updated-dependencies:
- dependency-name: "@smithy/node-http-handler"
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



(cherry picked from commit dbd503f368)

* chore(deps-dev): bump @biomejs/biome from 2.4.14 to 2.4.15 (#1772)

Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.14 to 2.4.15.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.4.15/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.4.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
...



(cherry picked from commit 7521c55910)

* chore(deps-dev): bump @types/node from 25.6.0 to 25.7.0 (#1773)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.7.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...



(cherry picked from commit ef734cca81)

* feat: expose run id in STS client user-agent (#1774)

* feat: expose run id in STS client user-agent

Closes #483.
This commit modifies the user-agent string so that it includes the
GITHUB_RUN_ID and the GITHUB_RUN_ATTEMPT, in the format typically used
by the SDK. User agent strings are logged to CloudTrail, allowing users
to correlate CloudTrail events with GHA runs. We took this approach
instead of logging the ACCESS_KEY_ID as suggested in the issue to avoid
logging sensitive information.

* feat: add github_action to ua string

(cherry picked from commit 29d1be3027)

* feat: add additional session tags by default (#1775)

Closes #390.
Note that 50 session tags are the AWS default, and this commit changes
our default set from 7 tags to 15 tags. This commit includes logic to
split the tags into "required" vs "overridable". Required tags are this
action's previous defaults and could never be overridden. Overridable
tags are the new set and can be overridden by custom-tags. The action
will not add tags if the addition plus the required plus the user's
custom tags exceed the AWS limit of 50 total tags. This ensures
backwards compat for the tag additions.

(cherry picked from commit e0ba768507)

* chore: document forgejo compatibility (#1776)

* chore: document forgejo compatibility

* chore: linting fixes

(cherry picked from commit f35a7d7d7e)

* fix: skip credential check on output-env-credentials: false (#1778)

Closes #1554.

(cherry picked from commit 58e7c47adf)

* chore: update README for additional claim support (#1779)

* chore: update README for additional claim support

* chore: lint fix (whitespace)

(cherry picked from commit 713aaabfec)

* chore(deps): bump @smithy/node-http-handler from 4.7.1 to 4.7.3 (#1781)

Bumps [@smithy/node-http-handler](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/node-http-handler) from 4.7.1 to 4.7.3.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/node-http-handler/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/node-http-handler@4.7.3/packages/node-http-handler)

---
updated-dependencies:
- dependency-name: "@smithy/node-http-handler"
  dependency-version: 4.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...



(cherry picked from commit a7c33ae483)

* chore(deps-dev): bump @types/node from 25.7.0 to 25.9.0 (#1785)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.7.0 to 25.9.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...



(cherry picked from commit ffde832a1d)

* chore(deps-dev): bump @aws-sdk/credential-provider-env (#1784)

Bumps [@aws-sdk/credential-provider-env](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-env) from 3.972.34 to 3.972.38.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages-internal/credential-provider-env/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/HEAD/packages-internal/credential-provider-env)

---
updated-dependencies:
- dependency-name: "@aws-sdk/credential-provider-env"
  dependency-version: 3.972.38
  dependency-type: direct:development
  update-type: version-update:semver-patch
...



(cherry picked from commit bc1093db1d)

* chore(deps-dev): bump @smithy/property-provider from 4.3.1 to 4.3.3 (#1783)

Bumps [@smithy/property-provider](https://github.com/smithy-lang/smithy-typescript/tree/HEAD/packages/property-provider) from 4.3.1 to 4.3.3.
- [Release notes](https://github.com/smithy-lang/smithy-typescript/releases)
- [Changelog](https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-typescript/commits/@smithy/property-provider@4.3.3/packages/property-provider)

---
updated-dependencies:
- dependency-name: "@smithy/property-provider"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...



(cherry picked from commit fe6ad3af19)

* chore(deps): bump @aws-sdk/client-sts from 3.1045.0 to 3.1049.0 (#1782)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1045.0 to 3.1049.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1049.0/clients/client-sts)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-sts"
  dependency-version: 3.1049.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...



(cherry picked from commit 4684f47f89)

* chore: reconcile lockfile and test formatting

* chore: Update dist

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Lehmann <lehmanmj@amazon.com>
Co-authored-by: Sylvain Verly <sylvain.verly@gmail.com>
This commit is contained in:
Tom Keller
2026-05-28 14:44:54 -07:00
committed by GitHub
parent 99214aa688
commit 4ab3589ed2
21 changed files with 7612 additions and 11695 deletions
+15 -10
View File
@@ -3,15 +3,18 @@ import { STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { ProxyAgent } from 'proxy-agent';
import { errorMessage, getCallerIdentity } from './helpers';
import { buildCustomUserAgent, errorMessage, getCallerIdentity } from './helpers';
import { ProxyResolver } from './ProxyResolver';
const USER_AGENT = 'configure-aws-credentials-for-github-actions';
if (!process.env.AWS_EXECUTION_ENV) {
process.env.AWS_EXECUTION_ENV = 'GitHubActions';
}
export interface CredentialsClientProps {
region?: string;
proxyServer?: string;
noProxy?: string;
stsEndpoint?: string;
roleChaining: boolean;
}
@@ -19,6 +22,7 @@ export class CredentialsClient {
public region?: string;
private _stsClient?: STSClient;
private readonly requestHandler?: NodeHttpHandler;
private readonly stsEndpoint?: string;
private roleChaining?: boolean;
constructor(props: CredentialsClientProps) {
@@ -41,19 +45,20 @@ export class CredentialsClient {
httpAgent: handler,
});
}
if (props.stsEndpoint) {
this.stsEndpoint = props.stsEndpoint;
}
this.roleChaining = props.roleChaining;
}
public get stsClient(): STSClient {
if (!this._stsClient || this.roleChaining) {
const config = { customUserAgent: USER_AGENT } as {
customUserAgent: string;
region?: string;
requestHandler?: NodeHttpHandler;
};
if (this.region !== undefined) config.region = this.region;
if (this.requestHandler !== undefined) config.requestHandler = this.requestHandler;
this._stsClient = new STSClient(config);
this._stsClient = new STSClient({
customUserAgent: buildCustomUserAgent(),
...(this.region !== undefined && { region: this.region }),
...(this.stsEndpoint !== undefined && { endpoint: this.stsEndpoint }),
...(this.requestHandler !== undefined && { requestHandler: this.requestHandler }),
});
}
return this._stsClient;
}
+119 -15
View File
@@ -77,6 +77,98 @@ export interface assumeRoleParams {
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: { arn: string }[];
customTags?: string;
}
const TAG_KEY_REGEX = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$/u;
const TAG_VALUE_REGEX = /^[\p{L}\p{Z}\p{N}_.:/=+\-@]*$/u;
const MAX_TAG_KEY_LENGTH = 128;
const MAX_TAG_VALUE_LENGTH = 256;
const MAX_SESSION_TAGS = 50;
// Identity/audit primitives. Always emitted and cannot be overridden by custom-tags.
const PROTECTED_TAG_SOURCES: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'Repository', envVar: 'GITHUB_REPOSITORY' },
{ key: 'Workflow', envVar: 'GITHUB_WORKFLOW' },
{ key: 'Action', envVar: 'GITHUB_ACTION' },
{ key: 'Actor', envVar: 'GITHUB_ACTOR' },
{ key: 'Commit', envVar: 'GITHUB_SHA' },
{ key: 'Branch', envVar: 'GITHUB_REF' },
];
// Convenience metadata. Custom-tags may override (suppresses the default for that key).
// Listed in priority order; lower-priority entries are dropped first if the user's custom-tags
// would push the total above MAX_SESSION_TAGS.
const OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY: ReadonlyArray<{ key: string; envVar: string }> = [
{ key: 'EventName', envVar: 'GITHUB_EVENT_NAME' },
{ key: 'BaseRef', envVar: 'GITHUB_BASE_REF' },
{ key: 'HeadRef', envVar: 'GITHUB_HEAD_REF' },
{ key: 'RefName', envVar: 'GITHUB_REF_NAME' },
{ key: 'RunId', envVar: 'GITHUB_RUN_ID' },
{ key: 'RefType', envVar: 'GITHUB_REF_TYPE' },
{ key: 'Job', envVar: 'GITHUB_JOB' },
{ key: 'TriggeringActor', envVar: 'GITHUB_TRIGGERING_ACTOR' },
];
const PROTECTED_TAG_KEYS = new Set<string>(['GitHub', ...PROTECTED_TAG_SOURCES.map((s) => s.key)]);
export function parseAndValidateCustomTags(customTags: string, existingTags: Tag[]): Tag[] {
let parsed: unknown;
try {
parsed = JSON.parse(customTags);
} catch {
throw new Error('custom-tags: input is not valid JSON');
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('custom-tags: input must be a JSON object (not an array or primitive)');
}
const newTags: Tag[] = [];
for (const [key, value] of Object.entries(parsed)) {
if (typeof value === 'object') {
throw new Error(
`custom-tags: value for key '${key}' must be a string, number, or boolean (not an object or array)`,
);
}
const stringValue = String(value);
if (key.length === 0 || key.length > MAX_TAG_KEY_LENGTH) {
throw new Error(`custom-tags: key '${key}' must be between 1 and ${MAX_TAG_KEY_LENGTH} characters`);
}
if (stringValue.length > MAX_TAG_VALUE_LENGTH) {
throw new Error(
`custom-tags: value for key '${key}' exceeds maximum length of ${MAX_TAG_VALUE_LENGTH} characters`,
);
}
if (!TAG_KEY_REGEX.test(key)) {
throw new Error(
`custom-tags: key '${key}' contains invalid characters. Allowed: unicode letters, digits, spaces, and _.:/=+-@`,
);
}
if (stringValue.length > 0 && !TAG_VALUE_REGEX.test(stringValue)) {
throw new Error(
`custom-tags: value for key '${key}' contains invalid characters. Allowed: unicode letters, digits, spaces, and _.:/=+-@`,
);
}
if (PROTECTED_TAG_KEYS.has(key)) {
throw new Error(
`custom-tags: key '${key}' conflicts with a protected session tag set by this action and cannot be overridden`,
);
}
newTags.push({ Key: key, Value: stringValue });
}
if (existingTags.length + newTags.length > MAX_SESSION_TAGS) {
throw new Error(
`custom-tags: total session tags (${existingTags.length + newTags.length}) would exceed the AWS limit of ${MAX_SESSION_TAGS}`,
);
}
return newTags;
}
export async function assumeRole(params: assumeRoleParams) {
@@ -93,6 +185,7 @@ export async function assumeRole(params: assumeRoleParams) {
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
} = { ...params };
// Load GitHub environment variables
@@ -101,26 +194,37 @@ export async function assumeRole(params: assumeRoleParams) {
throw new Error('Missing required environment variables. Are you running in GitHub Actions?');
}
// Load role session tags
const tagArray: Tag[] = [
{ Key: 'GitHub', Value: 'Actions' },
{ Key: 'Repository', Value: GITHUB_REPOSITORY },
{ Key: 'Workflow', Value: sanitizeGitHubVariables(GITHUB_WORKFLOW) },
{ Key: 'Action', Value: GITHUB_ACTION },
{ Key: 'Actor', Value: sanitizeGitHubVariables(GITHUB_ACTOR) },
{ Key: 'Commit', Value: GITHUB_SHA },
];
if (process.env.GITHUB_REF) {
tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
// Build session tags. Values are sanitized because the AWS tag value spec is more
// restrictive than permissible characters in environment variables.
const protectedTags: Tag[] = [{ Key: 'GitHub', Value: 'Actions' }];
for (const { key, envVar } of PROTECTED_TAG_SOURCES) {
const value = process.env[envVar];
if (value) {
protectedTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
const parsedCustomTags: Tag[] = customTags ? parseAndValidateCustomTags(customTags, protectedTags) : [];
const customTagKeys = new Set(parsedCustomTags.map((t) => t.Key));
const availableOverrideableSlots = MAX_SESSION_TAGS - protectedTags.length - parsedCustomTags.length;
const overrideableTags: Tag[] = [];
for (const { key, envVar } of OVERRIDEABLE_TAG_SOURCES_BY_PRIORITY) {
if (overrideableTags.length >= availableOverrideableSlots) break;
if (customTagKeys.has(key)) continue;
const value = process.env[envVar];
if (value) {
overrideableTags.push({ Key: key, Value: sanitizeGitHubVariables(value) });
}
}
const tagArray: Tag[] = [...protectedTags, ...overrideableTags, ...parsedCustomTags];
const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) {
core.debug('Role session tagging has been skipped.');
} else {
core.debug(`${tags.length} role session tags are being used.`);
core.debug(`${tags.length} role session tags are being used:`);
}
//only populate transitiveTagKeys array if user is actually using session tagging
+27 -4
View File
@@ -3,11 +3,32 @@ import * as path from 'node:path';
import * as core from '@actions/core';
import type { Credentials, STSClient } from '@aws-sdk/client-sts';
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import type { UserAgent } from '@smithy/types';
import type { CredentialsClient } from './CredentialsClient';
const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
const USER_AGENT_PREFIX = 'configure-aws-credentials-for-github-actions';
const UA_FIELDS: ReadonlyArray<{ env: string; label: string; pattern: RegExp }> = [
{ env: 'GITHUB_ACTION', label: 'action', pattern: /^[A-Za-z0-9_-]{1,128}$/ },
{ env: 'GITHUB_RUN_ID', label: 'run_id', pattern: /^[0-9]{1,20}$/ },
{ env: 'GITHUB_RUN_ATTEMPT', label: 'attempt', pattern: /^[0-9]{1,10}$/ },
];
export function buildCustomUserAgent(): UserAgent {
const tokens: UserAgent = [[USER_AGENT_PREFIX]];
for (const { env, label, pattern } of UA_FIELDS) {
const value = process.env[env];
if (value === undefined) continue;
if (pattern.test(value)) {
tokens.push(['md', `${label}#${value}`]);
} else {
core.warning(`${env} has unexpected format; omitting from User-Agent`);
}
}
return tokens;
}
export function translateEnvVariables() {
const envVars = [
@@ -191,6 +212,7 @@ export async function retryAndBackoff<T>(
maxRetries = 12,
retries = 0,
base = 50,
label?: string,
): Promise<T> {
try {
return await fn();
@@ -202,20 +224,21 @@ export async function retryAndBackoff<T>(
// It's retryable, so sleep and retry.
const delay = Math.random() * (2 ** retries * base);
const nextRetry = retries + 1;
const opName = label ? ` ${label}` : '';
core.debug(
`retryAndBackoff: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. ` +
core.info(
`Retry${opName}: attempt ${nextRetry} of ${maxRetries} failed: ${errorMessage(err)}. ` +
`Retrying after ${Math.floor(delay)}ms.`,
);
await sleep(delay);
if (nextRetry >= maxRetries) {
core.debug('retryAndBackoff: reached max retries; giving up.');
core.info(`Retry${opName}: reached max retries (${maxRetries}); giving up.`);
throw err;
}
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base);
return await retryAndBackoff(fn, isRetryable, maxRetries, nextRetry, base, label);
}
}
+90 -48
View File
@@ -19,6 +19,7 @@ import { writeProfileFiles } from './profileManager';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions';
const REGION_REGEX = /^[a-z0-9-]+$/g;
const ROLE_SESSION_NAME_REGEX = /^[\w+=,.@-]*$/;
export async function run() {
try {
@@ -43,6 +44,7 @@ export async function run() {
const roleSkipSessionTagging = getBooleanInput('role-skip-session-tagging', { required: false });
const transitiveTagKeys = core.getMultilineInput('transitive-tag-keys', { required: false });
const proxyServer = core.getInput('http-proxy', { required: false }) || process.env.HTTP_PROXY;
const customTags = core.getInput('custom-tags', { required: false });
const inlineSessionPolicy = core.getInput('inline-session-policy', { required: false });
const managedSessionPolicies = core.getMultilineInput('managed-session-policies', { required: false }).map((p) => {
return { arn: p };
@@ -63,6 +65,7 @@ export async function run() {
.map((s) => s.trim());
const forceSkipOidc = getBooleanInput('force-skip-oidc', { required: false });
const noProxy = core.getInput('no-proxy', { required: false });
const stsEndpoint = core.getInput('sts-endpoint', { required: false });
const globalTimeout = Number.parseInt(core.getInput('action-timeout-s', { required: false })) || 0;
let timeoutId: NodeJS.Timeout | undefined;
@@ -88,6 +91,9 @@ export async function run() {
maxRetries = 1;
}
const withRetry = <T>(fn: () => Promise<T>, label: string): Promise<T> =>
retryAndBackoff(fn, !disableRetry, maxRetries, 0, 50, label);
// Logic to decide whether to attempt to use OIDC or not
const useGitHubOIDCProvider = () => {
if (forceSkipOidc) return false;
@@ -124,15 +130,33 @@ export async function run() {
throw new Error(`Region is not valid: ${region}`);
}
if (roleSessionName.length < 2 || roleSessionName.length > 64) {
throw new Error(
`Role session name must be between 2 and 64 characters, got ${roleSessionName.length}: '${roleSessionName}'`,
);
}
if (!roleSessionName.match(ROLE_SESSION_NAME_REGEX)) {
throw new Error(
`Role session name is not valid: '${roleSessionName}'. Must satisfy regular expression pattern: [\\w+=,.@-]*`,
);
}
exportRegion(region, outputEnvCredentials);
// Instantiate credentials client
const clientProps: { region: string; proxyServer?: string; noProxy?: string; roleChaining: boolean } = {
const clientProps: {
region: string;
proxyServer?: string;
noProxy?: string;
stsEndpoint?: string;
roleChaining: boolean;
} = {
region,
roleChaining,
};
if (proxyServer) clientProps.proxyServer = proxyServer;
if (noProxy) clientProps.noProxy = noProxy;
if (stsEndpoint) clientProps.stsEndpoint = stsEndpoint;
const credentialsClient = new CredentialsClient(clientProps);
let sourceAccountId: string;
let webIdentityToken: string;
@@ -152,13 +176,9 @@ export async function run() {
// Else, export credentials provided as input
if (useGitHubOIDCProvider()) {
try {
webIdentityToken = await retryAndBackoff(
async () => {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries,
);
webIdentityToken = await withRetry(async () => {
return core.getIDToken(audience);
}, 'getIDToken');
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
}
@@ -179,59 +199,77 @@ export async function run() {
}
} else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
await withRetry(
() => credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds),
'validateCredentials',
);
sourceAccountId = await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
if (AccessKeyId || roleChaining) {
// Validate that the SDK can actually pick up credentials.
// This validates cases where this action is using existing environment credentials,
// and cases where the user intended to provide input credentials but the secrets inputs resolved to empty strings.
await credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
// Skip when output-env-credentials is false: input IAM keys were not written to env, so
// the default chain would resolve to ambient runner credentials and the access-key check
// would spuriously fail (see #1554).
if (outputEnvCredentials) {
await withRetry(
() => credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds),
'validateCredentials',
);
sourceAccountId = await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
}
if (customTags && (useGitHubOIDCProvider() || webIdentityTokenFile)) {
core.warning(
"'custom-tags' is set but will be ignored because session tags cannot be applied when using OIDC or web identity token authentication. " +
'Tags are controlled by the identity provider token claims in these authentication flows.',
);
}
// Get role credentials if configured to do so
if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput;
do {
roleCredentials = await retryAndBackoff(
async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
transitiveTagKeys,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
});
},
!disableRetry,
maxRetries,
);
roleCredentials = await withRetry(async () => {
return assumeRole({
credentialsClient,
sourceAccountId,
roleToAssume,
roleExternalId,
roleDuration,
roleSessionName,
roleSkipSessionTagging,
transitiveTagKeys,
webIdentityTokenFile,
webIdentityToken,
inlineSessionPolicy,
managedSessionPolicies,
customTags,
});
}, 'AssumeRole');
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser?.AssumedRoleId}`);
exportCredentials(roleCredentials.Credentials, outputCredentials, outputEnvCredentials);
// We need to validate the credentials in 2 of our use-cases
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// 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)
// If we are using a profile, don't validate credentials yet (since they most likely won't be in the environment).
// Wait until after creds are written to the profile file to try validation.
if ((!process.env.GITHUB_ACTIONS || AccessKeyId) && !awsProfile) {
await credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
// Validate that the SDK can pick up the assumed-role credentials from the environment.
// Skip when output-env-credentials is false: the credentials were never written to env,
// so the default credential provider chain would resolve to ambient runner credentials
// (e.g. an EC2 instance profile) and the access-key-id check would spuriously fail.
// Skip when using a profile: validation runs after the profile file is written below.
if ((!process.env.GITHUB_ACTIONS || AccessKeyId) && !awsProfile && outputEnvCredentials) {
await withRetry(
() =>
credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
),
'validateCredentials',
);
}
if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId);
await withRetry(() => exportAccountId(credentialsClient, maskAccountId), 'exportAccountId');
}
// Write profile files if profile mode is enabled
@@ -244,10 +282,14 @@ export async function run() {
// We then validate the credentials to make sure they work.
if (AccessKeyId || !process.env.GITHUB_ACTIONS) {
writeProfileFiles(awsProfile, roleCredentials.Credentials, region, true);
await credentialsClient.validateCredentials(
roleCredentials.Credentials.AccessKeyId,
roleChaining,
expectedAccountIds,
await withRetry(
() =>
credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId,
roleChaining,
expectedAccountIds,
),
'validateCredentials',
);
} else {
writeProfileFiles(awsProfile, roleCredentials.Credentials, region, overwriteAwsProfile);