Merge branch 'main' into remove-integ

This commit is contained in:
Tom Keller
2025-07-25 21:23:12 -07:00
committed by GitHub
43 changed files with 31432 additions and 29341 deletions

View File

@@ -1,2 +0,0 @@
build
dist

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]

View File

@@ -12,6 +12,14 @@ body:
description: What is the problem? A clear and concise description of the bug.
validations:
required: true
- type: checkboxes
id: regression
attributes:
label: Regression Issue
description: What is a regression? If it worked in a previous version but doesn't in the latest version, it's considered a regression. In this case, please provide specific version number in the report.
options:
- label: Select this option if this issue appears to be a regression.
required: false
- type: textarea
id: expected
attributes:

View File

@@ -0,0 +1,32 @@
on:
pull_request_review:
types: submitted
jobs:
approved_pr:
name: Automerge approved PRs
permissions:
contents: write
pull-requests: write
id-token: write
if: ${{ github.event.review.state == 'approved' && github.repository == 'aws-actions/configure-aws-credentials' && (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'MEMBER' || github.event.review.user.login == 'aws-sdk-osds') }}
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-west-2
role-to-assume: ${{ secrets.SECRETS_AWS_PACKAGING_ROLE_TO_ASSUME }}
role-duration-seconds: 900
role-session-name: SecretsManagerFetch
- name: Get bot user token
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
parse-json-secrets: true
secret-ids: |
${{ secrets.OSDS_PACKAGING_ROLE }}
- name: Enable PR automerge
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ env.OSDS_ACCESS_TOKEN }}

20
.github/workflows/cawsc-test.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Test Configure AWS Credential
on:
workflow_dispatch:
jobs:
cawsc:
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: CAWSC
uses: aws-actions/configure-aws-credentials@main
with:
aws-region: us-west-2
role-to-assume: ${{ secrets.SECRETS_AWS_PACKAGING_ROLE_TO_ASSUME }}
role-duration-seconds: 900
role-session-name: TestCAWSC
- name: Whoami
run: |
aws sts get-caller-identity

View File

@@ -0,0 +1,41 @@
name: Dependabot auto-approve
on:
pull_request:
workflow_dispatch:
permissions:
pull-requests: write
id-token: write
contents: read
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'aws-actions/configure-aws-credentials' }}
steps:
- name: Get Metadata
id: dependabot-metadata
uses: dependabot/fetch-metadata@v2
- uses: actions/checkout@v4
name: Clone repo
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-west-2
role-to-assume: ${{ secrets.SECRETS_AWS_PACKAGING_ROLE_TO_ASSUME }}
role-duration-seconds: 900
- name: Get bot user token
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
parse-json-secrets: true
secret-ids: |
${{ secrets.OSDS_PACKAGING_ROLE }}
- name: Approve PR if not already approved
run: |
gh pr checkout "$PR_URL"
if [ "$(gh pr status --json reviewDecision - q .currentBranch.reviewDecision)" != "APPROVED" ]; then
gh pr review "$PR_URL" --approve
else echo "PR already approved"
fi
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ env.OSDS_ACCESS_TOKEN }}

View File

@@ -0,0 +1,32 @@
# Apply potential regression label on issues
name: issue-regression-label
on:
issues:
types: [opened, edited]
jobs:
add-regression-label:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Fetch template body
id: check_regression
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TEMPLATE_BODY: ${{ github.event.issue.body }}
with:
script: |
const regressionPattern = /\[x\] Select this option if this issue appears to be a regression\./i;
const template = `${process.env.TEMPLATE_BODY}`
const match = regressionPattern.test(template);
core.setOutput('is_regression', match);
- name: Manage regression label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ steps.check_regression.outputs.is_regression }}" == "true" ]; then
gh issue edit ${{ github.event.issue.number }} --add-label "potential-regression" -R ${{ github.repository }}
else
gh issue edit ${{ github.event.issue.number }} --remove-label "potential-regression" -R ${{ github.repository }}
fi

View File

@@ -39,7 +39,7 @@ jobs:
with:
parse-json-secrets: true
secret-ids: |
OSDS,arn:aws:secretsmanager:us-west-2:206735643321:secret:github-aws-sdk-osds-automation-gebs9n
${{ secrets.OSDS_PACKAGING_ROLE }}
- name: Commit
run: |
echo "::add-mask::${{ env.OSDS_ACCESS_TOKEN }}"
@@ -48,4 +48,4 @@ jobs:
git remote set-url origin https://${{ env.OSDS_ACCESS_TOKEN }}@github.com/aws-actions/configure-aws-credentials.git
git add dist
git commit -m "chore: Update dist" || echo "No changes to commit"
git push origin
git push --force origin

43
.github/workflows/release-please.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
---
name: Release Please
on:
push:
branches:
- main
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-west-2
role-to-assume: ${{ secrets.SECRETS_AWS_PACKAGING_ROLE_TO_ASSUME }}
role-duration-seconds: 900
role-session-name: ${{ github.run_id }}
- name: Get git credentials
uses: aws-actions/aws-secretsmanager-get-secrets@v2
with:
parse-json-secrets: true
secret-ids: |
${{ secrets.OSDS_PACKAGING_ROLE }}
- name: Run release-please
uses: googleapis/release-please-action@v4
with:
release-type: node
token: ${{ env.OSDS_ACCESS_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

View File

@@ -1,46 +0,0 @@
queue_rules:
- name: default
conditions:
# Conditions to merge a queued PR
- check-success=Run unit tests (windows-latest)
- check-success=Run unit tests (ubuntu-latest)
- check-success=Run unit tests (macos-latest)
- "#approved-reviews-by>=1"
- -approved-reviews-by~=author
pull_request_rules:
- name: Automatically merge on CI success and review approval
conditions:
- base~=main|integ-tests
- "#approved-reviews-by>=1"
- -approved-reviews-by~=author
- check-success=Run unit tests (windows-latest)
- check-success=Run unit tests (ubuntu-latest)
- check-success=Run unit tests (macos-latest)
- label!=work-in-progress
- -title~=(WIP|wip)
- -merged
- -closed
- author!=dependabot[bot]
actions:
queue:
method: squash
name: default
- name: Automatically approve and merge Dependabot PRs
conditions:
- base~=main
- author=dependabot[bot]
- check-success=Run unit tests (windows-latest)
- check-success=Run unit tests (ubuntu-latest)
- check-success=Run unit tests (macos-latest)
- -title~=(WIP|wip)
- -label~=(blocked|do-not-merge)
- -merged
- -closed
actions:
review:
type: APPROVE
queue:
method: squash
name: default

View File

@@ -1,8 +0,0 @@
{
"printWidth": 120,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"overrides": []
}

View File

@@ -0,0 +1,5 @@
{
".release-please-manifest.json": "4.0.2",
"package.json": "4.0.2",
".": "4.2.1"
}

View File

@@ -2,6 +2,63 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [4.2.1](https://github.com/aws-actions/configure-aws-credentials/compare/v4.2.0...v4.2.1) (2025-05-14)
### Bug Fixes
* ensure explicit inputs take precedence over environment variables ([e56e6c4](https://github.com/aws-actions/configure-aws-credentials/commit/e56e6c4038915cd5a7238a671fe97f44c98a40b0))
* prioritize explicit inputs over environment variables ([df9c8fe](https://github.com/aws-actions/configure-aws-credentials/commit/df9c8fed6b364f0d1fb0e6e03a0ec26f1ea4e3fc))
## [4.2.0](https://github.com/aws-actions/configure-aws-credentials/compare/v4.1.0...v4.2.0) (2025-05-06)
### Features
* add Expiration field to Outputs ([a4f3267](https://github.com/aws-actions/configure-aws-credentials/commit/a4f326760c1c1bf49ab86051c658d6501816b930))
* Document role-duration-seconds range ([5a0cf01](https://github.com/aws-actions/configure-aws-credentials/commit/5a0cf0167f837dfa7af7d951ba6a78a38dc2b79e))
* support action inputs as environment variables ([#1338](https://github.com/aws-actions/configure-aws-credentials/issues/1338)) ([2c168ad](https://github.com/aws-actions/configure-aws-credentials/commit/2c168adcae62d67531ba83842723c8f30695116a))
### Bug Fixes
* make sure action builds, also fix dependabot autoapprove ([c401b8a](https://github.com/aws-actions/configure-aws-credentials/commit/c401b8a98c5067672f52e0387cdd87d54acfe1fd))
* role chaning on mulitple runs ([#1340](https://github.com/aws-actions/configure-aws-credentials/issues/1340)) ([9e38641](https://github.com/aws-actions/configure-aws-credentials/commit/9e386419117a9edd458297e4f1822a5df7506a03))
## [4.1.0](https://github.com/aws-actions/configure-aws-credentials/compare/v4.0.3...v4.1.0) (2025-02-08)
### Features
* idempotent fetch ([#1289](https://github.com/aws-actions/configure-aws-credentials/issues/1289)) ([eb70354](https://github.com/aws-actions/configure-aws-credentials/commit/eb70354fb423a380b6e4ab4b9f15d2ee9ffae911))
### Bug Fixes
* build failure due to tests ([#1283](https://github.com/aws-actions/configure-aws-credentials/issues/1283)) ([134d71e](https://github.com/aws-actions/configure-aws-credentials/commit/134d71efe0ecbe9ad6965f2f766c0cae63a7685f))
* Dependabot autoapprove ([#1284](https://github.com/aws-actions/configure-aws-credentials/issues/1284)) ([b9ee51d](https://github.com/aws-actions/configure-aws-credentials/commit/b9ee51dc600fe38c892e24f60ca26476e0e0b6de))
* Dependabot autoapprove id-token write permission ([#1285](https://github.com/aws-actions/configure-aws-credentials/issues/1285)) ([f0af89b](https://github.com/aws-actions/configure-aws-credentials/commit/f0af89b102390dcf10ce402195d74a98f24861f3))
* typo ([#1281](https://github.com/aws-actions/configure-aws-credentials/issues/1281)) ([39fd91c](https://github.com/aws-actions/configure-aws-credentials/commit/39fd91c08ed8bf770034de4e62662503e8007d76))
## [4.0.3](https://github.com/aws-actions/configure-aws-credentials/compare/v4.0.2...v4.0.3) (2025-01-27)
### Features
* added release-please action config ([0f88004](https://github.com/aws-actions/configure-aws-credentials/commit/0f88004d9c27e0bdbbc254b3f7c8053cb38f04d7))
### Bug Fixes
* add id-token permission to automerge ([97834a4](https://github.com/aws-actions/configure-aws-credentials/commit/97834a484a5ab3c40fa9e2eb40fcf8041105a573))
* cpy syntax on npm package ([#1195](https://github.com/aws-actions/configure-aws-credentials/issues/1195)) ([83b5a56](https://github.com/aws-actions/configure-aws-credentials/commit/83b5a565471214aec459e234bef606339fe07111))
* force push packaged files to main ([bfd2185](https://github.com/aws-actions/configure-aws-credentials/commit/bfd218503eb87938c29603a551e19c6b594f5fe5))
### Miscellaneous Chores
* release 4.0.3 ([ca00fd4](https://github.com/aws-actions/configure-aws-credentials/commit/ca00fd4d3842ad58c3c21ebfe69defa1f0e7bdc4))
## [4.0.2](https://github.com/aws-actions/configure-aws-credentials/compare/v4.0.1...v4.0.2) (2024-02-09)
* Revert 4.0.1 to remove warning

View File

@@ -105,17 +105,19 @@ See [action.yml](./action.yml) for more detail.
| audience | The JWT audience when using OIDC. Used in non-default AWS partitions, like China regions. | No |
| http-proxy | An HTTP proxy to use for API calls. | No |
| mask-aws-account-id | AWS account IDs are not considered secret. Setting this will hide account IDs from output anyway. | No |
| role-duration-seconds | The assumed role duration in seconds, if assuming a role. Defaults to 1 hour. | No |
| role-duration-seconds | The assumed role duration in seconds, if assuming a role. Defaults to 1 hour (3600 seconds). Acceptable values range from 15 minutes (900 seconds) to 12 hours (43200 seconds). | No |
| role-external-id | The external ID of the role to assume. Only needed if your role requires it. | No |
| role-session-name | Defaults to "GitHubActions", but may be changed if required. | No |
| role-skip-session-tagging | Skips session tagging if set. | No |
| inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No |
| managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No |
| output-credentials | When set, outputs fetched credentials as action step output. Defaults to false. | No |
| output-credentials | When set, outputs fetched credentials as action step output. (Outputs access-key-id, secret-access-key, session-token, and expiration). Defaults to false. | No |
| output-env-credentials | When set, outputs fetched credentials as environment variables (AWS_REGION, AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN). Defaults to true. Set to false if you need to avoid setting/changing env variables. You'd probably want to use output-credentials if you disable this. (NOTE: Setting to false will prevent the aws-account-id from being exported as a step output). | No |
| unset-current-credentials | When set, attempts to unset any existing credentials in your action runner. | No |
| disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | No |
| retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No |
| special-characters-workaround | Uncommonly, some environments cannot tolerate special characters in a secret key. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. | No |
| use-existing-credentials | When set, the action will check if existing credentials are valid and exit if they are. Defaults to false. | No |
#### Credential Lifetime
The default session duration is **1 hour**.
@@ -130,7 +132,13 @@ with the `role-external-id` input
#### Session tagging and name
The default session name is "GitHubActions", and you can modify it by specifying
the desired name in `role-session-name`. The session will be tagged with the
the desired name in `role-session-name`.
_Note: you might find it helpful to set the `role-session-name` to `${{ github.run_id }}`
so as to clarify in audit logs which AWS actions were performed by which workflow
run._
The session will be tagged with the
following tags: (Refer to [GitHub's documentation for `GITHUB_` environment
variable definitions](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-environment-variables#default-environment-variables))
@@ -183,7 +191,7 @@ line like this:
```
Or we can have a nicely formatted JSON as well:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
inline-session-policy: >-
{
@@ -204,13 +212,13 @@ The Amazon Resource Names (ARNs) of the IAM managed policies that you want to
use as managed session policies. The policies must exist in the same account as
the role. You can pass a single managed policy like this:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
managed-session-policies: arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
```
And we can pass multiple managed policies likes this:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
managed-session-policies: |
arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
@@ -285,10 +293,10 @@ You can specify the audience through the `audience` input:
```yaml
- name: Configure AWS Credentials for China region audience
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
audience: sts.amazonaws.com.cn
aws-region: us-east-3
aws-region: cn-northwest-1
role-to-assume: arn:aws-cn:iam::123456789100:role/my-github-actions-role
```
@@ -420,7 +428,7 @@ You can use this action to simply configure the region and account ID in the
environment, and then use the runner's credentials for all AWS API calls made by
your Actions workflow:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
```
@@ -430,7 +438,7 @@ APIs called by your Actions workflow.
Or, you can use this action to assume a role, and then use the role credentials
for all AWS API calls made by your Actions workflow:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: my-github-actions-role
@@ -452,7 +460,7 @@ variable.
Manually configured proxy:
```yaml
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: my-github-actions-role
@@ -479,7 +487,7 @@ should include the AWS CLI by default.
### AssumeRoleWithWebIdentity (recommended)
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
@@ -493,13 +501,13 @@ environment variable and use it to assume the role
### AssumeRole with role previously assumed by action in same workflow
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
role-session-name: MySessionName
- name: Configure other AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::987654321000:role/my-second-role
@@ -514,7 +522,7 @@ role, `arn:aws:iam::987654321000:role/my-second-role`.
### AssumeRole with static IAM credentials in repository secrets
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -533,7 +541,7 @@ name, like `role-to-assume: my-github-actions-role`.
```yaml
- name: Configure AWS Credentials 1
id: creds
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
@@ -542,7 +550,7 @@ name, like `role-to-assume: my-github-actions-role`.
run: |
aws sts get-caller-identity
- name: Configure AWS Credentials 2
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v4.1.0
with:
aws-region: us-east-2
aws-access-key-id: ${{ steps.creds.outputs.aws-access-key-id }}

2
__mocks__/fs.cjs Normal file
View File

@@ -0,0 +1,2 @@
const { fs } = require('memfs')
module.exports = fs

View File

@@ -61,6 +61,10 @@ inputs:
output-credentials:
description: Whether to set credentials as step output
required: false
output-env-credentials:
description: Whether to export credentials as environment variables. If you set this to false, you probably want to use output-credentials.
required: false
default: true
unset-current-credentials:
description: Whether to unset the existing credentials in your runner. May be useful if you run this action multiple times in the same job
required: false
@@ -73,6 +77,8 @@ inputs:
special-characters-workaround:
description: Some environments do not support special characters in AWS_SECRET_ACCESS_KEY. This option will retry fetching credentials until the secret access key does not contain special characters. This option overrides disable-retry and retry-max-attempts. This option is disabled by default
required: false
use-existing-credentials:
description: When enabled, this option will check if there are already valid credentials in the environment. If there are, new credentials will not be fetched. If there are not, the action will run as normal.
outputs:
aws-account-id:
description: The AWS account ID for the provided credentials
@@ -82,3 +88,5 @@ outputs:
description: The AWS secret access key for the provided credentials
aws-session-token:
description: The AWS session token for the provided credentials
aws-expiration:
description: The expiration time for the provided credentials

34
biome.jsonc Normal file
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",
},
},
}

23137
dist/cleanup/index.js generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
import { STSClient } from '@aws-sdk/client-sts';
export interface CredentialsClientProps {
region?: string;
proxyServer?: string;
}
export declare class CredentialsClient {
region?: string;
private _stsClient?;
private readonly requestHandler?;
constructor(props: CredentialsClientProps);
get stsClient(): STSClient;
validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean): Promise<void>;
private loadCredentials;
}

15
dist/cleanup/src/assumeRole.d.ts generated vendored
View File

@@ -1,15 +0,0 @@
import type { CredentialsClient } from './CredentialsClient';
export interface assumeRoleParams {
credentialsClient: CredentialsClient;
roleToAssume: string;
roleDuration: number;
roleSessionName: string;
roleSkipSessionTagging?: boolean;
sourceAccountId?: string;
roleExternalId?: string;
webIdentityTokenFile?: string;
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: any[];
}
export declare function assumeRole(params: assumeRoleParams): Promise<import("@aws-sdk/client-sts").AssumeRoleCommandOutput>;

11
dist/cleanup/src/cleanup/index.d.ts generated vendored
View File

@@ -1,11 +0,0 @@
/**
* When the GitHub Actions job is done, clean up any environment variables that
* may have been set by the configure-aws-credentials steps in the job.
*
* Environment variables are not intended to be shared across different jobs in
* the same GitHub Actions workflow: GitHub Actions documentation states that
* each job runs in a fresh instance. However, doing our own cleanup will
* give us additional assurance that these environment variables are not shared
* with any other jobs.
*/
export declare function cleanup(): void;

16
dist/cleanup/src/helpers.d.ts generated vendored
View File

@@ -1,16 +0,0 @@
import type { Credentials } from '@aws-sdk/client-sts';
import type { CredentialsClient } from './CredentialsClient';
export declare function exportCredentials(creds?: Partial<Credentials>, outputCredentials?: boolean): void;
export declare function unsetCredentials(): void;
export declare function exportRegion(region: string): void;
export declare function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean): Promise<string>;
export declare function sanitizeGitHubVariables(name: string): string;
export declare function defaultSleep(ms: number): Promise<unknown>;
declare let sleep: typeof defaultSleep;
export declare function withsleep(s: typeof sleep): void;
export declare function reset(): void;
export declare function verifyKeys(creds: Partial<Credentials> | undefined): boolean;
export declare function retryAndBackoff<T>(fn: () => Promise<T>, isRetryable: boolean, maxRetries?: number, retries?: number, base?: number): Promise<T>;
export declare function errorMessage(error: unknown): string;
export declare function isDefined<T>(i: T | undefined | null): i is T;
export {};

1
dist/cleanup/src/index.d.ts generated vendored
View File

@@ -1 +0,0 @@
export declare function run(): Promise<void>;

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1 +0,0 @@
export {};

1
dist/cleanup/test/index.test.d.ts generated vendored
View File

@@ -1 +0,0 @@
export {};

23967
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

11341
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{
"name": "configure-aws-credentials",
"description": "A GitHub Action to configure AWS credentials",
"version": "4.2.1",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"lint": "eslint .",
"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",
"test": "npm run lint && jest --verbose"
"build": "tsc",
"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 && cpy dist/THIRD-PARTY . && del-cli dist/THIRD-PARTY",
"test": "npm run lint && vitest run && npm run build"
},
"author": {
"name": "Amazon.com, Inc. or its affiliates",
@@ -13,36 +14,26 @@
"organization": true
},
"devDependencies": {
"@aws-sdk/credential-provider-env": "^3.515.0",
"@smithy/property-provider": "^3.1.3",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^22",
"@typescript-eslint/eslint-plugin": "<=5.62.0",
"@typescript-eslint/parser": "<=5.62.0",
"@vercel/ncc": "^0.38.1",
"aws-sdk-client-mock": "^4.0.1",
"copyfiles": "^2.4.1",
"del-cli": "^5.1.0",
"eslint": "^8",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"jest-junit": "^16",
"@aws-sdk/credential-provider-env": "^3.844.0",
"@biomejs/biome": "1.9.4",
"@smithy/property-provider": "^4.0.3",
"@types/node": "^24.0.3",
"@vercel/ncc": "^0.38.3",
"@vitest/coverage-v8": "^3.1.2",
"aws-sdk-client-mock": "^4.1.0",
"cpy-cli": "^5.0.0",
"del-cli": "^6.0.0",
"json-schema": "^0.4.0",
"prettier": "^3.3.3",
"standard-version": "^9",
"ts-jest": "^29.2.4",
"typescript": "^5.5.4"
"memfs": "^4.17.2",
"standard-version": "^9.5.0",
"typescript": "^5.8.3",
"vitest": "^3.1.2"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@aws-sdk/client-sts": "^3",
"@smithy/node-http-handler": "^3.1.4",
"https-proxy-agent": "^5.0.0"
"@actions/core": "^1.11.1",
"@aws-sdk/client-sts": "^3.840.0",
"@smithy/node-http-handler": "^4.1.0",
"https-proxy-agent": "^5.0.1"
},
"keywords": [
"aws",
@@ -55,7 +46,6 @@
"main": "build/index.js",
"license": "MIT",
"homepage": "https://github.com/aws-actions/configure-aws-credentials",
"version": "0.0.0",
"bugs": {
"url": "https://github.com/aws-actions/configure-aws-credentials/issues"
},

View File

@@ -0,0 +1,13 @@
{
"packages": {
".": {
"changelog-path": "CHANGELOG.md",
"release-type": "node",
"bump-minor-pre-major": false,
"bump-patch-for-minor-pre-major": false,
"draft": false,
"prerelease": false
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}

View File

@@ -1,5 +1,6 @@
import { info } from '@actions/core';
import { STSClient } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { errorMessage } from './helpers';
@@ -40,7 +41,7 @@ export class CredentialsClient {
}
public async validateCredentials(expectedAccessKeyId?: string, roleChaining?: boolean) {
let credentials;
let credentials: AwsCredentialIdentity;
try {
credentials = await this.loadCredentials();
if (!credentials.accessKeyId) {
@@ -55,7 +56,7 @@ export class CredentialsClient {
if (expectedAccessKeyId && expectedAccessKeyId !== actualAccessKeyId) {
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',
);
}
}

View File

@@ -1,6 +1,6 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import * as core from '@actions/core';
import type { AssumeRoleCommandInput, STSClient, Tag } 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({
...params,
WebIdentityToken: webIdentityToken,
})
}),
);
return creds;
} catch (error) {
@@ -27,10 +27,10 @@ async function assumeRoleWithWebIdentityTokenFile(
params: AssumeRoleCommandInput,
client: STSClient,
webIdentityTokenFile: string,
workspace: string
workspace: string,
) {
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)
? webIdentityTokenFile
@@ -46,7 +46,7 @@ async function assumeRoleWithWebIdentityTokenFile(
new AssumeRoleWithWebIdentityCommand({
...params,
WebIdentityToken: webIdentityToken,
})
}),
);
return creds;
} catch (error) {
@@ -75,7 +75,7 @@ export interface assumeRoleParams {
webIdentityTokenFile?: string;
webIdentityToken?: string;
inlineSessionPolicy?: string;
managedSessionPolicies?: any[];
managedSessionPolicies?: { arn: string }[];
}
export async function assumeRole(params: assumeRoleParams) {
@@ -108,8 +108,11 @@ export async function assumeRole(params: assumeRoleParams) {
{ 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']) });
if (process.env.GITHUB_REF) {
tagArray.push({
Key: 'Branch',
Value: sanitizeGitHubVariables(process.env.GITHUB_REF),
});
}
const tags = roleSkipSessionTagging ? undefined : tagArray;
if (!tags) {
@@ -123,7 +126,7 @@ export async function assumeRole(params: assumeRoleParams) {
if (!roleArn.startsWith('arn:aws')) {
assert(
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}`;
}
@@ -139,6 +142,7 @@ export async function assumeRole(params: assumeRoleParams) {
PolicyArns: managedSessionPolicies?.length ? managedSessionPolicies : undefined,
};
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]);
// Instantiate STS client
@@ -147,14 +151,14 @@ export async function assumeRole(params: assumeRoleParams) {
// Assume role using one of three methods
if (!!webIdentityToken) {
return assumeRoleWithOIDC(commonAssumeRoleParams, stsClient, webIdentityToken);
} else if (!!webIdentityTokenFile) {
}
if (!!webIdentityTokenFile) {
return assumeRoleWithWebIdentityTokenFile(
commonAssumeRoleParams,
stsClient,
webIdentityTokenFile,
GITHUB_WORKSPACE
GITHUB_WORKSPACE,
);
} else {
return assumeRoleWithCredentials(commonAssumeRoleParams, stsClient);
}
return assumeRoleWithCredentials(commonAssumeRoleParams, stsClient);
}

View File

@@ -13,18 +13,21 @@ import { errorMessage } from '../helpers';
*/
export function cleanup() {
try {
// The GitHub Actions toolkit does not have an option to completely unset
// environment variables, so we overwrite the current value with an empty
// string. The AWS CLI and AWS SDKs will behave correctly: they treat an
// empty string value as if the environment variable does not exist.
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
core.exportVariable('AWS_REGION', '');
} catch (error) {
core.setFailed(errorMessage(error));
const outputEnvCredentialsInput = core.getInput('output-env-credentials', { required: false }) || 'true';
if (outputEnvCredentialsInput === 'true') {
try {
// The GitHub Actions toolkit does not have an option to completely unset
// environment variables, so we overwrite the current value with an empty
// string. The AWS CLI and AWS SDKs will behave correctly: they treat an
// empty string value as if the environment variable does not exist.
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
core.exportVariable('AWS_REGION', '');
} catch (error) {
core.setFailed(errorMessage(error));
}
}
}
/* c8 ignore start */

View File

@@ -7,25 +7,70 @@ const MAX_TAG_VALUE_LENGTH = 256;
const SANITIZATION_CHARACTER = '_';
const SPECIAL_CHARS_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/;
export function translateEnvVariables() {
const envVars = [
'AWS_REGION',
'ROLE_TO_ASSUME',
'WEB_IDENTITY_TOKEN_FILE',
'ROLE_CHAINING',
'AUDIENCE',
'HTTP_PROXY',
'MASK_AWS_ACCOUNT_ID',
'ROLE_DURATION_SECONDS',
'ROLE_EXTERNAL_ID',
'ROLE_SESSION_NAME',
'ROLE_SKIP_SESSION_TAGGING',
'INLINE_SESSION_POLICY',
'MANAGED_SESSION_POLICIES',
'OUTPUT_CREDENTIALS',
'UNSET_CURRENT_CREDENTIALS',
'DISABLE_RETRY',
'RETRY_MAX_ATTEMPTS',
'SPECIAL_CHARACTERS_WORKAROUND',
'USE_EXISTING_CREDENTIALS',
];
for (const envVar of envVars) {
if (process.env[envVar]) {
const inputKey = `INPUT_${envVar.replace(/_/g, '-')}`;
process.env[inputKey] = process.env[inputKey] || process.env[envVar];
}
}
}
// Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets.
// Setting the credentials as secrets masks them in Github Actions logs
export function exportCredentials(creds?: Partial<Credentials>, outputCredentials?: boolean) {
export function exportCredentials(
creds?: Partial<Credentials>,
outputCredentials?: boolean,
outputEnvCredentials?: boolean,
) {
if (creds?.AccessKeyId) {
core.setSecret(creds.AccessKeyId);
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.setSecret(creds.SecretAccessKey);
core.exportVariable('AWS_SECRET_ACCESS_KEY', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.setSecret(creds.SessionToken);
core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken);
} else if (process.env['AWS_SESSION_TOKEN']) {
// clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', '');
}
if (outputEnvCredentials) {
if (creds?.AccessKeyId) {
core.exportVariable('AWS_ACCESS_KEY_ID', creds.AccessKeyId);
}
if (creds?.SecretAccessKey) {
core.exportVariable('AWS_SECRET_ACCESS_KEY', creds.SecretAccessKey);
}
if (creds?.SessionToken) {
core.exportVariable('AWS_SESSION_TOKEN', creds.SessionToken);
} else if (process.env.AWS_SESSION_TOKEN) {
// clear session token from previous credentials action
core.exportVariable('AWS_SESSION_TOKEN', '');
}
}
if (outputCredentials) {
@@ -38,20 +83,27 @@ export function exportCredentials(creds?: Partial<Credentials>, outputCredential
if (creds?.SessionToken) {
core.setOutput('aws-session-token', creds.SessionToken);
}
if (creds?.Expiration) {
core.setOutput('aws-expiration', creds.Expiration);
}
}
}
export function unsetCredentials() {
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_REGION', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
export function unsetCredentials(outputEnvCredentials?: boolean) {
if (outputEnvCredentials) {
core.exportVariable('AWS_ACCESS_KEY_ID', '');
core.exportVariable('AWS_SECRET_ACCESS_KEY', '');
core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_REGION', '');
core.exportVariable('AWS_DEFAULT_REGION', '');
}
}
export function exportRegion(region: string) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
export function exportRegion(region: string, outputEnvCredentials?: boolean) {
if (outputEnvCredentials) {
core.exportVariable('AWS_DEFAULT_REGION', region);
core.exportVariable('AWS_REGION', region);
}
}
// Obtains account ID from STS Client and sets it as output
@@ -116,7 +168,7 @@ export async function retryAndBackoff<T>(
isRetryable: boolean,
maxRetries = 12,
retries = 0,
base = 50
base = 50,
): Promise<T> {
try {
return await fn();
@@ -125,7 +177,8 @@ export async function retryAndBackoff<T>(
throw err;
}
// 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;
if (retries >= maxRetries) {
throw err;
@@ -143,3 +196,16 @@ export function isDefined<T>(i: T | undefined | null): i is T {
return i !== undefined && i !== null;
}
/* c8 ignore stop */
export async function areCredentialsValid(credentialsClient: CredentialsClient) {
const client = credentialsClient.stsClient;
try {
const identity = await client.send(new GetCallerIdentityCommand({}));
if (identity.Account) {
return true;
}
return false;
} catch (_) {
return false;
}
}

View File

@@ -1,13 +1,15 @@
import * as core from '@actions/core';
import type { AssumeRoleCommandOutput } from '@aws-sdk/client-sts';
import { assumeRole } from './assumeRole';
import { CredentialsClient } from './CredentialsClient';
import { assumeRole } from './assumeRole';
import {
areCredentialsValid,
errorMessage,
retryAndBackoff,
exportRegion,
exportCredentials,
exportAccountId,
exportCredentials,
exportRegion,
retryAndBackoff,
translateEnvVariables,
unsetCredentials,
verifyKeys,
} from './helpers';
@@ -18,30 +20,44 @@ const REGION_REGEX = /^[a-z0-9-]+$/g;
export async function run() {
try {
translateEnvVariables();
// Get inputs
const AccessKeyId = core.getInput('aws-access-key-id', { required: false });
const SecretAccessKey = core.getInput('aws-secret-access-key', { required: false });
const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SecretAccessKey = core.getInput('aws-secret-access-key', {
required: false,
});
const sessionTokenInput = core.getInput('aws-session-token', {
required: false,
});
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: true });
const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false });
const maskAccountIdInput = core.getInput('mask-aws-account-id', { required: false }) || 'false';
const maskAccountId = maskAccountIdInput.toLowerCase() === 'true';
const roleExternalId = core.getInput('role-external-id', { required: false });
const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });
const roleDuration = parseInt(core.getInput('role-duration-seconds', { required: false })) || DEFAULT_ROLE_DURATION;
const roleExternalId = core.getInput('role-external-id', {
required: false,
});
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 roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false }) || 'false';
const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
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 managedSessionPolicies: any[] = [];
const managedSessionPolicies: { arn: string }[] = [];
const roleChainingInput = core.getInput('role-chaining', { required: false }) || 'false';
const roleChaining = roleChainingInput.toLowerCase() === 'true';
const outputCredentialsInput = core.getInput('output-credentials', { required: false }) || 'false';
const outputCredentials = outputCredentialsInput.toLowerCase() === 'true';
const outputEnvCredentialsInput = core.getInput('output-env-credentials', { required: false }) || 'true';
const outputEnvCredentials = outputEnvCredentialsInput.toLowerCase() === 'true';
const unsetCurrentCredentialsInput = core.getInput('unset-current-credentials', { required: false }) || 'false';
const unsetCurrentCredentials = unsetCurrentCredentialsInput.toLowerCase() === 'true';
const disableRetryInput = core.getInput('disable-retry', { required: false }) || 'false';
@@ -49,7 +65,9 @@ export async function run() {
const specialCharacterWorkaroundInput =
core.getInput('special-characters-workaround', { required: false }) || 'false';
const specialCharacterWorkaround = specialCharacterWorkaroundInput.toLowerCase() === 'true';
let maxRetries = parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
const useExistingCredentialsInput = core.getInput('use-existing-credentials', { required: false }) || 'false';
const useExistingCredentials = useExistingCredentialsInput.toLowerCase() === 'true';
let maxRetries = Number.parseInt(core.getInput('retry-max-attempts', { required: false })) || 12;
switch (true) {
case specialCharacterWorkaround:
// 😳
@@ -74,17 +92,17 @@ export async function run() {
!!roleToAssume &&
!webIdentityTokenFile &&
!AccessKeyId &&
!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!roleChaining
) {
core.info(
'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 (
!!roleToAssume &&
!!process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'] &&
!!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN &&
!AccessKeyId &&
!webIdentityTokenFile &&
!roleChaining
@@ -92,19 +110,29 @@ export async function run() {
};
if (unsetCurrentCredentials) {
unsetCredentials();
unsetCredentials(outputEnvCredentials);
}
if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`);
}
exportRegion(region);
exportRegion(region, outputEnvCredentials);
// Instantiate credentials client
const credentialsClient = new CredentialsClient({ region, proxyServer });
let sourceAccountId: string;
let webIdentityToken: string;
//if the user wants to attempt to use existing credentials, check if we have some already
if (useExistingCredentials) {
const validCredentials = await areCredentialsValid(credentialsClient);
if (validCredentials) {
core.notice('Pre-existing credentials are valid. No need to generate new ones.');
return;
}
core.notice('No valid credentials exist. Running as normal.');
}
// If OIDC is being used, generate token
// Else, export credentials provided as input
if (useGitHubOIDCProvider()) {
@@ -114,7 +142,7 @@ export async function run() {
return core.getIDToken(audience);
},
!disableRetry,
maxRetries
maxRetries,
);
} catch (error) {
throw new Error(`getIDToken call failed: ${errorMessage(error)}`);
@@ -127,7 +155,7 @@ export async function run() {
// Plus, in the assume role case, if the AssumeRole call fails, we want
// the source credentials to already be masked as secrets
// in any error messages.
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken });
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials);
} else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up
await credentialsClient.validateCredentials();
@@ -146,7 +174,6 @@ export async function run() {
if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput;
do {
// eslint-disable-next-line no-await-in-loop
roleCredentials = await retryAndBackoff(
async () => {
return assumeRole({
@@ -164,27 +191,28 @@ export async function run() {
});
},
!disableRetry,
maxRetries
maxRetries,
);
// eslint-disable-next-line no-unmodified-loop-condition
} while (specialCharacterWorkaround && !verifyKeys(roleCredentials.Credentials));
core.info(`Authenticated as assumedRoleId ${roleCredentials.AssumedRoleUser!.AssumedRoleId!}`);
exportCredentials(roleCredentials.Credentials, outputCredentials);
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 (!process.env['GITHUB_ACTIONS'] || AccessKeyId) {
if (!process.env.GITHUB_ACTIONS || AccessKeyId) {
await credentialsClient.validateCredentials(roleCredentials.Credentials?.AccessKeyId);
}
await exportAccountId(credentialsClient, maskAccountId);
if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId);
}
} else {
core.info('Proceeding with IAM user credentials');
}
} catch (error) {
core.setFailed(errorMessage(error));
const showStackTrace = process.env['SHOW_STACK_TRACE'];
const showStackTrace = process.env.SHOW_STACK_TRACE;
if (showStackTrace === 'true') {
throw error;

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 * 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 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;
const mockedSTSClient = mockClient(STSClient);
describe('Configure AWS Credentials cleanup', {}, () => {
beforeEach(() => {
jest.resetModules();
jest.spyOn(core, 'exportVariable').mockImplementation();
jest.spyOn(core, 'setSecret').mockImplementation();
jest.spyOn(core, 'setOutput').mockImplementation();
jest.spyOn(core, 'setFailed').mockImplementation();
process.env = { ...OLD_ENV, ...ACTION_ENVIRONMENT_VARIABLES };
// Reset mock state
vi.restoreAllMocks();
mockedSTSClient.reset();
// Mock GitHub Actions core functions
vi.spyOn(core, 'exportVariable').mockImplementation((_n, _v) => {});
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',
};
});
afterEach(() => {
process.env = OLD_ENV;
});
test('replaces AWS credential and region env vars with empty strings', () => {
it('replaces AWS credential and region environment variables with empty strings', {}, () => {
cleanup();
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.exportVariable).toHaveBeenCalledTimes(5);
@@ -39,14 +38,16 @@ describe('Configure AWS Credentials', () => {
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', '');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', '');
});
test('error is caught and fails the action', () => {
jest.spyOn(core, 'exportVariable').mockImplementation(() => {
throw new Error();
it('handles errors', {}, () => {
vi.spyOn(core, 'exportVariable').mockImplementationOnce(() => {
throw new Error('Test error');
});
cleanup();
expect(core.setFailed).toHaveBeenCalled();
});
it(`doesn't export credentials as empty env variables if asked not to`, {}, () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.NO_ENV_CREDS_INPUTS));
cleanup();
expect(core.exportVariable).toHaveBeenCalledTimes(0);
})
});

View File

@@ -1,26 +1,56 @@
import { describe, it, expect, vi } from 'vitest';
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(() => {
jest.resetModules();
jest.clearAllMocks();
vi.restoreAllMocks();
});
test('removes brackets from GitHub Actor', () => {
expect(helpers.sanitizeGitHubVariables('foo[bot]')).toEqual('foo_bot_');
it('removes brackets from GitHub Actor', {}, () => {
const actor = 'actor[bot]';
expect(helpers.sanitizeGitHubVariables(actor)).toBe('actor_bot_');
});
test('removes special characters from worflow names', () => {
it('can sleep', {}, () => {
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@__________+___:____');
});
test('can sleep', () => {
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');
it("doesn't retry non-retryable errors", {}, async () => {
const fn = vi.fn().mockRejectedValue('i am not retryable');
await expect(helpers.retryAndBackoff(fn, false)).rejects.toMatch('i am not retryable');
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', Expiration: new Date(8640000000000000) }, true, true);
expect(core.setOutput).toHaveBeenCalledTimes(4);
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;
});
it(`won't output credentials to env if told not to`, {}, () => {
vi.spyOn(core, 'setOutput').mockImplementation(() => {});
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test', SessionToken: 'test', Expiration: new Date(8640000000000000) }, true, false);
helpers.unsetCredentials(false);
helpers.exportRegion('fake-test-region', false);
expect(core.setOutput).toHaveBeenCalledTimes(4);
expect(core.setSecret).toHaveBeenCalledTimes(3);
expect(core.exportVariable).toHaveBeenCalledTimes(0);
});
});

File diff suppressed because it is too large Load Diff

114
test/mockinputs.test.ts Normal file
View File

@@ -0,0 +1,114 @@
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',
},
USE_EXISTING_CREDENTIALS_INPUTS: {
'aws-region': 'fake-region-1',
'use-existing-credentials': 'true',
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
},
NO_ENV_CREDS_INPUTS: {
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'output-env-credentials': 'false'
},
STEP_BUT_NO_ENV_INPUTS: {
'role-to-assume': 'arn:aws:iam::111111111111:role/MY-ROLE',
'aws-region': 'fake-region-1',
'output-env-credentials': 'false',
'output-credentials': 'true',
}
};
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;

View File

@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": [
"test/**/*.ts"
],
"compilerOptions": {
"rootDir": "src"
},
}

View File

@@ -7,14 +7,13 @@
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"module": "CommonJS",
"resolveJsonModule": true,
"outDir": "build",
"declaration": true,
"declaration": false,
"newLine": "lf",
"noEmitOnError": true,
"sourceMap": true,
@@ -23,12 +22,14 @@
"lib": [ "ES2020" ],
"target": "ES2020",
"noErrorTruncation": true,
"esModuleInterop": true
"esModuleInterop": true,
"rootDir": "src",
},
"include": [
"src/**/*.ts",
"src/**/*.ts"
],
"exclude": [
"test/**/*.ts"
],
"exclude": [],
}

11
tsconfig.test.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"include": [
"test/**/*.ts",
"src/**/*.ts"
],
"exclude": [],
"compilerOptions": {
"rootDir": "./"
},
}

10
vitest.config.mts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
passWithNoTests: true,
include: ['test/**/*.test.ts'],
coverage: { enabled: true },
typecheck: { tsconfig: './tsconfig.test.json' },
},
});