Skip to content

Commit 8302fd0

Browse files
committed
fix(sync): migrate to using epoch time for sync timestamp
1 parent c637471 commit 8302fd0

2 files changed

Lines changed: 194 additions & 166 deletions

File tree

samcli/commands/sync/sync_context.py

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from dataclasses import dataclass
88
from datetime import datetime, timezone
99
from pathlib import Path
10-
from typing import Dict, Optional, cast
10+
from typing import Dict, Optional, Union, cast
1111

1212
import tomlkit
1313
from tomlkit.items import Item
@@ -65,46 +65,45 @@ def update_infra_sync_time(self) -> None:
6565
self.latest_infra_sync_time = datetime.now(timezone.utc)
6666

6767

68-
def _parse_datetime_from_toml(datetime_str: str) -> datetime:
68+
def _parse_time_from_toml(time_value: Union[str, int, float, datetime]) -> Optional[datetime]:
6969
"""
70-
Parse datetime string from TOML file and ensure it's timezone-aware UTC.
70+
Parse time from TOML file - supports both epoch and ISO format.
7171
72-
Handles three formats:
73-
1. With explicit timezone: "2024-05-08T15:16:43+00:00"
74-
2. Without timezone: "2024-05-08T15:16:43"
75-
3. With Z suffix (Zulu time): "2024-05-08T15:16:43Z"
76-
77-
Python 3.9/3.10 don't support 'Z' suffix in fromisoformat(), so we convert it.
72+
Handles legacy ISO format strings as sam migrat to using epoch timestamps for backward compatibility.
73+
New writes always use epoch format.
7874
7975
Parameters
8076
----------
81-
datetime_str: str
82-
ISO format datetime string from TOML file
77+
time_value: Union[str, int, float, datetime]
78+
Either epoch timestamp (new format), ISO format string (legacy), or datetime object
8379
8480
Returns
8581
-------
86-
datetime
87-
Timezone-aware datetime in UTC
88-
89-
Raises
90-
------
91-
ValueError
92-
If datetime_str is not a valid ISO format datetime string
82+
Optional[datetime]
83+
Timezone-aware datetime in UTC, or None if parsing fails
9384
"""
9485
try:
95-
# Handle 'Z' suffix for UTC timezone (Python 3.9/3.10 compatibility)
96-
if datetime_str.endswith("Z"):
97-
datetime_str = datetime_str[:-1] + "+00:00"
86+
if isinstance(time_value, datetime):
87+
return time_value if time_value.tzinfo else time_value.replace(tzinfo=timezone.utc)
9888

99-
parsed_datetime = datetime.fromisoformat(datetime_str)
89+
if isinstance(time_value, (int, float)):
90+
return datetime.fromtimestamp(time_value, tz=timezone.utc)
10091

101-
# Ensure timezone-aware (handles old sync.toml files without timezone)
102-
if parsed_datetime.tzinfo is None:
103-
parsed_datetime = parsed_datetime.replace(tzinfo=timezone.utc)
92+
if isinstance(time_value, str):
93+
if time_value.endswith("Z"):
94+
time_value = time_value[:-1] + "+00:00"
95+
parsed = datetime.fromisoformat(time_value)
96+
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
10497

105-
return parsed_datetime
106-
except (ValueError, AttributeError) as e:
107-
raise ValueError(f"Invalid datetime format in sync.toml: '{datetime_str}'") from e
98+
LOG.warning("Invalid time format in sync.toml: %s. Triggering full CloudFormation deployment.", time_value)
99+
return None
100+
except (ValueError, OSError) as e:
101+
LOG.warning(
102+
"Failed to parse timestamp from sync.toml: %s. Triggering full CloudFormation deployment. Error: %s",
103+
time_value,
104+
e,
105+
)
106+
return None
108107

109108

110109
def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:
@@ -124,7 +123,7 @@ def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:
124123
sync_state_toml_table = tomlkit.table()
125124
sync_state_toml_table[DEPENDENCY_LAYER] = sync_state.dependency_layer
126125
if sync_state.latest_infra_sync_time:
127-
sync_state_toml_table[LATEST_INFRA_SYNC_TIME] = sync_state.latest_infra_sync_time.isoformat()
126+
sync_state_toml_table[LATEST_INFRA_SYNC_TIME] = sync_state.latest_infra_sync_time.timestamp()
128127

129128
resource_sync_states_toml_table = tomlkit.table()
130129
for resource_id in sync_state.resource_sync_states:
@@ -133,7 +132,7 @@ def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:
133132
resource_sync_state_toml_table = tomlkit.table()
134133

135134
resource_sync_state_toml_table[HASH] = resource_sync_state.hash_value
136-
resource_sync_state_toml_table[SYNC_TIME] = resource_sync_state.sync_time.isoformat()
135+
resource_sync_state_toml_table[SYNC_TIME] = resource_sync_state.sync_time.timestamp()
137136

138137
# For Nested stack resources, replace "/" with "-"
139138
resource_id_toml = resource_id.replace("/", "-")
@@ -173,7 +172,10 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
173172
resource_sync_state_toml_table = resource_sync_states_toml_table.get(resource_id)
174173
sync_time_str = resource_sync_state_toml_table.get(SYNC_TIME)
175174
# Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
176-
sync_time = _parse_datetime_from_toml(sync_time_str)
175+
sync_time = _parse_time_from_toml(sync_time_str)
176+
if sync_time is None:
177+
# Skip this resource if timestamp is invalid - resource will be re-synced on next sam sync
178+
continue
177179
resource_sync_state = ResourceSyncState(
178180
resource_sync_state_toml_table.get(HASH),
179181
sync_time,
@@ -189,8 +191,7 @@ def _toml_document_to_sync_state(toml_document: Dict) -> Optional[SyncState]:
189191
dependency_layer = sync_state_toml_table.get(DEPENDENCY_LAYER)
190192
latest_infra_sync_time_str = sync_state_toml_table.get(LATEST_INFRA_SYNC_TIME)
191193
if latest_infra_sync_time_str:
192-
# Parse datetime and ensure it's timezone-aware UTC (consistent with how we write)
193-
latest_infra_sync_time = _parse_datetime_from_toml(str(latest_infra_sync_time_str))
194+
latest_infra_sync_time = _parse_time_from_toml(latest_infra_sync_time_str)
194195
sync_state = SyncState(dependency_layer, resource_sync_states, latest_infra_sync_time)
195196

196197
return sync_state

0 commit comments

Comments
 (0)