Conversation
ApiGatewayDeployment.make_auto_deployable hashed the swagger body, OpenAPI version, custom domain and function names, but not the stage's Variables. A variables-only change therefore produced the same deployment logical id, so no new deployment resource was created. Two consequences: the new stage variables were never deployed, and because the UpdateStage call points the stage back at the deployment it already references, any deployment made outside SAM since the last SAM deployment was reverted. Stage variables are now part of the hash, but only when set, so templates without stage variables keep their existing deployment logical id. Fixes aws#3703
| # change reuses the existing deployment: the new variables are never deployed, and any | ||
| # deployment made outside SAM since the last SAM deployment is reverted. | ||
| # Only added when set, so templates without stage variables keep their existing hash. | ||
| stage_variables = getattr(stage, "Variables", None) |
There was a problem hiding this comment.
[BUG] The hash uses the raw, unresolved stage.Variables, so the fix misses the common case where stage variables are parameter-driven. SamApi.to_cloudformation (samtranslator/model/sam_resources.py:1845-1847) runs resolve_parameter_refs over BinaryMediaTypes, Domain and Auth before constructing the generator, but never over self.Variables — it is passed through verbatim (line 1857) and lands on the stage unchanged (api_generator.py:463, and visible in tests/translator/output/intrinsic_functions.json:265 where the variables stay as Fn::Sub/Fn::Join).
So for a template like:
Parameters:
EndpointUri:
Type: String
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Variables:
EndpointUri: !Ref EndpointUrithe hash input is always {"EndpointUri": {"Ref": "EndpointUri"}} regardless of the deployed parameter value. Deploying with a new value for EndpointUri still yields the same deployment logical id, which is exactly the failure described in #3703: the variable change is not deployed, and UpdateStage points the stage back at the SAM-known deployment. Domain does not have this problem precisely because it is resolved first.
Resolving the variables for hashing purposes would close the gap. Note that Domain is resolved by reassigning self.Domain, which also rewrites the emitted template; for Variables that would inline parameter values into the output AWS::ApiGateway::Stage, so prefer resolving into a separate value used only for the hash rather than mutating self.Variables.
Everything else checks out: stage.Variables is assigned before make_auto_deployable is called (api_generator.py:463 vs 470), that is the only call site, {} and None both fall through the if so no-variable templates keep their logical id, and the 24 fixture updates cover exactly the 8 input templates that set API stage variables across all three partitions, with the logical id, Description digest and Stage.DeploymentId ref updated consistently in each.
The deployment hash used the raw stage variables, so a parameter-driven
variable such as {"Ref": "EndpointUri"} hashed identically for every
parameter value. Deploying a new value produced the same deployment
logical id, leaving the reported failure in place for that case: the
variable was never deployed and UpdateStage pointed the stage back at
the deployment SAM already knew about.
Resolve the variables for hashing only. resolve_parameter_refs mutates
its argument and inlines values, and the emitted AWS::ApiGateway::Stage
must keep the customer's intrinsics, so a deep copy is resolved and
passed to make_auto_deployable rather than assigned back to
self.Variables the way Domain is.
Adds an end-to-end test asserting two parameter values yield different
deployment ids (and that the same value stays stable), plus a guard that
the emitted Stage still carries the unresolved Ref. Updates the
GetHtmlApi deployment id in the six explicit_api fixtures, whose input
template already sets a parameter-driven stage variable.
|
Good catch — this was a real gap and I have fixed it in I reproduced it before changing anything: transforming a template whose stage variable is What changed:
Tests added in
Fixtures: your point is already covered by the repo's own corpus — Verification: Two notes on judgement calls, in case you would rather they went the other way:
Also worth flagging what I did not change: |
|
Review nudge — this PR is now fully green and has no open feedback.
It fixes #3703 ( Verification: Two things worth a maintainer's eye, since they are judgement calls rather than mechanics:
Happy to rebase, split the fixture churn into its own commit, or adjust anything else that would make review easier. |
Which issue(s) does this change fix?
Fixes #3703
Why is this change necessary?
ApiGatewayDeployment.make_auto_deployablebuilds its hash from the swagger body, the OpenAPI version, the custom domain and the associated function names — but not from the stage'sVariables. SinceAWS::ApiGateway::Stagedoes carryVariables, a template change that only touches stage variables produces the same deployment logical id, so CloudFormation creates no newAWS::ApiGateway::Deployment.That has two effects, both described in the issue:
UpdateStagecall points the stage back at the deployment it already references. If any deployment was made outside SAM since the last SAM deployment, the stage silently reverts to the SAM-known deployment id — losing the out-of-band changes.How does it address the issue?
Stage variables are appended to
hash_input, following the existingif openapi_version:/if domain:pattern.They are appended only when set, which is the important part for backwards compatibility: templates that define no stage variables keep their current deployment logical id and see no churn.
sort_keys=Truekeeps the hash stable regardless of key order in the template.What side effects does this change have?
Templates that do set stage variables get a new deployment logical id once, so the next deploy creates a new deployment. That is the intended behaviour — it is exactly the deployment that was previously being skipped — but it is a one-time redeploy for those stacks, so calling it out explicitly.
The 24 updated files under
tests/translator/output/are that same effect in the checked-in expected outputs: only the deployment logical id and itsRestApi deployment id: <sha>description changed. Eight testcases are affected (explicit_api,explicit_api_openapi_3,api_with_openapi_definition_body_no_flag,api_with_swagger_and_openapi_with_auth,function_with_alias_and_event_sources,global_handle_path_level_parameter,globals_for_api,intrinsic_functions) across the three partition folders. I rewrote just those identifiers rather than regenerating the files, so the diff stays reviewable.Testing
Verified on
developatd24401d, Python 3.12:tests/translator/test_api_resource.py:{}, which is the no-churn guard.tests/translator/— 2423 passed, 0 failed (42 failed before the fixtures were refreshed).test_region_configuration, and need network/endpoint data), plus the 3 new passing tests. No regressions.ruff check(pinnedruff~=0.15.6) andblack --checkclean on both changed Python files;git diff --checkclean.One existing test needed a one-line change:
test_make_auto_deployable_with_swagger_dictusesstage = MagicMock(), whose.Variablesis a truthyMagicMockrather than the unset value a realApiGatewayStagehas, so it now setsstage.Variables = Noneto express "no stage variables".By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.