1
0
Fork 0
mirror of synced 2026-06-05 15:04:04 +00:00

feat: Support usage of AWS Profiles (#1696)

* Support usage of AWS Profiles

* squash merge main updates w feature branch
Squashed commit of the following:

commit ef2df4679f908ff30d5a711258ace2fa906c4bf3
Author: Michael Lehmann <lehmanmj@amazon.com>
Date:   Tue Mar 17 11:24:04 2026 -0700

    dist update

commit db3779a0e9
Author: Jan Feddern <jf@novatec-gmbh.de>
Date:   Sun Dec 21 11:28:36 2025 +0100

    Support usage of AWS Profiles

* chore: Update dist

* consistent outputEnvCredentials

* take out tests temporarily

* chore: Update dist

* debug changes for static creds

* remove debug and only cleanup profile if it was set

* formatting fixes + remove profile from cleanup test

* feat: Support usage of AWS Profiles

Adds a config option to support writing to profile files instead of
exporting environment variables.

Closes #1594. Closes #1586. Closes #112.

* chore: fix failing test case and windows path

* chore: lint project markdown files

* chore: update scripts in package.json and tsconfig update

* make env vars consistent, readme linting

* debug for profile path env vars

* remove debug

* remove profile backups

* error if we try to overwrite

* add option to overwrite existing profiles

* tests for overwrite option

* default to no env vars

* remove default from action file

* add static credential env var support

* validation fix for static creds multi profile

* debug sleep for static creds validation

* wait syntax

* undo sleep for creds validate

* test coverage, readme/action yml updates, validate creds later on self-hosted runner

* security dependency updates

* chore(deps-dev): bump @biomejs/biome from 2.4.8 to 2.4.10 (#1709)

Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.8 to 2.4.10.
- [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.10/packages/@biomejs/biome)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

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

Bumps [@aws-sdk/credential-provider-env](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages-internal/credential-provider-env) from 3.972.22 to 3.972.24.
- [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.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: Update dist

* chore(deps): bump @aws-sdk/client-sts from 3.1015.0 to 3.1020.0 (#1710)

Bumps [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) from 3.1015.0 to 3.1020.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.1020.0/clients/client-sts)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: Update dist

* fix: do not write empty profile files

Also cleanup fix, additional test, README typo cleanup

* linting fix

* chore: linting fix

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Jan Feddern <jf@novatec-gmbh.de>
Co-authored-by: Michael Lehmann <lehmanmj@amazon.com>
Co-authored-by: GitHub Actions <github-aws-sdk-osds-automation@amazon.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Tom Keller 2026-04-03 16:27:00 -07:00 committed by GitHub
commit a7f0c828ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 3583 additions and 924 deletions

View file

@ -1,9 +1,11 @@
*Issue #, if available:* _Issue #, if available:_
*Description of changes:* _Description of changes:_
--- ---
* [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws-actions/configure-aws-credentials/blob/main/CONTRIBUTING.md) - [ ] Have you followed the guidelines in our
[Contributing guide?](https://github.com/aws-actions/configure-aws-credentials/blob/main/CONTRIBUTING.md)
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. By submitting this pull request, I confirm that you can use, modify, copy, and
redistribute this contribution, under the terms of your choice.

View file

@ -232,3 +232,50 @@ jobs:
retry-max-attempts: 4 retry-max-attempts: 4
- name: check creds - name: check creds
run: aws sts get-caller-identity run: aws sts get-caller-identity
profile-oidc:
if: ${{ github.event_name == 'workflow_dispatch' || (github.event.pull_request.user.login == 'aws-sdk-osds' && github.repository == 'aws-actions/configure-aws-credentials') }}
permissions:
id-token: write
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
name: OIDC profile writing test
steps:
- name: checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: make profile 1
uses: ./
with:
aws-region: us-east-1
role-to-assume: ${{ secrets.INTEG_PROFILE_ROLE_1 }}
aws-profile: TestProfile1
- name: make profile 2
uses: ./
with:
aws-region: us-east-2
role-to-assume: ${{ secrets.INTEG_PROFILE_ROLE_2 }}
aws-profile: TestProfile2
- name: check profile1
run: aws sts get-caller-identity --profile TestProfile1
- name: check profile2
run: aws sts get-caller-identity --profile TestProfile2
- name: Get Caller Identity
id: no-creds-sts-step
continue-on-error: true
run: |
aws sts get-caller-identity
- name: fail if we got caller id with env creds
if: steps.no-creds-sts-step.outcome == 'success'
run: exit 1

17
.markdownlint.yaml Normal file
View file

@ -0,0 +1,17 @@
default: true
MD007:
indent: 4
heading-style:
style: atx
ul-style:
style: dash
line-length:
stern: true
tables: false
code_blocks: false
no-inline-html:
allowed_elements:
- details
- summary
code-block-style:
style: fenced

View file

@ -1,4 +1,8 @@
## Code of Conduct # Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact This project has adopted the
opensource-codeofconduct@amazon.com with any additional questions or comments. [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For
more information see the
[Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
<opensource-codeofconduct@amazon.com> with any additional questions or
comments.

View file

@ -1,57 +1,90 @@
# Contributing Guidelines # Contributing Guidelines
Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional Thank you for your interest in contributing to our project. Whether it's a bug
documentation, we greatly value feedback and contributions from our community. report, new feature, correction, or additional documentation, we greatly value
feedback and contributions from our community.
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
information to effectively respond to your bug report or contribution.
Please read through this document before submitting any issues or pull requests
to ensure we have all the necessary information to effectively respond to your
bug report or contribution.
## Reporting Bugs/Feature Requests ## Reporting Bugs/Feature Requests
We welcome you to use the GitHub issue tracker to report bugs or suggest features. We welcome you to use the GitHub issue tracker to report bugs or suggest
features.
When filing an issue, please check [existing open](https://github.com/aws-actions/configure-aws-credentials/issues), or [recently closed](https://github.com/aws-actions/configure-aws-credentials/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already When filing an issue, please check [existing open][open-issues],
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: or [recently closed][closed-issues], issues to make sure somebody
else hasn't already reported the issue.
* A reproducible test case or series of steps [open-issues]: https://github.com/aws-actions/configure-aws-credentials/issues
* The version of our code being used [closed-issues]: https://github.com/aws-actions/configure-aws-credentials/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20
* Any modifications you've made relevant to the bug
* Anything unusual about your environment or deployment
Please try to include as much information as you can. Details like
these are incredibly useful:
- A reproducible test case or series of steps
- The version of our code being used
- Any modifications you've made relevant to the bug
- Anything unusual about your environment or deployment
## Contributing via Pull Requests ## Contributing via Pull Requests
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
1. You are working against the latest source on the *main* branch. Contributions via pull requests are much appreciated. Before sending us a pull
2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. request, please ensure that:
3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
1. You are working against the latest source on the _main_ branch.
2. You check existing open, and recently merged, pull requests to make sure
someone else hasn't addressed the problem already.
3. You open an issue to discuss any significant work - we would hate for your
time to be wasted.
To send us a pull request, please: To send us a pull request, please:
1. Fork the repository. 1. Fork the repository.
2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 2. Modify the source; please focus on the specific change you are contributing.
If you also reformat all the code, it will be hard for us to focus on your
change.
3. Ensure local tests pass. 3. Ensure local tests pass.
4. Commit to your fork using clear commit messages. 4. Commit to your fork using clear commit messages.
5. Send us a pull request, answering any default questions in the pull request interface. 5. Send us a pull request, answering any default questions in the pull request
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. interface.
6. Pay attention to any automated CI failures reported in the pull request, and
stay involved in the conversation.
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and GitHub provides additional document on
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request][create-pr].
[create-pr]:
https://help.github.com/articles/creating-a-pull-request/
## Finding contributions to work on ## Finding contributions to work on
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-actions/configure-aws-credentials/labels/help%20wanted) issues is a great place to start.
Looking at the existing issues is a great way to find something to contribute
on. As our projects, by default, use the default GitHub issue labels
(enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any
['help wanted'](https://github.com/aws-actions/configure-aws-credentials/labels/help%20wanted)
issues is a great place to start.
## Code of Conduct ## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.
This project has adopted the
[Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For
more information see the
[Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
<opensource-codeofconduct@amazon.com> with any additional questions or
comments.
## Security issue notifications ## Security issue notifications
If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
If you discover a potential security issue in this project we ask that you
notify AWS/Amazon Security via our
[vulnerability reporting page][vuln-report].
[vuln-report]:
http://aws.amazon.com/security/vulnerability-reporting/
Please do **not** create a public github issue.
## Automated Tools ## Automated Tools
@ -62,7 +95,7 @@ the following rules:
- All issue and pull request submissions to this repository that are sourced by - All issue and pull request submissions to this repository that are sourced by
AI must first be reviewed by a human before submitting to the repository. AI must first be reviewed by a human before submitting to the repository.
Items reviewed in this way must include a statement like "generated by AI Items reviewed in this way must include a statement like "generated by AI
tools, and reviewed by <person>" tools, and reviewed by [person]"
- Please ensure that your submissions are actually improvements. While we are - Please ensure that your submissions are actually improvements. While we are
grateful for any proposed fixes, even if they are very small, behavior that grateful for any proposed fixes, even if they are very small, behavior that
looks like creating noisy PRs or artificially inflating submission counts looks like creating noisy PRs or artificially inflating submission counts
@ -70,10 +103,15 @@ the following rules:
- We may close issues or pull requests, or limit your ability to interact with - We may close issues or pull requests, or limit your ability to interact with
this repository, for behavior that in our estimation violates these rules or this repository, for behavior that in our estimation violates these rules or
any of the other rules in this repository's any of the other rules in this repository's
[Code of Conduct][CODE_OF_CONDUCT.md]. [Code of Conduct](CODE_OF_CONDUCT.md).
## Licensing ## Licensing
See the [LICENSE](https://github.com/aws-actions/configure-aws-credentials/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. See the
[LICENSE](https://github.com/aws-actions/configure-aws-credentials/blob/main/LICENSE)
file for our project's licensing. We will ask you to confirm the licensing of
your contribution.
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. We may ask you to sign a
[Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement)
for larger changes.

647
README.md
View file

@ -1,15 +1,17 @@
Configure AWS Credentials # Configure AWS Credentials
=========================
Authenticate to AWS in GitHub Actions! Works especially well with [AWS Secrets Authenticate to AWS in GitHub Actions! Works especially well with
Manager](https://github.com/aws-actions/aws-secretsmanager-get-secrets). [AWS Secrets Manager][secretsmanager].
[secretsmanager]:
https://github.com/aws-actions/aws-secretsmanager-get-secrets
## Quick Start (OIDC, recommended)
Quick Start (OIDC, recommended)
-------------------------------
1. Create an IAM Identity Provider in your AWS account for GitHub OIDC. (See 1. Create an IAM Identity Provider in your AWS account for GitHub OIDC. (See
[OIDC configuration](#oidc-configuration) below for details.) [OIDC configuration](#oidc-configuration-details) below for details.)
2. Create an IAM Role in your AWS account with a trust policy that allows GitHub 2. Create an IAM Role in your AWS account with a trust policy that allows
Actions to assume it: GitHub Actions to assume it. (Expand the sections below) <details>
<details>
<summary>GitHub OIDC Trust Policy</summary> <summary>GitHub OIDC Trust Policy</summary>
```json ```json
@ -32,11 +34,12 @@ Actions to assume it:
] ]
} }
``` ```
</details> </details>
3. Attach permissions to the IAM Role that allow it to access the AWS resources 3. Attach permissions to the IAM Role that allow it to access the AWS resources
you need. you need.
4. Add the following to your GitHub Actions workflow: 4. Add the following to your GitHub Actions workflow: <details>
<details>
<summary>Example Workflow</summary> <summary>Example Workflow</summary>
```yaml ```yaml
@ -48,7 +51,7 @@ you need.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@main # Or a specific version uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
role-to-assume: <Role ARN you created in step 2> role-to-assume: <Role ARN you created in step 2>
aws-region: <AWS Region you want to use> aws-region: <AWS Region you want to use>
@ -57,30 +60,38 @@ you need.
# Your commands that require AWS credentials # Your commands that require AWS credentials
aws sts get-caller-identity aws sts get-caller-identity
``` ```
</details>
That's it! Your GitHub Actions workflow can now access AWS resources using the
IAM Role you created. Other authentication scenarios are also supported (see
below).
Security Recommendations </details>
------------------------
* Use temporary credentials when possible. OIDC is recommended because it That's it! Your GitHub Actions workflow can now access AWS resources using
the IAM Role you created. Other authentication scenarios are also supported
(see below).
## Security Recommendations
- Use temporary credentials when possible. OIDC is recommended because it
provides temporary credentials and it's easy to set up. provides temporary credentials and it's easy to set up.
* Do not store credentials in your repository's code. Consider using - Do not store credentials in your repository's code. Consider using
[git-secrets](https://github.com/awslabs/git-secrets) to prevent committing [git-secrets](https://github.com/awslabs/git-secrets) to prevent committing
secrets to your repository. secrets to your repository.
* [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) - [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege)
to your workflows. Grant only those permissions that are necessary for the to your workflows. Grant only those permissions that are necessary for the
workflow to run. workflow to run.
* [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) - [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log)
of the credentials used in workflows. of the credentials used in workflows.
* Periodically rotate any long-lived credentials that you use. - Periodically rotate any long-lived credentials that you use.
* Store sensitive information in a secure way, such as using - Store sensitive information in a secure way, such as using
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or
[GitHub Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets). [GitHub Secrets][gh-secrets].
- Be especially careful about running Actions in non-ephemeral environments, or
[triggering workflows on `pull_request_target`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target)
events.
[gh-secrets]:
https://docs.github.com/en/actions/security-guides/encrypted-secrets
## Non-OIDC Authentication Options
Other Authentication Scenarios
------------------------------
This action supports five different authentication methods that are configured This action supports five different authentication methods that are configured
by specifying different inputs. by specifying different inputs.
@ -94,85 +105,178 @@ by specifying different inputs.
5. Use credentials stored in the Action environment to fetch temporary 5. Use credentials stored in the Action environment to fetch temporary
credentials via STS AssumeRole. credentials via STS AssumeRole.
Because we use the AWS JavaScript SDK, we always will use the [credential Because we use the AWS JavaScript SDK, we always will use the
resolution flow for Node.js](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html). [credential resolution flow for Node.js][cred-resolution].
[cred-resolution]:
https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
Depending on your inputs, the action might override parts of this flow. Depending on your inputs, the action might override parts of this flow.
<details> <details>
<summary>Inputs and their effects on the credential resolution flow</summary> <summary>Inputs and their effects on the credential resolution flow</summary>
| **Identity Used** | `aws-access-key-id` | `role-to-assume` | `web-identity-token-file` | `role-chaining` | | **Identity Used** | `aws-access-key-id` | `role-to-assume` | `web-identity-token-file` | `role-chaining` |
| ---------------------------------------- | ------------------- | ---------------- | ------------------------- | --------------- | | --------------------------------------- | ------------------- | ---------------- | ------------------------- | --------------- |
| [✅ Recommended] GitHub OIDC | | ✔ | | | | [✅ Recommended] GitHub OIDC | | ✔ | | |
| IAM User (no AssumeRole) | ✔ | | | | | IAM User (no AssumeRole) | ✔ | | | |
| AssumeRole using static IAM credentials | ✔ | ✔ | | | | AssumeRole using static IAM credentials | ✔ | ✔ | | |
| AssumeWithWebIdentity use a token file | | ✔ | ✔ | | | AssumeWithWebIdentity use a token file | | ✔ | ✔ | |
| AssumeRole using existing credentials | | ✔ | | ✔ | | AssumeRole using existing credentials | | ✔ | | ✔ |
_Note: `role-chaining` is not always necessary to use existing credentials. If
you're getting a "Credentials loaded by the SDK do not match" error, try
enabling this option._
*Note: `role-chaining` is not always necessary to use existing credentials.
If you're getting a "Credentials loaded by the SDK do not match" error,
try enabling this option.*
</details> </details>
Additionally, **`aws-region`** is always required. Additionally, **`aws-region`** is always required.
*Note: If you use GitHub Enterprise Server, you must use the you may need to _Note: If you use GitHub Enterprise Server, you may need to adjust examples
adjust examples here to match your environment.* here to match your environment._
## Additional Options
Additional Options
------------------
### Options ### Options
See [action.yml](./action.yml) for more detail.
The options list can be expanded below. See [action.yml](./action.yml) for more
detail.
<details> <details>
<summary>Options list and descriptions</summary> <summary>Options list and descriptions</summary>
| Option | Description | Required | | Option | Description | Required |
|---------------------------|---------------------------------------------------------------------------------------------------|----------| | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| aws-region | Which AWS region to use | Yes | | aws-region | Which AWS region to use | Yes |
| role-to-assume | Role for which to fetch credentials. Only required for some authentication types. | No | | aws-profile | Name of the AWS profile to configure. When provided, credentials are written to `~/.aws/credentials` and `~/.aws/config` files. This enables configuring multiple profiles in a single workflow. Name cannot contain whitespace, square brackets, or slashes. When set, credentials will not be exported as environment variables unless `output-env-credentials` is manually set to true. | No |
| aws-access-key-id | AWS access key to use. Only required for some authentication types. | No | | overwrite-aws-profile | Overwrite the given AWS profile if it already exists. When set to false or not set, an error will be thrown if the profile already exists. | No |
| aws-secret-access-key | AWS secret key to use. Only required for some authentication types. | No | | role-to-assume | Role for which to fetch credentials. Only required for some authentication types. | No |
| aws-session-token | AWS session token to use. Used in uncommon authentication scenarios. | No | | aws-access-key-id | AWS access key to use. Only required for some authentication types. | No |
| role-chaining | Use existing credentials from the environment to assume a new role. | No | | aws-secret-access-key | AWS secret key to use. Only required for some authentication types. | No |
| audience | The JWT audience when using OIDC. Used in non-default AWS partitions, like China regions. | No | | aws-session-token | AWS session token to use. Used in uncommon authentication scenarios. | No |
| http-proxy | An HTTP proxy to use for API calls. | No | | role-chaining | Use existing credentials from the environment to assume a new role. | No |
| mask-aws-account-id | AWS account IDs are not considered secret. Setting this will hide account IDs from output anyway. | No | | audience | The JWT audience when using OIDC. Used in non-default AWS partitions, like China regions. | 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 | | http-proxy | An HTTP proxy to use for API calls. | No |
| role-external-id | The external ID of the role to assume. Only needed if your role requires it. | No | | mask-aws-account-id | AWS account IDs are not considered secret. Setting this will hide account IDs from output anyway. | No |
| role-session-name | Defaults to "GitHubActions", but may be changed if required. | 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-skip-session-tagging | Skips session tagging if set. | No | | role-external-id | The external ID of the role to assume. Only needed if your role requires it. | No |
| transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No | | role-session-name | Defaults to "GitHubActions", but may be changed if required. | No |
| inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | No | | role-skip-session-tagging | Skips session tagging if set. | No |
| managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No | | transitive-tag-keys | Define a list of transitive tag keys to pass when assuming a role. | No |
| output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No | | inline-session-policy | You may further restrict the assumed role policy by defining an inline policy here. | 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 | | managed-session-policies | You may further restrict the assumed role policy by specifying a managed policy here. | No |
| unset-current-credentials | When set, attempts to unset any existing credentials in your action runner. | No | | output-credentials | When set, outputs fetched credentials as action step output. (Outputs aws-access-key-id, aws-secret-access-key, aws-session-token, aws-account-id, authenticated-arn, and aws-expiration). Defaults to false. | No |
| disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | 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, and AWS_PROFILE (if profile option is used)). Defaults to true when `aws-profile` is not set, and false when `aws-profile` is set. Set to false to avoid setting env variables. (NOTE: Setting to false will prevent aws-account-id from being exported as a step output). | No |
| retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No | | unset-current-credentials | When set, attempts to unset any existing credentials in your action runner. | 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 | | disable-retry | Disabled retry/backoff logic for assume role calls. By default, retries are enabled. | No |
| use-existing-credentials | When set, the action will check if existing credentials are valid and exit if they are. Defaults to false. | No | | retry-max-attempts | Limits the number of retry attempts before giving up. Defaults to 12. | No |
| allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | 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 |
| force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No | | use-existing-credentials | When set, the action will check if existing credentials are valid and exit if they are. Defaults to false. | No |
| action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No | | allowed-account-ids | A comma-delimited list of expected AWS account IDs. The action will fail if we receive credentials for the wrong account. | No |
| force-skip-oidc | When set, the action will skip using GitHub OIDC provider even if the id-token permission is set. | No |
| action-timeout-s | Global timeout for the action in seconds. If set to a value greater than 0, the action will fail if it takes longer than this time to complete. | No |
</details> </details>
#### Adjust the retry mechanism #### Adjust the retry mechanism
You can configure retry settings for if the STS call fails. By default, we
retry with exponential backoff `12` times. You can disable this behavior You can configure retry settings for if the STS call fails. By default, we retry
altogether by setting the `disable-retry` input to `true`, or you can configure with exponential backoff `12` times. You can disable this behavior altogether by
the number of times it retries with the `retry-max-attempts` input. setting the `disable-retry` input to `true`, or you can configure the number of
times it retries with the `retry-max-attempts` input.
#### Mask account ID #### Mask account ID
Your account ID is not masked by default in workflow logs. You can set the Your account ID is not masked by default in workflow logs. You can set the
`mask-aws-account-id` input to `true` to mask your account ID in workflow logs `mask-aws-account-id` input to `true` to mask your account ID in workflow logs
if desired. if desired.
#### Unset current credentials #### Unset current credentials
Sometimes, existing credentials in your runner can get in the way of the Sometimes, existing credentials in your runner can get in the way of the
intended outcome. You can set the `unset-current-credentials` input to `true` to intended outcome. You can set the `unset-current-credentials` input to `true` to
work around this issue. work around this issue.
#### Skip the cleanup step #### Configure named AWS profiles
By default, this action exports credentials as environment variables
(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.). However, you can use the
`aws-profile` input to configure named AWS profiles. When `aws-profile` is
provided, credentials are written to `~/.aws/credentials` and `~/.aws/config`
files (which are created if they don't already exist). The default locations of
these files will be overridden if the `AWS_SHARED_CREDENTIALS_FILE` and
`AWS_CONFIG_FILE` environment variables are present.
Profile names may not contain whitespace, square brackets, or forward or
backslashes.
Writing to a profile will prevent credentials being written to the environment
by default. Use `output-env-credentials: true` if you would like the
credentials to also be exported as environment variables.
By default, the action will not overwrite existing profiles. If you would like
to overwrite a profile, set the `overwrite-aws-profile` input to `true`.
_Note: When writing profiles, the action will preserve existing profile sections
in the credentials and config files. However, comments in these files will not
be preserved._
_Caution: Writing to the AWS configuration file means that credentials will
persist in the execution environment even after the action cleanup step. Use
extreme care to ensure that this is safe in your environment and you do not leak
valid credentials unintentionally. Writing to configuration files is intended
for unusual authentication scenarios._
For using profiles with static IAM User Credentials or when using one
role to assume another, role chaining is needed:
<details>
If using static credentials, it's necessary to set `role-chaining: true` and
specify the profile name as an environment variable in the job step:
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::123456789100:role/my-role
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-profile: MyProfile1
role-chaining: true
env:
AWS_PROFILE: MyProfile1
```
If you are using one role to assume another while using profiles, the
subsequent steps must set `role-chaining: true` and specify the prior profile's
name as step environment variables:
```yaml
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::123456789100:role/my-first-role
aws-profile: firstRoleInChain
- name: assume second role
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::987654321000:role/my-second-role
role-chaining: true
aws-profile: secondRoleInChain
env:
AWS_PROFILE: firstRoleInChain
```
</details>
See the [Examples](#examples) section for more usage examples.
#### Skip the cleanup
By default, this action runs a post-job cleanup step that removes credentials By default, this action runs a post-job cleanup step that removes credentials
from the environment. To skip this step, set the `AWS_SKIP_CLEANUP_STEP` from the environment. To skip this step, set the `AWS_SKIP_CLEANUP_STEP`
environment variable to `true`: environment variable to `true`:
@ -191,8 +295,9 @@ this action will always consider the `HTTP_PROXY` environment variable.
<summary>Proxy configuration</summary> <summary>Proxy configuration</summary>
Manually configured proxy: Manually configured proxy:
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
aws-region: us-east-2 aws-region: us-east-2
role-to-assume: my-github-actions-role role-to-assume: my-github-actions-role
@ -200,16 +305,22 @@ with:
``` ```
Proxy configured in the environment variable: Proxy configured in the environment variable:
```bash ```bash
# Your environment configuration # Your environment configuration
HTTP_PROXY="http://companydomain.com:3128" HTTP_PROXY="http://companydomain.com:3128"
``` ```
</details> </details>
#### Special characters in AWS_SECRET_ACCESS_KEY #### Special characters in AWS_SECRET_ACCESS_KEY
Some edge cases are unable to properly parse an `AWS_SECRET_ACCESS_KEY` if it Some edge cases are unable to properly parse an `AWS_SECRET_ACCESS_KEY` if it
contains special characters. For more information, please see the contains special characters. For more information, please see the
[AWS CLI documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-troubleshooting.html#tshoot-signature-does-not-match). [AWS CLI documentation][aws-cli-troubleshooting].
[aws-cli-troubleshooting]:
https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-troubleshooting.html#tshoot-signature-does-not-match
If you set the `special-characters-workaround` option, this action will If you set the `special-characters-workaround` option, this action will
continually retry fetching credentials until we get one that does not have continually retry fetching credentials until we get one that does not have
special characters. This option overrides the `disable-retry` and special characters. This option overrides the `disable-retry` and
@ -217,18 +328,21 @@ special characters. This option overrides the `disable-retry` and
unless required, because retrying APIs infinitely until they succeed is not best unless required, because retrying APIs infinitely until they succeed is not best
practice. practice.
Session Naming and Policies ## Session Naming and Policies
---------------------------
The default session name is "GitHubActions", and you can modify it by specifying The default session name is "GitHubActions", and you can modify it by specifying
the desired name in `role-session-name`. the desired name in `role-session-name`.
*Note: you might find it helpful to set the `role-session-name` to `${{ github.run_id }}` _Note: you might find it helpful to set the `role-session-name` to
so as to clarify in audit logs which AWS actions were performed by which workflow `${{ github.run_id }}` so as to clarify in audit logs which AWS actions were
run.* performed by which workflow run._
The session will be tagged with the The session will be tagged with the following tags: (Refer to
following tags: (Refer to [GitHub's documentation for `GITHUB_` environment [GitHub's documentation for `GITHUB_` environment variable
variable definitions](https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables)) definitions][gh-env-vars])
[gh-env-vars]:
https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
| Key | Value | | Key | Value |
| ---------- | ----------------- | | ---------- | ----------------- |
@ -244,31 +358,39 @@ _Note: all tag values must conform to
[the tag requirements](https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html). [the tag requirements](https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html).
Particularly, `GITHUB_WORKFLOW` will be truncated if it's too long. If Particularly, `GITHUB_WORKFLOW` will be truncated if it's too long. If
`GITHUB_ACTOR` or `GITHUB_WORKFLOW` contain invalid characters, the characters `GITHUB_ACTOR` or `GITHUB_WORKFLOW` contain invalid characters, the characters
will be replaced with an '*'._ will be replaced with an '\*'._
The action will use session tagging by default unless you are using OIDC. The action will use session tagging by default unless you are using OIDC.
To [forward session tags to subsequent sessions in a role chain](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining), To [forward session tags to subsequent sessions in a role
you can use the `transitive-tag-keys` input to specify the keys of the tags to be passed. chain][session-tag-chaining], you can use
[session-tag-chaining]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html#id_session-tags_role-chaining
the `transitive-tag-keys` input to specify the keys of the tags to be passed.
_Note that all subsequent roles in the chain must have
`role-skip-session-tagging` set to `true`_
_Note that all subsequent roles in the chain must have `role-skip-session-tagging` set to `true`_
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6 uses: aws-actions/configure-aws-credentials@v6
with: with:
transitive-tag-keys: | transitive-tag-keys: |
Repository Repository
Workflow Workflow
Action Action
Actor Actor
``` ```
### Session policies ### Session policies
Session policies are not required, but they allow you to limit the scope of the Session policies are not required, but they allow you to limit the scope of the
fetched credentials without making changes to IAM roles. You can specify inline fetched credentials without making changes to IAM roles. You can specify inline
session policies right in your workflow file, or refer to an existing managed session policies right in your workflow file, or refer to an existing managed
session policy by its ARN. session policy by its ARN.
#### Inline session policies #### Inline session policies
An IAM policy in stringified JSON format that you want to use as an inline An IAM policy in stringified JSON format that you want to use as an inline
session policy. Depending on preferences, the JSON could be written on a single session policy. Depending on preferences, the JSON could be written on a single
line. line.
@ -277,30 +399,34 @@ line.
<summary>Inline session policy examples</summary> <summary>Inline session policy examples</summary>
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
inline-session-policy: '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:List*","Resource":"*"}]}' inline-session-policy: '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:List*","Resource":"*"}]}'
``` ```
Or we can have a nicely formatted JSON as well: Or we can have a nicely formatted JSON as well:
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
inline-session-policy: >- inline-session-policy: >-
{ {
"Version": "2012-10-17", "Version": "2012-10-17",
"Statement": [ "Statement": [
{ {
"Sid":"Stmt1", "Sid":"Stmt1",
"Effect":"Allow", "Effect":"Allow",
"Action":"s3:List*", "Action":"s3:List*",
"Resource":"*" "Resource":"*"
} }
] ]
} }
``` ```
</details> </details>
#### Managed session policies #### Managed session policies
The Amazon Resource Names (ARNs) of the IAM managed policies that you want to 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 use as managed session policies. The policies must exist in the same account as
the role. the role.
@ -309,36 +435,40 @@ the role.
<summary>Managed session policy examples</summary> <summary>Managed session policy examples</summary>
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
managed-session-policies: arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess managed-session-policies: arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
``` ```
And we can pass multiple managed policies likes this: And we can pass multiple managed policies likes this:
```yaml ```yaml
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
managed-session-policies: | managed-session-policies: |
arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
arn:aws:iam::aws:policy/AmazonS3OutpostsReadOnlyAccess arn:aws:iam::aws:policy/AmazonS3OutpostsReadOnlyAccess
``` ```
</details> </details>
OIDC Configuration ## OIDC Configuration Details
-------------------
We recommend using [GitHub's OIDC We recommend using
provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) [GitHub's OIDC provider](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
to get short-lived AWS credentials needed for your actions. When using OIDC, you to get short-lived AWS credentials needed for your actions. When using OIDC, you
configure IAM to accept JWTs from GitHub's OIDC endpoint. This action will configure IAM to accept JWTs from GitHub's OIDC endpoint. This action will then
then create a JWT unique to the workflow run using the OIDC endpoint, and it create a JWT unique to the workflow run using the OIDC endpoint, and it will use
will use the JWT to assume the specified role with short-term credentials. the JWT to assume the specified role with short-term credentials.
To get this to work To get this to work
1. Configure your workflow to use the `id-token: write` permission. 1. Configure your workflow to use the `id-token: write` permission.
2. Configure your audience, if required. 2. Configure your audience, if required.
3. In your AWS account, configure IAM to trust GitHub's OIDC identity provider. 3. In your AWS account, configure IAM to trust GitHub's OIDC identity provider.
4. Configure an IAM role with appropriate claim limits and permission scope. 4. Configure an IAM role with appropriate claim limits and permission scope.
*Note*: Naming your role "GitHubActions" has been reported to not work. See _Note_: Naming your role "GitHubActions" has been reported to not work. See
[#953](https://github.com/aws-actions/configure-aws-credentials/issues/953). [#953](https://github.com/aws-actions/configure-aws-credentials/issues/953).
5. Specify that role's ARN when setting up this action. 5. Specify that role's ARN when setting up this action.
@ -347,24 +477,26 @@ To get this to work
When the JWT is created, an audience needs to be specified. Normally, you would When the JWT is created, an audience needs to be specified. Normally, you would
use `sts.amazonaws.com`, and this action uses this by default if you don't use `sts.amazonaws.com`, and this action uses this by default if you don't
specify one. This will work for most cases. Changing the default audience may specify one. This will work for most cases. Changing the default audience may be
be necessary when using non-default AWS partitions, such as China regions. necessary when using non-default AWS partitions, such as China regions. You can
You can specify the audience through the `audience` input: specify the audience through the `audience` input:
```yaml ```yaml
- name: Configure AWS Credentials for China region audience - name: Configure AWS Credentials for China region audience
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
audience: sts.amazonaws.com.cn audience: sts.amazonaws.com.cn
aws-region: cn-northwest-1 aws-region: cn-northwest-1
role-to-assume: arn:aws-cn:iam::123456789100:role/my-github-actions-role role-to-assume: arn:aws-cn:iam::123456789100:role/my-github-actions-role
``` ```
### Configuring IAM to trust GitHub ### Configuring IAM to trust GitHub
To use GitHub's OIDC provider, you must first set up federation in your AWS To use GitHub's OIDC provider, you must first set up federation in your AWS
account. This involves creating an IAM Identity Provider that trusts GitHub's account. This involves creating an IAM Identity Provider that trusts GitHub's
OIDC endpoint. You can create an IAM Identity Provider in the AWS Management OIDC endpoint. You can create an IAM Identity Provider in the AWS Management
Console by specifying the following details: Console by specifying the following details:
- **Provider Type**: OIDC - **Provider Type**: OIDC
- **Provider URL**: `https://token.actions.githubusercontent.com` - **Provider URL**: `https://token.actions.githubusercontent.com`
- **Audience**: `sts.amazonaws.com` (or your custom audience if you specified - **Audience**: `sts.amazonaws.com` (or your custom audience if you specified
@ -383,14 +515,18 @@ aws iam create-open-id-connect-provider \
``` ```
### Claims and scoping permissions ### Claims and scoping permissions
To align with the Amazon IAM best practice of [granting least
privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), To align with the Amazon IAM best practice of
[granting least privilege][least-privilege],
[least-privilege]:
https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege
the assume role policy document should contain a the assume role policy document should contain a
[`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html) [`Condition`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
that specifies a subject (`sub`) allowed to assume the role. [GitHub also that specifies a subject (`sub`) allowed to assume the role.
recommends](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#defining-trust-conditions-on-cloud-roles-using-oidc-claims) [GitHub also recommends](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#defining-trust-conditions-on-cloud-roles-using-oidc-claims)
filtering for the correct audience (`aud`). See [AWS IAM filtering for the correct audience (`aud`). See
documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif) [AWS IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif)
on which claims you can filter for in your trust policies. on which claims you can filter for in your trust policies.
Without a subject (`sub`) condition, any GitHub user or repository could Without a subject (`sub`) condition, any GitHub user or repository could
@ -399,7 +535,8 @@ and repository as shown in the CloudFormation template. However, scoping it down
to your org and repo may cause the role assumption to fail in some cases. See to your org and repo may cause the role assumption to fail in some cases. See
[Example subject claims](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims) [Example subject claims](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims)
for specific details on what the subject value will be depending on your for specific details on what the subject value will be depending on your
workflow. You can also [customize your subject claim](https://docs.github.com/en/actions/reference/security/oidc#customizing-the-token-claims) workflow. You can also
[customize your subject claim](https://docs.github.com/en/actions/reference/security/oidc#customizing-the-token-claims)
if you want full control over the information you can filter for in your trust if you want full control over the information you can filter for in your trust
policy. If you aren't sure what your subject (`sub`) key is, you can add the policy. If you aren't sure what your subject (`sub`) key is, you can add the
[`actions-oidc-debugger`](https://github.com/github/actions-oidc-debugger) [`actions-oidc-debugger`](https://github.com/github/actions-oidc-debugger)
@ -407,7 +544,11 @@ action to your workflow to see the value of the subject (`sub`) key, as well as
other claims. other claims.
Additional claim conditions can be added for higher specificity as explained in Additional claim conditions can be added for higher specificity as explained in
the [GitHub documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect). the
[GitHub documentation][gh-oidc-hardening].
[gh-oidc-hardening]:
https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
Due to implementation details, not every OIDC claim is presently supported by Due to implementation details, not every OIDC claim is presently supported by
IAM. IAM.
@ -415,113 +556,165 @@ IAM.
For further information on OIDC and GitHub Actions, please see: For further information on OIDC and GitHub Actions, please see:
* [AWS docs: Creating OpenID Connect (OIDC) identity providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) - [AWS docs: Creating OpenID Connect (OIDC) identity providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)
* [AWS docs: IAM JSON policy elements: Condition](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html) - [AWS docs: IAM JSON policy elements: Condition](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html)
* [GitHub docs: About security hardening with OpenID Connect](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) - [GitHub docs: About security hardening with OpenID Connect](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
* [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) - [GitHub docs: Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
* [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/) - [GitHub changelog: GitHub Actions: Secure cloud deployments with OpenID Connect](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)
Examples ## Examples
--------
### AssumeRoleWithWebIdentity ### AssumeRoleWithWebIdentity
```yaml ```yaml
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
aws-region: us-east-2 aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
role-session-name: MySessionName role-session-name: MySessionName
``` ```
In this example, the Action will load the OIDC token from the GitHub-provided In this example, the Action will load the OIDC token from the GitHub-provided
environment variable and use it to assume the role environment variable and use it to assume the role
`arn:aws:iam::123456789100:role/my-github-actions-role` with the session name `arn:aws:iam::123456789100:role/my-github-actions-role` with the session name
`MySessionName`. `MySessionName`.
### AssumeRole with role previously assumed by action in same workflow ### AssumeRole with role previously assumed by action in same workflow
```yaml ```yaml
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
aws-region: us-east-2 aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
role-session-name: MySessionName role-session-name: MySessionName
- name: Configure other AWS Credentials - name: Configure other AWS Credentials
uses: aws-actions/configure-aws-credentials@v6.0.0 uses: aws-actions/configure-aws-credentials@v6.1.0
with: with:
aws-region: us-east-2 aws-region: us-east-2
role-to-assume: arn:aws:iam::987654321000:role/my-second-role role-to-assume: arn:aws:iam::987654321000:role/my-second-role
role-session-name: MySessionName role-session-name: MySessionName
role-chaining: true role-chaining: true
``` ```
In this two-step example, the first step will use OIDC to assume the role In this two-step example, the first step will use OIDC to assume the role
`arn:aws:iam::123456789100:role/my-github-actions-role` just as in the prior `arn:aws:iam::123456789100:role/my-github-actions-role` just as in the prior
example. Following that, a second step will use this role to assume a different example. Following that, a second step will use this role to assume a different
role, `arn:aws:iam::987654321000:role/my-second-role`. role, `arn:aws:iam::987654321000:role/my-second-role`.
Note that the trust relationship/trust policy of the second role must grant the permissions `sts:AssumeRole` and `sts:TagSession` to the first role. (Or, alternatively, the `TagSession` permission can be omitted if you are using the `role-skip-session-tagging: true` flag for the second step.) Note that the trust relationship/trust policy of the second role must grant the
permissions `sts:AssumeRole` and `sts:TagSession` to the first role. (Or,
alternatively, the `TagSession` permission can be omitted if you are using the
`role-skip-session-tagging: true` flag for the second step.)
### AssumeRole with static IAM credentials in repository secrets ### AssumeRole with static IAM credentials in repository secrets
```yaml
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6.0.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-2
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
role-duration-seconds: 1200
role-session-name: MySessionName
```
In this example, the secret `AWS_ROLE_TO_ASSUME` contains a string like
`arn:aws:iam::123456789100:role/my-github-actions-role`. To assume a role in
the same account as the static credentials, you can simply specify the role
name, like `role-to-assume: my-github-actions-role`.
### Retrieving credentials from step output, AssumeRole with temporary credentials
```yaml ```yaml
- name: Configure AWS Credentials 1 - name: Configure AWS Credentials
id: creds uses: aws-actions/configure-aws-credentials@v6.1.0
uses: aws-actions/configure-aws-credentials@v6.0.0 with:
with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-region: us-east-2 aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role aws-region: us-east-2
output-credentials: true role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
- name: get caller identity 1 role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
run: | role-duration-seconds: 1200
aws sts get-caller-identity role-session-name: MySessionName
- name: Configure AWS Credentials 2
uses: aws-actions/configure-aws-credentials@v6.0.0
with:
aws-region: us-east-2
aws-access-key-id: ${{ steps.creds.outputs.aws-access-key-id }}
aws-secret-access-key: ${{ steps.creds.outputs.aws-secret-access-key }}
aws-session-token: ${{ steps.creds.outputs.aws-session-token }}
role-to-assume: arn:aws:iam::123456789100:role/my-other-github-actions-role
- name: get caller identity2
run: |
aws sts get-caller-identity
``` ```
In this example, the secret `AWS_ROLE_TO_ASSUME` contains a string like
`arn:aws:iam::123456789100:role/my-github-actions-role`. To assume a role in the
same account as the static credentials, you can simply specify the role name,
like `role-to-assume: my-github-actions-role`.
### Retrieving credentials from step output
```yaml
- name: Configure AWS Credentials 1
id: creds
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
output-credentials: true
- name: get caller identity 1
run: |
aws sts get-caller-identity
- name: Configure AWS Credentials 2
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-east-2
aws-access-key-id: ${{ steps.creds.outputs.aws-access-key-id }}
aws-secret-access-key: ${{ steps.creds.outputs.aws-secret-access-key }}
aws-session-token: ${{ steps.creds.outputs.aws-session-token }}
role-to-assume: arn:aws:iam::123456789100:role/my-other-github-actions-role
- name: get caller identity2
run: |
aws sts get-caller-identity
```
This example shows that you can reference the fetched credentials as outputs if This example shows that you can reference the fetched credentials as outputs if
`output-credentials` is set to true. This example also shows that you can use `output-credentials` is set to true. This example also shows that you can use
the `aws-session-token` input in a situation where session tokens are fetched the `aws-session-token` input in a situation where session tokens are fetched
and passed to this action. and passed to this action.
Versioning ### Configure multiple AWS profiles in a single workflow
----------
Starting with version 5.0.0, this action uses semantic-style release tags and ```yaml
[immutable releases](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases). - name: Configure AWS Credentials for Dev
A floating version tag (vN) is also provided for convenience: this tag will uses: aws-actions/configure-aws-credentials@v6.1.0
move to the latest major version (vN -> vN.2.1, vM -> vM.0.0, etc.). with:
aws-region: us-east-1
role-to-assume: arn:aws:iam::111111111111:role/dev-role
aws-profile: dev
- name: Configure AWS Credentials for Prod
uses: aws-actions/configure-aws-credentials@v6.1.0
with:
aws-region: us-west-2
role-to-assume: arn:aws:iam::222222222222:role/prod-role
aws-profile: prod
- name: Use multiple profiles
run: |
# Check caller identity for dev account
aws sts get-caller-identity --profile dev
# Check caller identity for prod account
aws sts get-caller-identity --profile prod
# Deploy to dev using CDK
cdk deploy --profile dev
```
This example shows how to configure multiple named AWS profiles in a single
workflow. When using the `aws-profile` input, credentials are written to
`~/.aws/credentials` and `~/.aws/config` files, allowing you to reference
different profiles using the `--profile` flag with AWS CLI, SDKs, CDK, and
other tools.
Each profile is independent and can authenticate to different AWS accounts or
use different roles. This is particularly useful for multi-account deployments
or when you need to interact with multiple AWS environments in a single job.
## Versioning
Starting with version 5.0.0, this action uses semantic-style release tags and
[immutable releases][immutable-releases].
[immutable-releases]:
https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases
A floating version tag (vN) is also provided for convenience: this tag will move
to the latest major version (vN -> vN.2.1, vM -> vM.0.0, etc.).
## License
License
-------
This code is made available under the MIT license. This code is made available under the MIT license.
Security Disclosures ## Security Disclosures
--------------------
If you would like to report a potential security issue in this project, please If you would like to report a potential security issue in this project, please
do not create a GitHub issue. Instead, please follow the instructions do not create a GitHub issue. Instead, please follow the instructions
[here](https://aws.amazon.com/security/vulnerability-reporting/) or [on the vulnerability reporting page](https://aws.amazon.com/security/vulnerability-reporting/)
[email AWS security](mailto:aws-security@amazon.com) directly. or [email AWS security](mailto:aws-security@amazon.com) directly.

View file

@ -13,6 +13,12 @@ inputs:
aws-region: aws-region:
description: AWS Region, e.g. us-east-2 description: AWS Region, e.g. us-east-2
required: true required: true
aws-profile:
description: Name of the AWS profile to configure. When provided, credentials are written to ~/.aws/credentials and ~/.aws/config files instead of env variables (unless output-env-credentials is manually set to true). Name cannot contain whitespace, square brackets, or slashes.
required: false
overwrite-aws-profile:
description: Overwrite the given AWS profile if it already exists. Requires aws-profile. When set to false or not set, an error will be thrown if the profile already exists.
required: false
role-to-assume: role-to-assume:
description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials. description: The Amazon Resource Name (ARN) of the role to assume. Use the provided credentials to assume an IAM role and configure the Actions environment with the assumed role credentials rather than with the provided credentials.
required: false required: false
@ -71,7 +77,6 @@ inputs:
output-env-credentials: output-env-credentials:
description: Whether to export credentials as environment variables. If you set this to false, you probably want to use output-credentials. description: Whether to export credentials as environment variables. If you set this to false, you probably want to use output-credentials.
required: false required: false
default: "true"
unset-current-credentials: 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 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 required: false

31
dist/cleanup/index.js generated vendored
View file

@ -21418,9 +21418,9 @@ var require_core = __commonJS({
exports2.exportVariable = exportVariable3; exports2.exportVariable = exportVariable3;
exports2.setSecret = setSecret2; exports2.setSecret = setSecret2;
exports2.addPath = addPath; exports2.addPath = addPath;
exports2.getInput = getInput2; exports2.getInput = getInput3;
exports2.getMultilineInput = getMultilineInput; exports2.getMultilineInput = getMultilineInput;
exports2.getBooleanInput = getBooleanInput2; exports2.getBooleanInput = getBooleanInput;
exports2.setOutput = setOutput2; exports2.setOutput = setOutput2;
exports2.setCommandEcho = setCommandEcho; exports2.setCommandEcho = setCommandEcho;
exports2.setFailed = setFailed2; exports2.setFailed = setFailed2;
@ -21468,7 +21468,7 @@ var require_core = __commonJS({
} }
process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`; process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`;
} }
function getInput2(name, options) { function getInput3(name, options) {
const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) { if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`); throw new Error(`Input required and not supplied: ${name}`);
@ -21479,16 +21479,16 @@ var require_core = __commonJS({
return val.trim(); return val.trim();
} }
function getMultilineInput(name, options) { function getMultilineInput(name, options) {
const inputs = getInput2(name, options).split("\n").filter((x) => x !== ""); const inputs = getInput3(name, options).split("\n").filter((x) => x !== "");
if (options && options.trimWhitespace === false) { if (options && options.trimWhitespace === false) {
return inputs; return inputs;
} }
return inputs.map((input) => input.trim()); return inputs.map((input) => input.trim());
} }
function getBooleanInput2(name, options) { function getBooleanInput(name, options) {
const trueValue = ["true", "True", "TRUE"]; const trueValue = ["true", "True", "TRUE"];
const falseValue = ["false", "False", "FALSE"]; const falseValue = ["false", "False", "FALSE"];
const val = getInput2(name, options); const val = getInput3(name, options);
if (trueValue.includes(val)) if (trueValue.includes(val))
return true; return true;
if (falseValue.includes(val)) if (falseValue.includes(val))
@ -21597,30 +21597,19 @@ var core = __toESM(require_core());
function errorMessage(error) { function errorMessage(error) {
return error instanceof Error ? error.message : String(error); return error instanceof Error ? error.message : String(error);
} }
function getBooleanInput(name, options) {
const trueValue = ["true", "True", "TRUE"];
const falseValue = ["false", "False", "FALSE"];
const optionsWithoutDefault = { ...options };
delete optionsWithoutDefault.default;
const val = core.getInput(name, optionsWithoutDefault);
if (trueValue.includes(val)) return true;
if (falseValue.includes(val)) return false;
if (val === "") return options?.default ?? false;
throw new TypeError(
`Input does not meet YAML 1.2 "Core Schema" specification: ${name}
Support boolean input list: \`true | True | TRUE | false | False | FALSE\``
);
}
// src/cleanup/index.ts // src/cleanup/index.ts
function cleanup() { function cleanup() {
if (getBooleanInput("output-env-credentials", { required: false, default: true })) { if (core2.getInput("output-env-credentials") !== "false") {
try { try {
core2.exportVariable("AWS_ACCESS_KEY_ID", ""); core2.exportVariable("AWS_ACCESS_KEY_ID", "");
core2.exportVariable("AWS_SECRET_ACCESS_KEY", ""); core2.exportVariable("AWS_SECRET_ACCESS_KEY", "");
core2.exportVariable("AWS_SESSION_TOKEN", ""); core2.exportVariable("AWS_SESSION_TOKEN", "");
core2.exportVariable("AWS_DEFAULT_REGION", ""); core2.exportVariable("AWS_DEFAULT_REGION", "");
core2.exportVariable("AWS_REGION", ""); core2.exportVariable("AWS_REGION", "");
if (core2.getInput("aws-profile")) {
core2.exportVariable("AWS_PROFILE", "");
}
} catch (error) { } catch (error) {
core2.setFailed(errorMessage(error)); core2.setFailed(errorMessage(error));
} }

1206
dist/index.js generated vendored

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,13 @@
## [federated-setup](./federated-setup/README.md) ## [federated-setup](./federated-setup/README.md)
The directory contains templates for setting up the `configure-aws-credentials` federation between your GitHub Organization/repository and your AWS account. The directory contains templates for setting up the `configure-aws-credentials`
federation between your GitHub Organization/repository and your AWS account.
## [cfn-deploy-example](./cfn-deploy-example/README.md) ## [cfn-deploy-example](./cfn-deploy-example/README.md)
Repository example uses aws-action `configure-aws-credentials` with OIDC federation template [github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml). Example demonstrates a repository that deploys AWS CloudFormation template using cfn-deploy GitHub Action. Repository example uses aws-action `configure-aws-credentials` with OIDC
federation template
[github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml).
Example demonstrates a repository that deploys AWS CloudFormation template using
cfn-deploy GitHub Action.

View file

@ -1,14 +1,24 @@
# cfn-deploy example # cfn-deploy example
Example uses aws-action `configure-aws-credentials` with OIDC federation. Prior to using this example project, the user needs to deploy the [github-actions-oidc-federation-and-role](../federated-setup/github-actions-oidc-federation-and-role.yml) template in the AWS account they want to deploy the CloudFormation template into. Specify the GitHub Organization name, repository name, and the specific branch you want to deploy on. Example uses aws-action `configure-aws-credentials` with OIDC federation. Prior
to using this example project, the user needs to deploy the
[github-actions-oidc-federation-and-role](../federated-setup/github-actions-oidc-federation-and-role.yml)
template in the AWS account they want to deploy the CloudFormation template
into. Specify the GitHub Organization name, repository name, and the specific
branch you want to deploy on.
Within the [github/workflows](./.github/workflows/) directory there is a [compliance.yml](./.github/workflows/compliance.yml) and a [deploy.yml](./.github/workflows/deploy.yml). The deploy.yml file leverages the aws-action `configure-aws-credentials` and accesses GitHub Action Secrets for some of the variables. The compliance.yml runs static application security testing using cfn-guard. Within the [github/workflows](./.github/workflows/) directory there is a
[compliance.yml](./.github/workflows/compliance.yml) and a
[deploy.yml](./.github/workflows/deploy.yml). The deploy.yml file leverages the
aws-action `configure-aws-credentials` and accesses GitHub Action Secrets for
some of the variables. The compliance.yml runs static application security
testing using cfn-guard.
To use the example you will need to set the following GitHub Action Secrets: To use the example you will need to set the following GitHub Action Secrets:
| Secret Key | Used With | Description | | Secret Key | Used With | Description |
| --------- | -------- | -----------| | --------------- | -------------------------------- | ---------------------------------------- |
| AWS_ACCOUNT_ID | configure-aws-credentials | The AWS account ID | | AWS_ACCOUNT_ID | configure-aws-credentials | The AWS account ID |
| AWS_DEPLOY_ROLE | configure-aws-credentials | The name of the IAM role | | AWS_DEPLOY_ROLE | configure-aws-credentials | The name of the IAM role |
| VPC_ID | aws-cloudformation-github-deploy | VPC ID the EC2 Bastion is deployed to | | VPC_ID | aws-cloudformation-github-deploy | VPC ID the EC2 Bastion is deployed to |
| SUBNET_ID | aws-cloudformation-github-deploy | Subnet ID the EC2 Bastion is deployed to | | SUBNET_ID | aws-cloudformation-github-deploy | Subnet ID the EC2 Bastion is deployed to |

View file

@ -2,8 +2,10 @@
## [github-action-oidc-federation](./github-actions-oidc-federation.yml) ## [github-action-oidc-federation](./github-actions-oidc-federation.yml)
Setup of the OIDC federation between your GitHub Organization/repository and your AWS account. Setup of the OIDC federation between your GitHub Organization/repository and
your AWS account.
## [github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml) ## [github-actions-oidc-federation-and-role](./github-actions-oidc-federation-and-role.yml)
Setup of the OIDC federation between your GitHub Organization/repository and your AWS account along with a role that only executes on specific branch. Setup of the OIDC federation between your GitHub Organization/repository and
your AWS account along with a role that only executes on specific branch.

1145
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@
"version": "6.0.0", "version": "6.0.0",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"lint": "biome check --error-on-warnings ./src", "lint": "biome check --error-on-warnings ./src && markdownlint -i node_modules -i CHANGELOG.md '**/*.md'",
"lint:fix": "biome check --write ./src", "lint:fix": "biome check --write ./src && markdownlint -i node_modules -i CHANGELOG.md -f '**/*.md'",
"package": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js && esbuild src/cleanup/index.ts --bundle --platform=node --target=node24 --outfile=dist/cleanup/index.js && npm run license", "package": "esbuild src/index.ts --bundle --platform=node --target=node24 --outfile=dist/index.js && esbuild src/cleanup/index.ts --bundle --platform=node --target=node24 --outfile=dist/cleanup/index.js && npm run license",
"test": "npm run lint && vitest run && npm run build", "test": "npm run lint && vitest run && npm run build",
"clean": "del-cli coverage test-reports node_modules", "clean": "del-cli coverage test-reports node_modules",
@ -26,6 +26,7 @@
"esbuild": "^0.27.4", "esbuild": "^0.27.4",
"generate-license-file": "^4.1.1", "generate-license-file": "^4.1.1",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"markdownlint-cli": "^0.48.0",
"memfs": "^4.57.1", "memfs": "^4.57.1",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"typescript": "^6.0.2", "typescript": "^6.0.2",

View file

@ -14,7 +14,8 @@ import { errorMessage, getBooleanInput } from '../helpers';
export function cleanup() { export function cleanup() {
// Only attempt to change environment variables if we changed them in the first place // Only attempt to change environment variables if we changed them in the first place
if (getBooleanInput('output-env-credentials', { required: false, default: true })) { const awsProfile = core.getInput('aws-profile', { required: false });
if (getBooleanInput('output-env-credentials', { required: false, default: !awsProfile })) {
try { try {
// The GitHub Actions toolkit does not have an option to completely unset // The GitHub Actions toolkit does not have an option to completely unset
// environment variables, so we overwrite the current value with an empty // environment variables, so we overwrite the current value with an empty
@ -25,6 +26,9 @@ export function cleanup() {
core.exportVariable('AWS_SESSION_TOKEN', ''); core.exportVariable('AWS_SESSION_TOKEN', '');
core.exportVariable('AWS_DEFAULT_REGION', ''); core.exportVariable('AWS_DEFAULT_REGION', '');
core.exportVariable('AWS_REGION', ''); core.exportVariable('AWS_REGION', '');
if (core.getInput('aws-profile')) {
core.exportVariable('AWS_PROFILE', '');
}
} catch (error) { } catch (error) {
core.setFailed(errorMessage(error)); core.setFailed(errorMessage(error));
} }

View file

@ -30,6 +30,7 @@ export function translateEnvVariables() {
'SPECIAL_CHARACTERS_WORKAROUND', 'SPECIAL_CHARACTERS_WORKAROUND',
'USE_EXISTING_CREDENTIALS', 'USE_EXISTING_CREDENTIALS',
'NO_PROXY', 'NO_PROXY',
'OVERWRITE_AWS_PROFILE',
]; ];
// Treat HTTPS_PROXY as HTTP_PROXY. Precedence is HTTPS_PROXY > HTTP_PROXY // Treat HTTPS_PROXY as HTTP_PROXY. Precedence is HTTPS_PROXY > HTTP_PROXY
if (process.env.HTTPS_PROXY) process.env.HTTP_PROXY = process.env.HTTPS_PROXY; if (process.env.HTTPS_PROXY) process.env.HTTP_PROXY = process.env.HTTPS_PROXY;

View file

@ -14,6 +14,7 @@ import {
unsetCredentials, unsetCredentials,
verifyKeys, verifyKeys,
} from './helpers'; } from './helpers';
import { writeProfileFiles } from './profileManager';
const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds) const DEFAULT_ROLE_DURATION = 3600; // One hour (seconds)
const ROLE_SESSION_NAME = 'GitHubActions'; const ROLE_SESSION_NAME = 'GitHubActions';
@ -29,6 +30,8 @@ export async function run() {
const sessionTokenInput = core.getInput('aws-session-token', { required: false }); const sessionTokenInput = core.getInput('aws-session-token', { required: false });
const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput; const SessionToken = sessionTokenInput === '' ? undefined : sessionTokenInput;
const region = core.getInput('aws-region', { required: true }); const region = core.getInput('aws-region', { required: true });
const awsProfile = core.getInput('aws-profile', { required: false });
const overwriteAwsProfile = getBooleanInput('overwrite-aws-profile', { required: false });
const roleToAssume = core.getInput('role-to-assume', { required: false }); const roleToAssume = core.getInput('role-to-assume', { required: false });
const audience = core.getInput('audience', { required: false }); const audience = core.getInput('audience', { required: false });
const maskAccountId = getBooleanInput('mask-aws-account-id', { required: false }); const maskAccountId = getBooleanInput('mask-aws-account-id', { required: false });
@ -46,7 +49,9 @@ export async function run() {
}); });
const roleChaining = getBooleanInput('role-chaining', { required: false }); const roleChaining = getBooleanInput('role-chaining', { required: false });
const outputCredentials = getBooleanInput('output-credentials', { required: false }); const outputCredentials = getBooleanInput('output-credentials', { required: false });
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: true }); // Default to always outputting environment credentials unless profile is specified. If profile is specified, default to
// no environment credentials (but still output them if the user specifically requests it).
const outputEnvCredentials = getBooleanInput('output-env-credentials', { required: false, default: !awsProfile });
const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false }); const unsetCurrentCredentials = getBooleanInput('unset-current-credentials', { required: false });
let disableRetry = getBooleanInput('disable-retry', { required: false }); let disableRetry = getBooleanInput('disable-retry', { required: false });
const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false }); const specialCharacterWorkaround = getBooleanInput('special-characters-workaround', { required: false });
@ -118,6 +123,7 @@ export async function run() {
if (!region.match(REGION_REGEX)) { if (!region.match(REGION_REGEX)) {
throw new Error(`Region is not valid: ${region}`); throw new Error(`Region is not valid: ${region}`);
} }
exportRegion(region, outputEnvCredentials); exportRegion(region, outputEnvCredentials);
// Instantiate credentials client // Instantiate credentials client
@ -165,6 +171,12 @@ export async function run() {
// the source credentials to already be masked as secrets // the source credentials to already be masked as secrets
// in any error messages. // in any error messages.
exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials); exportCredentials({ AccessKeyId, SecretAccessKey, SessionToken }, outputCredentials, outputEnvCredentials);
// If using IAM User Credentials, write to profile now so that the assumeRole call can succeed (and also for
// credential validation before role assumption).
if (awsProfile) {
writeProfileFiles(awsProfile, { AccessKeyId, SecretAccessKey, SessionToken }, region, overwriteAwsProfile);
}
} else if (!webIdentityTokenFile && !roleChaining) { } else if (!webIdentityTokenFile && !roleChaining) {
// Proceed only if credentials can be picked up // Proceed only if credentials can be picked up
await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds); await credentialsClient.validateCredentials(undefined, roleChaining, expectedAccountIds);
@ -178,7 +190,6 @@ export async function run() {
await credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds); await credentialsClient.validateCredentials(AccessKeyId, roleChaining, expectedAccountIds);
sourceAccountId = await exportAccountId(credentialsClient, maskAccountId); sourceAccountId = await exportAccountId(credentialsClient, maskAccountId);
} }
// Get role credentials if configured to do so // Get role credentials if configured to do so
if (roleToAssume) { if (roleToAssume) {
let roleCredentials: AssumeRoleCommandOutput; let roleCredentials: AssumeRoleCommandOutput;
@ -210,7 +221,9 @@ export async function run() {
// First: self-hosted runners. If the GITHUB_ACTIONS environment variable // First: self-hosted runners. If the GITHUB_ACTIONS environment variable
// is set to `true` then we are NOT in a self-hosted runner. // is set to `true` then we are NOT in a self-hosted runner.
// Second: Customer provided credentials manually (IAM User keys stored in GH Secrets) // Second: Customer provided credentials manually (IAM User keys stored in GH Secrets)
if (!process.env.GITHUB_ACTIONS || AccessKeyId) { // If 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( await credentialsClient.validateCredentials(
roleCredentials.Credentials?.AccessKeyId, roleCredentials.Credentials?.AccessKeyId,
roleChaining, roleChaining,
@ -220,8 +233,38 @@ export async function run() {
if (outputEnvCredentials) { if (outputEnvCredentials) {
await exportAccountId(credentialsClient, maskAccountId); await exportAccountId(credentialsClient, maskAccountId);
} }
// Write profile files if profile mode is enabled
if (awsProfile) {
if (!roleCredentials.Credentials) {
throw new Error('AssumeRole call succeeded but returned no credentials');
}
// If user provided IAM User Credentials and then we assumed a role, overwrite the profile file to add
// the session token. (this only overwrites the profile within a single run of the action).
// 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,
);
} else {
writeProfileFiles(awsProfile, roleCredentials.Credentials, region, overwriteAwsProfile);
}
if (outputEnvCredentials) {
core.exportVariable('AWS_PROFILE', awsProfile);
}
}
} else { } else {
core.info('Proceeding with IAM user credentials'); core.info('Proceeding with IAM user credentials');
if (awsProfile) {
if (outputEnvCredentials) {
core.exportVariable('AWS_PROFILE', awsProfile);
}
}
} }
// Clear timeout on successful completion // Clear timeout on successful completion

214
src/profileManager.ts Normal file
View file

@ -0,0 +1,214 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import * as core from '@actions/core';
import type { Credentials } from '@aws-sdk/client-sts';
/**
* Parse an INI-format string into a nested object.
* Preserves literal section names (e.g. "profile dev" stays as-is).
*/
export function parseIni(iniData: string): Record<string, Record<string, string>> {
const result: Record<string, Record<string, string>> = {};
let currentSection: string | undefined;
for (const line of iniData.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) {
continue;
}
const sectionMatch = trimmed.match(/^\[([^\]]*)\]$/);
if (sectionMatch) {
currentSection = sectionMatch[1] as string;
if (currentSection === '__proto__') {
currentSection = undefined;
continue;
}
result[currentSection] = result[currentSection] || {};
continue;
}
if (currentSection) {
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
const value = trimmed.substring(eqIndex + 1).trim();
if (key !== '__proto__') {
const section = result[currentSection];
if (section) {
section[key] = value;
}
}
}
}
}
return result;
}
/**
* Serialize a nested object into INI-format string.
*/
export function stringifyIni(data: Record<string, Record<string, string>>): string {
const sections: string[] = [];
for (const [sectionName, sectionData] of Object.entries(data)) {
const lines: string[] = [`[${sectionName}]`];
for (const [key, value] of Object.entries(sectionData)) {
lines.push(`${key} = ${value}`);
}
sections.push(lines.join('\n'));
}
return `${sections.join('\n\n')}\n`;
}
interface ProfileFilePaths {
credentials: string;
config: string;
}
/**
* Get the file paths for AWS credentials and config files
* Respects AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE environment variables
*/
export function getProfileFilePaths(): ProfileFilePaths {
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || path.join(os.homedir(), '.aws', 'credentials');
const configPath = process.env.AWS_CONFIG_FILE || path.join(os.homedir(), '.aws', 'config');
return {
credentials: credentialsPath,
config: configPath,
};
}
/**
* Ensure the AWS directory exists with secure permissions
* Creates the directory with 700 permissions (rwx for owner only)
*/
export function ensureAwsDirectoryExists(filePath: string): void {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
core.debug(`Creating directory: ${dir}`);
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
}
/**
* Validate profile name format
* Profile names must be non-empty, contain no whitespace, brackets, or path separators
*/
export function validateProfileName(profileName: string): void {
if (!profileName || profileName.trim() === '') {
throw new Error('aws-profile must not be empty');
}
if (/\s/.test(profileName)) {
throw new Error('aws-profile must not contain whitespace');
}
// INI section names can't contain brackets
if (/[[\]]/.test(profileName)) {
throw new Error('aws-profile must not contain brackets');
}
// Prevent path traversal
if (profileName.includes('/') || profileName.includes('\\')) {
throw new Error('aws-profile must not contain path separators');
}
}
/**
* Merge a profile section into an INI file
* Reads existing file, updates the specified section, and writes back
*/
export function mergeProfileSection(
filePath: string,
sectionName: string,
data: Record<string, string>,
overwriteAwsProfile: boolean,
): void {
let existingContent: Record<string, Record<string, string>> = {};
// Read existing file if it exists
if (fs.existsSync(filePath)) {
core.debug(`Reading existing file: ${filePath}`);
const fileContent = fs.readFileSync(filePath, 'utf-8');
existingContent = parseIni(fileContent);
}
if (existingContent[sectionName] && !overwriteAwsProfile) {
throw new Error(
`Profile with name "${sectionName}" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`,
);
}
// Merge: update existing profile or add new one
existingContent[sectionName] = data;
const content = stringifyIni(existingContent);
core.debug(`Writing profile to ${filePath}`);
fs.writeFileSync(filePath, content, { mode: 0o600 });
}
/**
* Write AWS profile files with credentials and configuration
* This is the main entry point for profile file operations
*
* @param profileName - Name of the AWS profile to configure
* @param credentials - AWS credentials (access key, secret key, session token)
* @param region - AWS region
*/
export function writeProfileFiles(
profileName: string,
credentials: Partial<Credentials>,
region: string,
overwriteAwsProfile: boolean,
): void {
try {
// Validate profile name
validateProfileName(profileName);
const paths = getProfileFilePaths();
// Ensure .aws directory exists
ensureAwsDirectoryExists(paths.credentials);
ensureAwsDirectoryExists(paths.config);
// Prepare credentials data
const credentialsData: Record<string, string> = {};
if (credentials.AccessKeyId) {
credentialsData.aws_access_key_id = credentials.AccessKeyId;
}
if (credentials.SecretAccessKey) {
credentialsData.aws_secret_access_key = credentials.SecretAccessKey;
}
if (credentials.SessionToken) {
credentialsData.aws_session_token = credentials.SessionToken;
}
// Credentials file uses [profileName] syntax
const credsSectionName = profileName;
// Config file uses [profile profileName] syntax, except for 'default'
const configSectionName = profileName === 'default' ? 'default' : `profile ${profileName}`;
// Prepare config data
const configData: Record<string, string> = {
region: region,
};
// Write to credentials file
core.info(`Writing credentials to profile: ${profileName}`);
mergeProfileSection(paths.credentials, credsSectionName, credentialsData, overwriteAwsProfile);
// Write to config file
core.info(`Writing config to profile: ${profileName}`);
mergeProfileSection(paths.config, configSectionName, configData, overwriteAwsProfile);
core.info(`✓ Successfully configured AWS profile: ${profileName}`);
} catch (error) {
throw new Error(
`Failed to write AWS profile '${profileName}': ${error instanceof Error ? error.message : String(error)}`,
);
}
}

View file

@ -38,6 +38,26 @@ describe('Configure AWS Credentials cleanup', {}, () => {
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', ''); expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', '');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', ''); expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', '');
}); });
it('also clears AWS_PROFILE when aws-profile was set', {}, () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'aws-profile') return 'my-profile';
if (name === 'output-env-credentials') return 'true';
return '';
});
cleanup();
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.exportVariable).toHaveBeenCalledTimes(6);
expect(core.exportVariable).toHaveBeenCalledWith('AWS_PROFILE', '');
});
it('skips env cleanup when aws-profile is set without output-env-credentials', {}, () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'aws-profile') return 'my-profile';
return '';
});
cleanup();
expect(core.setFailed).toHaveBeenCalledTimes(0);
expect(core.exportVariable).toHaveBeenCalledTimes(0);
});
it('handles errors', {}, () => { it('handles errors', {}, () => {
vi.spyOn(core, 'exportVariable').mockImplementationOnce(() => { vi.spyOn(core, 'exportVariable').mockImplementationOnce(() => {
throw new Error('Test error'); throw new Error('Test error');

View file

@ -1,11 +1,11 @@
import { beforeEach } from 'node:test';
import * as core from '@actions/core'; import * as core from '@actions/core';
import { describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as helpers from '../src/helpers'; import * as helpers from '../src/helpers';
describe('Configure AWS Credentials helpers', {}, () => { describe('Configure AWS Credentials helpers', {}, () => {
beforeEach(() => { beforeEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
vi.spyOn(core, 'debug').mockImplementation(() => {});
}); });
it('removes brackets from GitHub Actor', {}, () => { it('removes brackets from GitHub Actor', {}, () => {
const actor = 'actor[bot]'; const actor = 'actor[bot]';
@ -91,6 +91,7 @@ describe('Configure AWS Credentials helpers', {}, () => {
}); });
it('clears session token when not provided', {}, () => { it('clears session token when not provided', {}, () => {
vi.spyOn(core, 'setSecret').mockImplementation(() => {});
vi.spyOn(core, 'exportVariable').mockImplementation(() => {}); vi.spyOn(core, 'exportVariable').mockImplementation(() => {});
process.env.AWS_SESSION_TOKEN = 'old-token'; process.env.AWS_SESSION_TOKEN = 'old-token';
helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test' }, false, true); helpers.exportCredentials({ AccessKeyId: 'test', SecretAccessKey: 'test' }, false, true);

View file

@ -10,6 +10,7 @@ import { fs, vol } from 'memfs';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CredentialsClient } from '../src/CredentialsClient'; import { CredentialsClient } from '../src/CredentialsClient';
import { run } from '../src/index'; import { run } from '../src/index';
import * as profileManager from '../src/profileManager';
import mocks from './mockinputs.test'; import mocks from './mockinputs.test';
const mockedSTSClient = mockClient(STSClient); const mockedSTSClient = mockClient(STSClient);
@ -822,4 +823,202 @@ describe('Configure AWS Credentials', {}, () => {
expect(core.setFailed).not.toHaveBeenCalled(); expect(core.setFailed).not.toHaveBeenCalled();
}); });
}); });
describe('AWS Profile Support', {}, () => {
beforeEach(() => {
vi.clearAllMocks();
mockedSTSClient.reset();
vi.mock('node:fs');
vol.reset();
});
it('writes profile files with OIDC authentication', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.GH_OIDC_INPUTS,
'aws-profile': 'dev',
}),
);
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
// Verify credentials were NOT exported to environment variables
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SESSION_TOKEN', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_PROFILE', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_REGION', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_DEFAULT_REGION', expect.anything());
// Verify profile files were written
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: dev');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: dev');
expect(core.setFailed).not.toHaveBeenCalled();
});
it('writes profile files with IAM user credentials', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.IAM_USER_INPUTS,
'aws-profile': 'production',
}),
);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials').mockResolvedValue({
accessKeyId: 'MYAWSACCESSKEYID',
});
await run();
// Verify credentials were NOT exported to environment variables
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SESSION_TOKEN', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_PROFILE', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_REGION', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_DEFAULT_REGION', expect.anything());
// Verify profile files were written
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: production');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: production');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: production');
expect(core.setFailed).not.toHaveBeenCalled();
});
it('writes profile files with IAM user role assumption', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.IAM_ASSUMEROLE_INPUTS,
'aws-profile': 'assumed-role',
}),
);
mockedSTSClient.on(AssumeRoleCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
// biome-ignore lint/suspicious/noExplicitAny: any required to mock private method
vi.spyOn(CredentialsClient.prototype as any, 'loadCredentials')
.mockResolvedValueOnce({ accessKeyId: 'MYAWSACCESSKEYID' })
.mockResolvedValueOnce({ accessKeyId: 'STSAWSACCESSKEYID' });
vi.spyOn(profileManager, 'writeProfileFiles');
await run();
// Verify credentials were NOT exported to environment variables
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_SESSION_TOKEN', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_PROFILE', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_REGION', expect.anything());
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_DEFAULT_REGION', expect.anything());
// Verify profile files were written
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: assumed-role');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: assumed-role');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: assumed-role');
// Verify profile files were written twice (first to write access key id and access key, second to write
// actual session token after role assumption
expect(profileManager.writeProfileFiles).toHaveBeenCalledTimes(2);
expect(core.setFailed).not.toHaveBeenCalled();
});
it('respects output-env-credentials=true with profiles', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.GH_OIDC_INPUTS,
'aws-profile': 'dev',
'output-env-credentials': 'true',
}),
);
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
// verify that env vars were exported
expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', 'STSAWSACCESSKEYID');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 'STSAWSSECRETACCESSKEY');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', 'STSAWSSESSIONTOKEN');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_PROFILE', 'dev');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'fake-region-1');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'fake-region-1');
// Verify profile files were still written
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: dev');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: dev');
expect(core.setFailed).not.toHaveBeenCalled();
});
it('maintains backward compatibility when aws-profile is not specified', async () => {
vi.spyOn(core, 'getInput').mockImplementation(mocks.getInput(mocks.GH_OIDC_INPUTS));
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
// Verify credentials WERE exported to environment variables (backward compatibility)
expect(core.exportVariable).toHaveBeenCalledWith('AWS_ACCESS_KEY_ID', 'STSAWSACCESSKEYID');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SECRET_ACCESS_KEY', 'STSAWSSECRETACCESSKEY');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', 'STSAWSSESSIONTOKEN');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_REGION', 'fake-region-1');
expect(core.exportVariable).toHaveBeenCalledWith('AWS_DEFAULT_REGION', 'fake-region-1');
// Verify AWS_PROFILE was NOT exported
expect(core.exportVariable).not.toHaveBeenCalledWith('AWS_PROFILE', expect.anything());
// Verify profile files were NOT written
expect(core.info).not.toHaveBeenCalledWith(expect.stringContaining('Writing credentials to profile'));
expect(core.info).not.toHaveBeenCalledWith(expect.stringContaining('✓ Successfully configured AWS profile:'));
expect(core.setFailed).not.toHaveBeenCalled();
});
it('handles default profile correctly', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.GH_OIDC_INPUTS,
'aws-profile': 'default',
}),
);
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
// Verify profile files were written for 'default' profile
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: default');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: default');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: default');
expect(core.setFailed).not.toHaveBeenCalled();
});
it('rejects invalid profile names with whitespace', async () => {
vi.spyOn(core, 'getInput').mockImplementation(
mocks.getInput({
...mocks.GH_OIDC_INPUTS,
'aws-profile': 'invalid profile',
}),
);
vi.spyOn(core, 'getIDToken').mockResolvedValue('testoidctoken');
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolves(mocks.outputs.STS_CREDENTIALS);
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('whitespace'));
});
});
}); });

724
test/profileManager.test.ts Normal file
View file

@ -0,0 +1,724 @@
import * as core from '@actions/core';
import { fs, vol } from 'memfs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
ensureAwsDirectoryExists,
getProfileFilePaths,
mergeProfileSection,
parseIni,
stringifyIni,
validateProfileName,
writeProfileFiles,
} from '../src/profileManager';
describe('Profile Manager', {}, () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.mock('node:fs');
vol.reset();
vi.spyOn(core, 'debug').mockImplementation(() => {});
vi.spyOn(core, 'info').mockImplementation(() => {});
});
describe('parseIni', {}, () => {
it('parses a single section', {}, () => {
const result = parseIni('[dev]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n');
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' });
});
it('parses multiple sections', {}, () => {
const result = parseIni('[dev]\nkey=dev_val\n\n[prod]\nkey=prod_val\n');
expect(result.dev.key).toBe('dev_val');
expect(result.prod.key).toBe('prod_val');
});
it('skips comments and empty lines', {}, () => {
const result = parseIni('# comment\n; another comment\n\n[dev]\nkey=val\n');
expect(result.dev).toEqual({ key: 'val' });
});
it('trims whitespace around keys and values', {}, () => {
const result = parseIni('[dev]\n key = val \n');
expect(result.dev.key).toBe('val');
});
it('preserves section names with spaces (e.g. profile prefix)', {}, () => {
const result = parseIni('[profile dev]\nregion=us-east-1\n');
expect(result['profile dev']).toEqual({ region: 'us-east-1' });
});
it('guards against __proto__ section pollution', {}, () => {
const result = parseIni('[__proto__]\npolluted=true\n[safe]\nkey=val\n');
expect(result.__proto__).not.toHaveProperty('polluted');
expect(result.safe).toEqual({ key: 'val' });
});
it('guards against __proto__ key pollution', {}, () => {
const result = parseIni('[dev]\n__proto__=evil\naws_access_key_id=AKIA\n');
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA' });
expect(result.dev).not.toHaveProperty('__proto__', 'evil');
});
it('handles values containing equals signs', {}, () => {
const result = parseIni('[dev]\naws_session_token=FwoGZXIvYXdzEBYa/base64==\n');
expect(result.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYa/base64==');
});
it('handles empty values', {}, () => {
const result = parseIni('[dev]\ncli_pager=\n');
expect(result.dev.cli_pager).toBe('');
});
it('returns empty object for empty input', {}, () => {
expect(parseIni('')).toEqual({});
});
it('returns empty object for whitespace-only input', {}, () => {
expect(parseIni(' \n\n \n')).toEqual({});
});
it('handles Windows line endings (CRLF)', {}, () => {
const result = parseIni('[dev]\r\naws_access_key_id=AKIA\r\naws_secret_access_key=secret\r\n');
expect(result.dev).toEqual({ aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' });
});
});
describe('stringifyIni', {}, () => {
it('serializes a single section', {}, () => {
const result = stringifyIni({ dev: { key: 'val' } });
expect(result).toBe('[dev]\nkey = val\n');
});
it('serializes multiple sections with blank line separator', {}, () => {
const result = stringifyIni({ dev: { a: '1' }, prod: { b: '2' } });
expect(result).toBe('[dev]\na = 1\n\n[prod]\nb = 2\n');
});
it('round-trips through parseIni', {}, () => {
const data = { dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }, 'profile prod': { region: 'us-west-2' } };
const roundTripped = parseIni(stringifyIni(data));
expect(roundTripped).toEqual(data);
});
it('handles empty data object', {}, () => {
const result = stringifyIni({});
expect(result).toBe('\n');
expect(parseIni(result)).toEqual({});
});
it('handles section with no keys', {}, () => {
const result = stringifyIni({ dev: {} });
expect(result).toBe('[dev]\n');
});
});
describe('validateProfileName', {}, () => {
it('accepts valid profile names', {}, () => {
expect(() => validateProfileName('dev')).not.toThrow();
expect(() => validateProfileName('production')).not.toThrow();
expect(() => validateProfileName('my-profile-123')).not.toThrow();
expect(() => validateProfileName('default')).not.toThrow();
});
it('rejects empty profile names', {}, () => {
expect(() => validateProfileName('')).toThrow('aws-profile must not be empty');
expect(() => validateProfileName(' ')).toThrow('aws-profile must not be empty');
});
it('rejects profile names with whitespace', {}, () => {
expect(() => validateProfileName('my profile')).toThrow('aws-profile must not contain whitespace');
expect(() => validateProfileName('dev\ntest')).toThrow('aws-profile must not contain whitespace');
expect(() => validateProfileName('prod\tenv')).toThrow('aws-profile must not contain whitespace');
});
it('rejects profile names with brackets', {}, () => {
expect(() => validateProfileName('dev[test]')).toThrow('aws-profile must not contain brackets');
expect(() => validateProfileName('[profile]')).toThrow('aws-profile must not contain brackets');
});
it('rejects profile names with path separators', {}, () => {
expect(() => validateProfileName('dev/test')).toThrow('aws-profile must not contain path separators');
expect(() => validateProfileName('dev\\test')).toThrow('aws-profile must not contain path separators');
expect(() => validateProfileName('../etc/passwd')).toThrow('aws-profile must not contain path separators');
});
});
describe('getProfileFilePaths', {}, () => {
it('returns default paths when env vars not set', {}, () => {
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
delete process.env.AWS_CONFIG_FILE;
const paths = getProfileFilePaths();
expect(paths.credentials).toMatch(/\.aws[/\\]credentials$/);
expect(paths.config).toMatch(/\.aws[/\\]config$/);
});
it('respects AWS_SHARED_CREDENTIALS_FILE env var', {}, () => {
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/path/credentials';
process.env.AWS_CONFIG_FILE = '/custom/path/config';
const paths = getProfileFilePaths();
expect(paths.credentials).toBe('/custom/path/credentials');
expect(paths.config).toBe('/custom/path/config');
});
});
describe('ensureAwsDirectoryExists', {}, () => {
it('creates directory if it does not exist', {}, () => {
const filePath = '/home/runner/.aws/credentials';
ensureAwsDirectoryExists(filePath);
expect(fs.existsSync('/home/runner/.aws')).toBe(true);
});
it('does not error if directory already exists', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
expect(() => ensureAwsDirectoryExists(filePath)).not.toThrow();
});
it('creates nested directories', {}, () => {
const filePath = '/home/runner/custom/path/.aws/credentials';
ensureAwsDirectoryExists(filePath);
expect(fs.existsSync('/home/runner/custom/path/.aws')).toBe(true);
});
});
describe('mergeProfileSection', {}, () => {
it('creates new file with profile section', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}, false);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev).toBeDefined();
expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
expect(parsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
});
it('merges with existing profiles', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'devSecretKey',
}, false);
// Add second profile
mergeProfileSection(filePath, 'prod', {
aws_access_key_id: 'AKIAPRODEXAMPLE',
aws_secret_access_key: 'prodSecretKey',
}, false);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev).toBeDefined();
expect(parsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
expect(parsed.prod).toBeDefined();
expect(parsed.prod.aws_access_key_id).toBe('AKIAPRODEXAMPLE');
});
it('overwrites existing profile with same name', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
aws_session_token: 'oldSessionToken'
}, false);
// Overwrite with new credentials
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'newSessionToken',
}, true);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev.aws_access_key_id).toBe('NEW_KEY');
expect(parsed.dev.aws_secret_access_key).toBe('newSecretKey');
expect(parsed.dev.aws_session_token).toBe('newSessionToken');
});
it('overwriting a profile removes stale keys', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create profile with session token
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
aws_session_token: 'old-token',
}, false);
// Overwrite without session token
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA2',
aws_secret_access_key: 'secret2',
}, true);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev.aws_access_key_id).toBe('AKIA2');
expect(parsed.dev.aws_secret_access_key).toBe('secret2');
expect(parsed.dev.aws_session_token).toBeUndefined();
});
it('handles empty existing file', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
fs.writeFileSync(filePath, '', { mode: 0o600 });
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
}, false);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev.aws_access_key_id).toBe('AKIA');
expect(parsed.dev.aws_secret_access_key).toBe('secret');
});
it('errors if profile name already exists but overwrite flag is false', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
}, false);
// Overwrite with new credentials
expect(() => mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'sessionToken',
}, false)).toThrow(`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.dev.aws_access_key_id).toBe('OLD_KEY');
expect(parsed.dev.aws_secret_access_key).toBe('oldSecretKey');
expect(parsed.dev.aws_session_token).toBeUndefined();
});
});
describe('writeProfileFiles', {}, () => {
beforeEach(() => {
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
delete process.env.AWS_CONFIG_FILE;
});
it('writes credentials and config for new profile', {}, () => {
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
},
'us-east-1',
false,
);
// Check credentials file
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const credParsed = parseIni(credContent);
expect(credParsed.dev).toBeDefined();
expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
expect(credParsed.dev.aws_session_token).toBe('FwoGZXIvYXdzEBYaDEXAMPLE');
// Check config file
const configPath = getProfileFilePaths().config;
const configContent = fs.readFileSync(configPath, 'utf-8');
const configParsed = parseIni(configContent);
expect(configParsed['profile dev']).toBeDefined();
expect(configParsed['profile dev'].region).toBe('us-east-1');
});
it('uses correct section naming for default profile', {}, () => {
writeProfileFiles(
'default',
{
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
},
'us-west-2',
false,
);
// Check credentials file uses [default]
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const credParsed = parseIni(credContent);
expect(credParsed.default).toBeDefined();
expect(credParsed['profile default']).toBeUndefined();
// Check config file uses [default] (not [profile default])
const configPath = getProfileFilePaths().config;
const configContent = fs.readFileSync(configPath, 'utf-8');
const configParsed = parseIni(configContent);
expect(configParsed.default).toBeDefined();
expect(configParsed['profile default']).toBeUndefined();
});
it('supports multiple profiles', {}, () => {
// Write first profile
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIADEV',
SecretAccessKey: 'devSecret',
},
'us-east-1',
false,
);
// Write second profile
writeProfileFiles(
'prod',
{
AccessKeyId: 'AKIAPROD',
SecretAccessKey: 'prodSecret',
SessionToken: 'prodToken',
},
'us-west-2',
false,
);
// Verify both profiles exist
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const credParsed = parseIni(credContent);
expect(credParsed.dev).toBeDefined();
expect(credParsed.prod).toBeDefined();
expect(credParsed.dev.aws_access_key_id).toBe('AKIADEV');
expect(credParsed.prod.aws_access_key_id).toBe('AKIAPROD');
});
it('handles credentials without session token', {}, () => {
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
},
'us-east-1',
false,
);
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const credParsed = parseIni(credContent);
expect(credParsed.dev.aws_access_key_id).toBe('AKIAIOSFODNN7EXAMPLE');
expect(credParsed.dev.aws_secret_access_key).toBe('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
expect(credParsed.dev.aws_session_token).toBeUndefined();
});
it('throws error for invalid profile name', {}, () => {
expect(() =>
writeProfileFiles(
'invalid profile',
{
AccessKeyId: 'AKIA',
SecretAccessKey: 'secret',
},
'us-east-1',
false,
),
).toThrow('Failed to write AWS profile');
expect(() =>
writeProfileFiles(
'invalid profile',
{
AccessKeyId: 'AKIA',
SecretAccessKey: 'secret',
},
'us-east-1',
false
),
).toThrow('whitespace');
});
it('respects custom file paths from env vars', {}, () => {
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom/credentials';
process.env.AWS_CONFIG_FILE = '/custom/config';
fs.mkdirSync('/custom', { recursive: true });
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIA',
SecretAccessKey: 'secret',
},
'us-east-1',
false
);
expect(fs.existsSync('/custom/credentials')).toBe(true);
expect(fs.existsSync('/custom/config')).toBe(true);
});
it('logs info messages', {}, () => {
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIA',
SecretAccessKey: 'secret',
},
'us-east-1',
false
);
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
expect(core.info).toHaveBeenCalledWith('Writing config to profile: dev');
expect(core.info).toHaveBeenCalledWith('✓ Successfully configured AWS profile: dev');
});
it('preserves pre-existing unrelated profiles in credentials file', {}, () => {
const credsPath = getProfileFilePaths().credentials;
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
fs.writeFileSync(
credsPath,
'[personal]\naws_access_key_id=AKIAPERSONAL\naws_secret_access_key=personalSecret\naws_session_token=personalToken\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false,
);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
// Pre-existing profile must be fully intact
expect(parsed.personal).toEqual({
aws_access_key_id: 'AKIAPERSONAL',
aws_secret_access_key: 'personalSecret',
aws_session_token: 'personalToken',
});
// New profile also present
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
});
it('preserves pre-existing config with extra keys', {}, () => {
const configPath = getProfileFilePaths().config;
fs.mkdirSync(require('node:path').dirname(configPath), { recursive: true });
fs.writeFileSync(
configPath,
'[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = parseIni(content);
expect(parsed['profile personal']).toEqual({
region: 'eu-west-1',
output: 'json',
cli_pager: '',
});
expect(parsed['profile dev'].region).toBe('us-east-1');
});
it('preserves pre-existing default profile when writing a named profile', {}, () => {
const credsPath = getProfileFilePaths().credentials;
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
fs.writeFileSync(
credsPath,
'[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-west-2',
false
);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
expect(parsed.default).toEqual({
aws_access_key_id: 'AKIADEFAULT',
aws_secret_access_key: 'defaultSecret',
});
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
});
it('comments in pre-existing files are stripped on round-trip', {}, () => {
const credsPath = getProfileFilePaths().credentials;
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
fs.writeFileSync(
credsPath,
'# My important comment\n[personal]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
const content = fs.readFileSync(credsPath, 'utf-8') as string;
// Comment is lost (known trade-off), but profile data is preserved
expect(content).not.toContain('# My important comment');
const parsed = parseIni(content);
expect(parsed.personal.aws_access_key_id).toBe('AKIA');
expect(parsed.dev.aws_access_key_id).toBe('AKIADEV');
});
it('writes empty section when credentials object has no keys', {}, () => {
writeProfileFiles('dev', {}, 'us-east-1', false);
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const credParsed = parseIni(credContent);
// Section exists but has no credential keys
expect(credParsed.dev).toEqual({});
// Config still gets region
const configPath = getProfileFilePaths().config;
const configContent = fs.readFileSync(configPath, 'utf-8');
const configParsed = parseIni(configContent);
expect(configParsed['profile dev'].region).toBe('us-east-1');
});
it('resolves credentials and config paths independently from env vars', {}, () => {
process.env.AWS_SHARED_CREDENTIALS_FILE = '/custom-creds/credentials';
// AWS_CONFIG_FILE is NOT set — should use default path
fs.mkdirSync('/custom-creds', { recursive: true });
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
expect(fs.existsSync('/custom-creds/credentials')).toBe(true);
// Config file should be at the default path (under homedir)
const defaultConfigPath = require('node:path').join(require('node:os').homedir(), '.aws', 'config');
expect(fs.existsSync(defaultConfigPath)).toBe(true);
});
it('produces AWS CLI-compatible INI output (golden file)', {}, () => {
writeProfileFiles(
'dev',
{
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
},
'us-east-1',
false
);
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const configPath = getProfileFilePaths().config;
const configContent = fs.readFileSync(configPath, 'utf-8');
// Verify exact byte-for-byte format matching AWS CLI style:
// - [section] header on its own line
// - key = value with spaces around =
// - LF line endings, trailing newline
expect(credContent).toBe(
'[dev]\n' +
'aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n' +
'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' +
'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n',
);
});
it('golden file for multi-profile output', {}, () => {
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
writeProfileFiles(
'prod',
{ AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken' },
'us-west-2',
false
);
const credsPath = getProfileFilePaths().credentials;
const credContent = fs.readFileSync(credsPath, 'utf-8');
const configPath = getProfileFilePaths().config;
const configContent = fs.readFileSync(configPath, 'utf-8');
expect(credContent).toBe(
'[dev]\n' +
'aws_access_key_id = AKIADEV\n' +
'aws_secret_access_key = devSecret\n' +
'\n' +
'[prod]\n' +
'aws_access_key_id = AKIAPROD\n' +
'aws_secret_access_key = prodSecret\n' +
'aws_session_token = prodToken\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n' +
'\n' +
'[profile prod]\n' +
'region = us-west-2\n',
);
});
});
});

View file

@ -23,7 +23,8 @@
"target": "ES2020", "target": "ES2020",
"noErrorTruncation": true, "noErrorTruncation": true,
"esModuleInterop": true, "esModuleInterop": true,
"rootDir": "src" "rootDir": "src",
"skipLibCheck": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["test/**/*.ts"] "exclude": ["test/**/*.ts"]