Skip to content

Commit fd201a2

Browse files
author
Abhimanyu Siwach
committed
feat: add version-aware AWS CLI guidance to credential error messages
Users who hit expired credential errors now see contextual guidance based on their AWS CLI version: - Not installed → install link + "aws login" - Installed but < 2.32.0 → upgrade link + "aws login" - >= 2.32.0 → "aws login" Preserves original error messages for invalid credentials (check env vars) and missing credentials (env vars fallback). Also updates stale "aws sso login" reference to "aws login" in pre-deploy identity flow.
1 parent 33523a6 commit fd201a2

File tree

5 files changed

+89
-9
lines changed

5 files changed

+89
-9
lines changed

src/cli/aws/account.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getAwsLoginGuidance } from '../external-requirements/checks';
12
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
23
import { fromEnv, fromNodeProviderChain } from '@aws-sdk/credential-providers';
34
import type { AwsCredentialIdentityProvider } from '@smithy/types';
@@ -46,9 +47,10 @@ export async function detectAccount(): Promise<string | null> {
4647
const code = (err as { name?: string })?.name ?? (err as { Code?: string })?.Code;
4748

4849
if (code === 'ExpiredTokenException' || code === 'ExpiredToken') {
50+
const guidance = await getAwsLoginGuidance();
4951
throw new AwsCredentialsError(
5052
'AWS credentials expired.',
51-
'AWS credentials expired.\n\nTo fix this:\n Run: aws login'
53+
`AWS credentials expired.\n\nTo fix this:\n ${guidance}`
5254
);
5355
}
5456

@@ -77,12 +79,10 @@ export async function detectAccount(): Promise<string | null> {
7779
export async function validateAwsCredentials(): Promise<void> {
7880
const account = await detectAccount();
7981
if (!account) {
82+
const guidance = await getAwsLoginGuidance();
8083
throw new AwsCredentialsError(
8184
'No AWS credentials configured.',
82-
'No AWS credentials configured.\n\n' +
83-
'To fix this:\n' +
84-
' 1. Run: aws login\n' +
85-
' 2. Or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables'
85+
`No AWS credentials configured.\n\nTo fix this:\n ${guidance}\n Or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables`
8686
);
8787
}
8888
}

src/cli/external-requirements/checks.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { checkSubprocess, isWindows, runSubprocessCapture } from '../../lib';
55
import type { AgentCoreProjectSpec, TargetLanguage } from '../../schema';
66
import { detectContainerRuntime } from './detect';
7-
import { NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions';
7+
import { AWS_CLI_MIN_VERSION, NODE_MIN_VERSION, formatSemVer, parseSemVer, semVerGte } from './versions';
88

99
/**
1010
* Result of a version check.
@@ -70,6 +70,73 @@ export async function checkUvVersion(): Promise<VersionCheckResult> {
7070
return { satisfied: true, current, required: 'any', binary: 'uv' };
7171
}
7272

73+
const AWS_CLI_INSTALL_URL = 'https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html';
74+
75+
/**
76+
* Extract version from `aws --version` output.
77+
* Expected format: "aws-cli/2.32.0 Python/3.11.6 Darwin/23.3.0 ..."
78+
*/
79+
function parseAwsCliVersion(output: string): string | null {
80+
const match = /aws-cli\/(\d+\.\d+\.\d+)/.exec(output.trim());
81+
return match?.[1] ?? null;
82+
}
83+
84+
/**
85+
* Check that AWS CLI meets minimum version requirement for `aws login`.
86+
*/
87+
export async function checkAwsCliVersion(): Promise<VersionCheckResult> {
88+
const required = formatSemVer(AWS_CLI_MIN_VERSION);
89+
90+
const result = await runSubprocessCapture('aws', ['--version']);
91+
if (result.code !== 0) {
92+
return { satisfied: false, current: null, required, binary: 'aws' };
93+
}
94+
95+
const versionStr = parseAwsCliVersion(result.stdout);
96+
if (!versionStr) {
97+
return { satisfied: false, current: null, required, binary: 'aws' };
98+
}
99+
100+
const current = parseSemVer(versionStr);
101+
if (!current) {
102+
return { satisfied: false, current: versionStr, required, binary: 'aws' };
103+
}
104+
105+
return {
106+
satisfied: semVerGte(current, AWS_CLI_MIN_VERSION),
107+
current: versionStr,
108+
required,
109+
binary: 'aws',
110+
};
111+
}
112+
113+
/** Cached result for getAwsLoginGuidance */
114+
let _awsLoginGuidance: string | null = null;
115+
116+
/**
117+
* Get version-aware guidance for authenticating with AWS.
118+
* Checks if AWS CLI is installed and whether it supports `aws login`.
119+
* Result is cached for the lifetime of the process.
120+
*/
121+
export async function getAwsLoginGuidance(): Promise<string> {
122+
if (_awsLoginGuidance) return _awsLoginGuidance;
123+
124+
const check = await checkAwsCliVersion();
125+
126+
if (check.current === null) {
127+
// AWS CLI not installed
128+
_awsLoginGuidance = `1. Install AWS CLI (v${formatSemVer(AWS_CLI_MIN_VERSION)}+): ${AWS_CLI_INSTALL_URL}\n 2. Run: aws login`;
129+
} else if (!check.satisfied) {
130+
// AWS CLI installed but too old
131+
_awsLoginGuidance = `1. Update AWS CLI from v${check.current} to v${formatSemVer(AWS_CLI_MIN_VERSION)}+: ${AWS_CLI_INSTALL_URL}\n 2. Run: aws login`;
132+
} else {
133+
// AWS CLI is new enough
134+
_awsLoginGuidance = 'Run: aws login';
135+
}
136+
137+
return _awsLoginGuidance;
138+
}
139+
73140
/**
74141
* Format a version check failure as a user-friendly error message.
75142
*/
@@ -234,7 +301,7 @@ export async function checkCreateDependencies(
234301
});
235302
if (!awsAvailable) {
236303
warnings.push(
237-
"'aws' CLI not found. Required for 'aws sso login' and profile configuration. Install from https://aws.amazon.com/cli/"
304+
`'aws' CLI not found. Required for 'aws login'. Install v${formatSemVer(AWS_CLI_MIN_VERSION)}+ from ${AWS_CLI_INSTALL_URL}`
238305
);
239306
}
240307

src/cli/external-requirements/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
export { parseSemVer, compareSemVer, semVerGte, formatSemVer, NODE_MIN_VERSION, type SemVer } from './versions';
1+
export {
2+
parseSemVer,
3+
compareSemVer,
4+
semVerGte,
5+
formatSemVer,
6+
NODE_MIN_VERSION,
7+
AWS_CLI_MIN_VERSION,
8+
type SemVer,
9+
} from './versions';
210

311
export {
412
checkNodeVersion,
513
checkUvVersion,
14+
checkAwsCliVersion,
15+
getAwsLoginGuidance,
616
formatVersionError,
717
requiresUv,
818
requiresContainerRuntime,

src/cli/external-requirements/versions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ export function formatSemVer(v: SemVer): string {
5959

6060
/** Minimum Node.js version required for CDK synth (ES2022 target) */
6161
export const NODE_MIN_VERSION: SemVer = { major: 18, minor: 0, patch: 0 };
62+
63+
/** Minimum AWS CLI version required for `aws login` */
64+
export const AWS_CLI_MIN_VERSION: SemVer = { major: 2, minor: 32, patch: 0 };

src/cli/operations/deploy/pre-deploy-identity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ async function setupApiKeyCredentialProvider(
171171
// Provide clearer error message for AWS credentials issues
172172
let errorMessage: string;
173173
if (isNoCredentialsError(error)) {
174-
errorMessage = 'AWS credentials not found. Run `aws sso login` or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY.';
174+
errorMessage = 'AWS credentials not found. Run `aws login` to authenticate.';
175175
} else {
176176
errorMessage = error instanceof Error ? error.message : String(error);
177177
}

0 commit comments

Comments
 (0)