* 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>
214 lines
6.6 KiB
TypeScript
214 lines
6.6 KiB
TypeScript
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)}`,
|
|
);
|
|
}
|
|
}
|