Skip to content

Commit f8a49d1

Browse files
hyunsiesclaude
andcommitted
fix: F034, F031, F017, F026, F041 — validation and lifecycle UX (v21.0.4)
F034: Agreement dates validated as real calendar dates (catches Feb 31). F031: MPE ID length check added (1-44 chars, matching Lambda name limit). F026: update.sh detects single-account stacks and provides SSM update instructions instead of misleading "StackSet not found" error. F017: update.sh scope edit validated for JSON correctness after sed. F041: deploy.sh warns before updating existing stacks about CFN resource deletion behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7c36e17 commit f8a49d1

2 files changed

Lines changed: 53 additions & 21 deletions

File tree

configurator.html

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,15 +1141,22 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
11411141

11421142
<script>
11431143
// Template version — single source of truth for the SemVer constant.
1144-
// Must match `TEMPLATE_VERSION = 'v21.0.3'` in map2-auto-tagger-optimized.yaml (sync-check enforces this).
1145-
const TEMPLATE_VERSION = 'v21.0.3';
1144+
// Must match `TEMPLATE_VERSION = 'v21.0.4'` in map2-auto-tagger-optimized.yaml (sync-check enforces this).
1145+
const TEMPLATE_VERSION = 'v21.0.4';
11461146

11471147
// Version history surfaced in the Update flow. Bullets are intentionally English-only —
11481148
// translating release notes across 7 languages for every PR is unsustainable. Labels
11491149
// (titles, buttons) go through i18n; change bullets stay in source form.
11501150
// Tags: bugfix, coverage, breaking, security, perf, other.
11511151
// sync-check.py enforces that the newest entry's version matches TEMPLATE_VERSION.
11521152
const VERSION_HISTORY = [
1153+
{
1154+
version: 'v21.0.4',
1155+
date: '2026-04-28',
1156+
changes: [
1157+
{ tag: 'bugfix', text: 'Configurator validation: agreement dates now validated as real calendar dates (F034). MPE ID length check added — 1-44 chars (F031). update.sh detects single-account stacks and provides SSM instructions (F026). update.sh scope edit validated for JSON correctness (F017). deploy.sh warns before updating existing stacks (F041).' },
1158+
],
1159+
},
11531160
{
11541161
version: 'v21.0.3',
11551162
date: '2026-04-28',
@@ -1332,7 +1339,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
13321339
changes: [
13331340
{ tag: 'bugfix', text: 'Editor-mode update.sh generator: every aws CLI call now has explicit --region "$REGION". Customers running the script with AWS_DEFAULT_REGION unset or set to a different region no longer deploy into the wrong region or fail on cross-region StackSet lookup.' },
13341341
{ tag: 'bugfix', text: 'Editor-mode update.sh now reads the current template via describe-stack-set --query StackSet.TemplateBody instead of downloading the deprecated S3 staging copy (auto-map-tagger-<acct>/map-auto-tagger-accounts-<mpe>.yaml). The staging object was only created during the initial multi-account deploy and could be missing / stale / garbage-collected — this eliminates the S3 dependency entirely.' },
1335-
{ tag: 'bugfix', text: 'Upgrade-mode compare_versions now strict-validates three-part numeric SemVer (vMAJOR.MINOR.PATCH) and returns "error" on unparseable input instead of silently falling through to "patch". Prior behavior misclassified e.g. v21.0.3-rc1 → v20.3.0 as patch; upgrade_one caller now fails closed and directs the operator to --force.' },
1342+
{ tag: 'bugfix', text: 'Upgrade-mode compare_versions now strict-validates three-part numeric SemVer (vMAJOR.MINOR.PATCH) and returns "error" on unparseable input instead of silently falling through to "patch". Prior behavior misclassified e.g. v21.0.4-rc1 → v20.3.0 as patch; upgrade_one caller now fails closed and directs the operator to --force.' },
13361343
],
13371344
},
13381345
{
@@ -1575,7 +1582,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
15751582
if (action === 'add') rows.push([t('ui_backfill_title'), backfill ? t('rv_backfill_enabled') : t('rv_disabled')]);
15761583
// Safe DOM construction: v may contain user-controlled values
15771584
// (customer name, account IDs) so use textContent rather than
1578-
// innerHTML template-literal interpolation. (§1.94, v21.0.3)
1585+
// innerHTML template-literal interpolation. (§1.94, v21.0.4)
15791586
table.replaceChildren();
15801587
rows.forEach(([k, v], i) => {
15811588
const tr = document.createElement('tr');
@@ -1682,7 +1689,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
16821689
];
16831690
// Safe DOM construction: v may contain user-controlled values
16841691
// (MPE IDs) so use textContent rather than innerHTML template-
1685-
// literal interpolation. (§1.94, v21.0.3)
1692+
// literal interpolation. (§1.94, v21.0.4)
16861693
table.replaceChildren();
16871694
rows.forEach(([k, v], i) => {
16881695
const tr = document.createElement('tr');
@@ -1831,7 +1838,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
18311838
// (MPE IDs, region strings) so use textContent rather than
18321839
// innerHTML template-literal interpolation. Preserves the
18331840
// delete-review styles (padding 10px 12px, v-cell font-weight
1834-
// 600). (§1.94, v21.0.3)
1841+
// 600). (§1.94, v21.0.4)
18351842
const deleteTable = document.getElementById('delete-reviewTable');
18361843
deleteTable.replaceChildren();
18371844
rows.forEach(([k, v], i) => {
@@ -2221,7 +2228,17 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
22212228
# ── Step 1: Verify StackSet exists ────────────
22222229
echo "Step 1: Checking existing deployment..."
22232230
if ! aws cloudformation describe-stack-set --region "$REGION" --stack-set-name "$STACKSET_NAME" > /dev/null 2>&1; then
2224-
echo " ❌ StackSet '$STACKSET_NAME' not found in region $REGION."
2231+
SINGLE_STATUS=\$(aws cloudformation describe-stacks --region "$REGION" --stack-name "$STACKSET_NAME" --query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NONE")
2232+
if [ "$SINGLE_STATUS" != "NONE" ] && [ "$SINGLE_STATUS" != "None" ]; then
2233+
echo " ⚠️ Single-account stack '$STACKSET_NAME' found (status: $SINGLE_STATUS)."
2234+
echo " This update tool targets multi-account StackSet deployments."
2235+
echo " For single-account scope changes, update SSM directly:"
2236+
echo " aws ssm get-parameter --name '/auto-map-tagger/$MPE/config' --region $REGION"
2237+
echo " # Edit the JSON, then:"
2238+
echo " aws ssm put-parameter --name '/auto-map-tagger/$MPE/config' --value '<new-json>' --type String --overwrite --region $REGION"
2239+
exit 1
2240+
fi
2241+
echo " ❌ No deployment '$STACKSET_NAME' found in region $REGION."
22252242
echo " Run the initial deploy.sh from the configurator first."
22262243
exit 1
22272244
fi
@@ -2258,6 +2275,12 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
22582275
rm -f "$TEMPLATE.bak"
22592276

22602277
NEW_SCOPE=$(grep -o '"scoped_account_ids": \\[[^]]*\\]' "$TEMPLATE" | head -1 | sed 's/"scoped_account_ids": //')
2278+
if ! python3 -c "import json; json.loads('$NEW_SCOPE')" 2>/dev/null; then
2279+
echo " ❌ Scope update produced invalid JSON: $NEW_SCOPE"
2280+
echo " Update SSM parameter directly instead."
2281+
rm -f "$TEMPLATE" "$TEMPLATE.bak"
2282+
exit 1
2283+
fi
22612284
sed -i.bak "s|ScopedAccounts: .*|ScopedAccounts: '$NEW_SCOPE'|" "$TEMPLATE"
22622285
rm -f "$TEMPLATE.bak"
22632286

@@ -2987,12 +3010,19 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
29873010

29883011

29893012
// --- Validation ---
3013+
function isValidCalendarDate(dateStr) {
3014+
if (!dateStr) return false;
3015+
const [y, m, d] = dateStr.split('-').map(Number);
3016+
const dt = new Date(y, m - 1, d);
3017+
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
3018+
}
3019+
29903020
function validate() {
29913021
let valid = true;
29923022
const mpeId = document.getElementById('mpeId').value.trim();
29933023
const date = document.getElementById('agreementDate').value;
29943024

2995-
if (!/^(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]{10}$/.test(mpeId)) {
3025+
if (!/^[A-Z0-9]+$/.test(mpeId) || mpeId.length < 1 || mpeId.length > 44) {
29963026
document.getElementById('mpeId').classList.add('error');
29973027
document.getElementById('mpeId-error').style.display = 'block';
29983028
valid = false;
@@ -3001,7 +3031,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
30013031
document.getElementById('mpeId-error').style.display = 'none';
30023032
}
30033033

3004-
if (!date) {
3034+
if (!date || !isValidCalendarDate(date)) {
30053035
document.getElementById('agreementDate').classList.add('error');
30063036
document.getElementById('agreementDate-error').style.display = 'block';
30073037
valid = false;
@@ -3011,7 +3041,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
30113041
}
30123042

30133043
const endDate = document.getElementById('agreementEndDate').value;
3014-
if (!endDate || endDate <= date) {
3044+
if (!endDate || !isValidCalendarDate(endDate) || endDate <= date) {
30153045
document.getElementById('agreementEndDate').classList.add('error');
30163046
document.getElementById('agreementEndDate-error').style.display = 'block';
30173047
valid = false;
@@ -3171,7 +3201,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
31713201
// (customer name, email, account IDs, VPC IDs) so use textContent
31723202
// rather than innerHTML template-literal interpolation. Preserves
31733203
// the main-deploy styles (width 200px, v-cell monospace 13px).
3174-
// (§1.94, v21.0.3)
3204+
// (§1.94, v21.0.4)
31753205
table.replaceChildren();
31763206
rows.forEach(([k, v]) => {
31773207
const tr = document.createElement('tr');
@@ -7989,7 +8019,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
79898019
# — most other AWS services throw 'ThrottlingException' (with "ing").
79908020
# Both variants must be enumerated, alongside the generic rate-limit
79918021
# codes. Hoisted to module scope so _retry_throttles() and the RGTA
7992-
# retry loop below share the same constant (v21.0.3, §1.81/§1.92).
8022+
# retry loop below share the same constant (v21.0.4, §1.81/§1.92).
79938023
THROTTLE_CODES = {'ThrottlingException', 'ThrottledException', 'RequestLimitExceeded', 'TooManyRequestsException'}
79948024

79958025
def _retry_throttles(fn):
@@ -8000,7 +8030,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
80008030
to the outer _process_event classifier on the first attempt. Absorbs
80018031
short throttles inside the invocation so we don't burn one of the
80028032
5 SQS redeliveries (180s VT each) on a recoverable rate limit
8003-
(v21.0.3, §1.81/§1.92).
8033+
(v21.0.4, §1.81/§1.92).
80048034
"""
80058035
for attempt in range(4):
80068036
try:
@@ -8026,7 +8056,7 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
80268056
# Each native tag call is wrapped in _retry_throttles() so short
80278057
# throttle bursts are absorbed in-invocation (up to 4 attempts,
80288058
# ~15s total worst case) rather than burning SQS redelivery
8029-
# budget. (§1.81/§1.92, v21.0.3)
8059+
# budget. (§1.81/§1.92, v21.0.4)
80308060
if ':s3:::' in arn:
80318061
s3 = get_service_client('s3')
80328062
bucket = arn.split(':::')[1]
@@ -9693,6 +9723,8 @@ <h3 style="margin:20px 0 8px;font-size:14px;" data-i18n="ui_editor_script_previe
96939723
--region "\$REGION" > /dev/null
96949724
aws cloudformation wait stack-create-complete --stack-name "\$STACK_NAME" --region "\$REGION"
96959725
else
9726+
echo " ⚠️ Stack \$STACK_NAME already exists (status: \$STACK_STATUS)."
9727+
echo " Updating in-place. Resources not in the new template will be deleted by CFN."
96969728
UPDATE_OUT=\$(aws cloudformation update-stack \\
96979729
--stack-name "\$STACK_NAME" \\
96989730
\$TEMPLATE_REF \\

map2-auto-tagger-optimized.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
AWSTemplateFormatVersion: '2010-09-09'
55
Description: >
6-
MAP 2.0 Auto-Tagger v21.0.3 - Auto-tags AWS resources with map-migrated for MAP 2.0
6+
MAP 2.0 Auto-Tagger v21.0.4 - Auto-tags AWS resources with map-migrated for MAP 2.0
77
credit eligibility. 190+ resource types. Deploy once; tagging happens within 60-90 s
88
of resource creation. Daily reconciliation Lambda (RGTA-based) catches any tags the
99
live Lambda missed. Three-path error classifier + TagFailureByClass CloudWatch metric
@@ -65,7 +65,7 @@ Resources:
6565
Name: !Sub '/auto-map-tagger/${MpeId}/version'
6666
Type: String
6767
Description: MAP 2.0 Auto-Tagger template version pinned at deploy time
68-
Value: v21.0.3
68+
Value: v21.0.4
6969

7070
# SSM Parameter Store - single source of truth for config.
7171
# Tier: Intelligent-Tiering leaves the parameter in the free Standard tier
@@ -630,7 +630,7 @@ Resources:
630630
# Template version pinned at deploy time. Surfaced in CloudWatch Logs on
631631
# every cold start so ops can trace which version processed an event
632632
# without reading the CFN stack or SSM parameter.
633-
TEMPLATE_VERSION = 'v21.0.3'
633+
TEMPLATE_VERSION = 'v21.0.4'
634634
print(f'auto-map-tagger {TEMPLATE_VERSION} cold start')
635635
636636
ssm = boto3.client('ssm')
@@ -2130,7 +2130,7 @@ Resources:
21302130
# — most other AWS services throw 'ThrottlingException' (with "ing").
21312131
# Both variants must be enumerated, alongside the generic rate-limit
21322132
# codes. Hoisted to module scope so _retry_throttles() and the RGTA
2133-
# retry loop below share the same constant (v21.0.3, §1.81/§1.92).
2133+
# retry loop below share the same constant (v21.0.4, §1.81/§1.92).
21342134
THROTTLE_CODES = {'ThrottlingException', 'ThrottledException', 'RequestLimitExceeded', 'TooManyRequestsException'}
21352135
21362136
def _retry_throttles(fn):
@@ -2141,7 +2141,7 @@ Resources:
21412141
to the outer _process_event classifier on the first attempt. Absorbs
21422142
short throttles inside the invocation so we don't burn one of the
21432143
5 SQS redeliveries (180s VT each) on a recoverable rate limit
2144-
(v21.0.3, §1.81/§1.92).
2144+
(v21.0.4, §1.81/§1.92).
21452145
"""
21462146
for attempt in range(4):
21472147
try:
@@ -2167,7 +2167,7 @@ Resources:
21672167
# Each native tag call is wrapped in _retry_throttles() so short
21682168
# throttle bursts are absorbed in-invocation (up to 4 attempts,
21692169
# ~15s total worst case) rather than burning SQS redelivery
2170-
# budget. (§1.81/§1.92, v21.0.3)
2170+
# budget. (§1.81/§1.92, v21.0.4)
21712171
if ':s3:::' in arn:
21722172
s3 = get_service_client('s3')
21732173
bucket = arn.split(':::')[1]
@@ -3157,7 +3157,7 @@ Outputs:
31573157
Value: !Ref MapConfig
31583158
TemplateVersion:
31593159
Description: MAP 2.0 Auto-Tagger template version (pinned at deploy time)
3160-
Value: v21.0.3
3160+
Value: v21.0.4
31613161
AlertTopicArn:
31623162
Description: SNS topic for tagger alerts - subscribe your email here
31633163
Value: !Ref AlertTopic

0 commit comments

Comments
 (0)