magic-pocket-cli 0.41.0__tar.gz → 0.41.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/PKG-INFO +2 -2
  2. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/deploy_cli.py +99 -8
  3. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/django_cli.py +69 -17
  4. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/cloudfront.py +11 -2
  5. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cloudfront.yaml +9 -0
  6. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pyproject.toml +2 -2
  7. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/.gitignore +0 -0
  8. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/__init__.py +0 -0
  9. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/__init__.py +0 -0
  10. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/aws_auth.py +0 -0
  11. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/backup_cli.py +0 -0
  12. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/build_cli.py +0 -0
  13. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/cleanup_deprecated_cli.py +0 -0
  14. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/cloudfront_cli.py +0 -0
  15. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  16. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  17. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/container_cli.py +0 -0
  18. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/destroy_cli.py +0 -0
  19. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/dsql_cli.py +0 -0
  20. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/image_cli.py +0 -0
  21. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/inbound_cli.py +0 -0
  22. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/interaction.py +0 -0
  23. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/main_cli.py +0 -0
  24. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/migrate_cli.py +0 -0
  25. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/neon_cli.py +0 -0
  26. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/permissions_cli.py +0 -0
  27. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/rds_cli.py +0 -0
  28. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/removed_flags.py +0 -0
  29. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/resource_helper.py +0 -0
  30. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/runtime_config_cli.py +0 -0
  31. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/s3_cli.py +0 -0
  32. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/status_cli.py +0 -0
  33. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/store_url_helper.py +0 -0
  34. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/tidb_cli.py +0 -0
  35. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/upstash_cli.py +0 -0
  36. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/url_helper.py +0 -0
  37. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/vpc_cli.py +0 -0
  38. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/cli/waf_cli.py +0 -0
  39. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/mediator.py +0 -0
  40. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/migrations.py +0 -0
  41. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/__init__.py +0 -0
  42. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/__init__.py +0 -0
  43. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/backup_common.py +0 -0
  44. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/backup_legacy.py +0 -0
  45. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  46. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
  47. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/context_check.py +0 -0
  48. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/depot.py +0 -0
  49. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/docker.py +0 -0
  50. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  51. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/cloudformation.py +0 -0
  52. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/ecr.py +0 -0
  53. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/efs.py +0 -0
  54. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/iam_roles.py +0 -0
  55. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/lambdahandler.py +0 -0
  56. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/poll.py +0 -0
  57. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/s3_utils.py +0 -0
  58. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/stack_backed.py +0 -0
  59. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/stage_roles.py +0 -0
  60. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/aws/state.py +0 -0
  61. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/backup.py +0 -0
  62. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/cloudfront_acm.py +0 -0
  63. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/cloudfront_keys.py +0 -0
  64. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/cloudfront_waf.py +0 -0
  65. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/container.py +0 -0
  66. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/dsql.py +0 -0
  67. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/inbound.py +0 -0
  68. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/inbound_domain.py +0 -0
  69. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/inbound_template.py +0 -0
  70. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/neon.py +0 -0
  71. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/rds.py +0 -0
  72. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/s3.py +0 -0
  73. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/sqs_alert.py +0 -0
  74. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/tidb.py +0 -0
  75. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/upstash.py +0 -0
  76. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/resources/vpc.py +0 -0
  77. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  78. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_basic_auth_prelude.js +0 -0
  79. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_cache_control.js +0 -0
  80. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_deploy_hash_strip.js +0 -0
  81. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
  82. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  83. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  84. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  85. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  86. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  87. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/container.yaml +0 -0
  88. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  89. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/init/django-dotenv.env +0 -0
  90. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/init/django-settings.py +0 -0
  91. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  92. {magic_pocket_cli-0.41.0 → magic_pocket_cli-0.41.1}/pocket_cli/templates/init/pocket_simple.toml +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: magic-pocket-cli
3
- Version: 0.41.0
3
+ Version: 0.41.1
4
4
  Summary: CLI and deploy tools for magic-pocket.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awscrt>=0.19.0
7
7
  Requires-Dist: click>=8.1.7
8
8
  Requires-Dist: deepdiff>=6.7.1
9
9
  Requires-Dist: jinja2>=3.1.3
10
- Requires-Dist: magic-pocket<0.42,>=0.41.0
10
+ Requires-Dist: magic-pocket<0.42,>=0.41.1
11
11
  Requires-Dist: pathspec>=1.0.4
12
12
  Requires-Dist: python-on-whales>=0.68.0
13
13
  Requires-Dist: pyyaml>=6.0.1
@@ -1,5 +1,6 @@
1
1
  import inspect
2
2
  import webbrowser
3
+ from collections.abc import Callable
3
4
 
4
5
  import click
5
6
 
@@ -113,15 +114,53 @@ def deploy_init_resources(context: Context, *, state_bucket: str = ""):
113
114
  resource.deploy_init()
114
115
 
115
116
 
116
- def deploy_frontend(context: Context, *, skip_build: bool = False):
117
+ def deploy_frontend(
118
+ context: Context,
119
+ *,
120
+ skip_build: bool = False,
121
+ exclude: frozenset[tuple[str, str]] = frozenset(),
122
+ ):
123
+ """uploadable な route を upload する。
124
+
125
+ exclude は upload 済みの (cloudfront, route) で、対象から外す。
126
+ """
117
127
  for _name, cf_ctx in context.cloudfront.items():
118
- cf = CloudFront(cf_ctx)
119
- if not cf_ctx.uploadable_routes:
128
+ routes = [
129
+ r for r in cf_ctx.uploadable_routes if (cf_ctx.name, r.name) not in exclude
130
+ ]
131
+ if not routes:
120
132
  continue
133
+ cf = CloudFront(cf_ctx)
121
134
  if cf.status == "NOEXIST":
122
135
  echo.warning("CloudFront '%s' が未作成です。スキップします。" % cf_ctx.name)
123
136
  continue
124
- cf.upload(skip_build=skip_build)
137
+ cf.upload(skip_build=skip_build, routes=routes)
138
+
139
+
140
+ def deploy_versioned_frontend(context: Context) -> frozenset[tuple[str, str]]:
141
+ """versioning route を Lambda 更新より先に upload する。
142
+
143
+ Lambda が新しい版の URL を出し始めた後に upload すると、その間に読まれた
144
+ ファイルは upload 前の内容 (新規ファイルなら 403) が新しい URL のキャッシュに入り、
145
+ ブラウザに versioned_max_age の間残る (KN1676)。versioning route の URL は版ごとに
146
+ 変わるので、先に upload しても稼働中の版の表示は壊れない。
147
+
148
+ SPA など versioning の無い route は対象外 (HTML が Lambda より先に切り替わる)。
149
+ CloudFront が未作成 (初回 deploy) の場合は upload せず、deploy_frontend に任せる。
150
+ 戻り値は upload した (cloudfront, route) で、deploy_frontend の exclude に渡す。
151
+ """
152
+ uploaded: set[tuple[str, str]] = set()
153
+ for _name, cf_ctx in context.cloudfront.items():
154
+ routes = [r for r in cf_ctx.uploadable_routes if r.versioning]
155
+ if not routes:
156
+ continue
157
+ cf = CloudFront(cf_ctx)
158
+ if cf.status == "NOEXIST":
159
+ continue
160
+ echo.log("versioning route を Lambda 更新より先に upload します...")
161
+ cf.upload(routes=routes)
162
+ uploaded.update((cf_ctx.name, r.name) for r in routes)
163
+ return frozenset(uploaded)
125
164
 
126
165
 
127
166
  def upload_managed_assets(context: Context):
@@ -216,11 +255,20 @@ def build_image(context: Context, *, tag: str) -> list[str]:
216
255
  return targets
217
256
 
218
257
 
219
- def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
258
+ def _deploy_pipeline(
259
+ context: Context,
260
+ *,
261
+ openpath=None,
262
+ skip_frontend=False,
263
+ before_lambda_update: Callable[[], None] | None = None,
264
+ ):
220
265
  """deploy / promote 共通のパイプライン本体。
221
266
 
222
267
  promote 時は各 container の promote_commit_hash が設定済みで、
223
268
  deploy_init 内の image build が retag に置き換わる以外は deploy と同一。
269
+
270
+ before_lambda_update は image build の後・Lambda 更新 (deploy_resources) の前に
271
+ 呼ばれる。`pocket django deploy` が static を先に upload するのに使う。
224
272
  """
225
273
  # DEPLOY_HASH の解決結果を deploy 時に 1 回可視化する (env 伝播漏れで
226
274
  # 黙って git short hash に落ちる footgun の早期発見用)。
@@ -239,6 +287,12 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
239
287
  state_bucket = state_store.bucket_name
240
288
  check_removed_inbound(context, state_store)
241
289
  deploy_init_resources(context, state_bucket=state_bucket)
290
+ # static は Lambda が新しい版の URL を出し始める前に upload する (KN1676)
291
+ uploaded_routes: frozenset[tuple[str, str]] = frozenset()
292
+ if not skip_frontend:
293
+ uploaded_routes = deploy_versioned_frontend(context)
294
+ if before_lambda_update is not None:
295
+ before_lambda_update()
242
296
  deploy_resources(context, state_bucket=state_bucket)
243
297
  cleanup_unused_inbound_domains(context, state_store)
244
298
  # リリース跨ぎ移行の掃除フェーズ (旧配置の削除は deploy 成功後にしか
@@ -246,7 +300,7 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
246
300
  migrations.run_deploy_cleanup(context)
247
301
  upload_managed_assets(context)
248
302
  if not skip_frontend:
249
- deploy_frontend(context)
303
+ deploy_frontend(context, exclude=uploaded_routes)
250
304
  # build 時の other-read 警告はログに埋もれて気付けないため最後に再掲する
251
305
  resummarize_world_read_warnings()
252
306
  # DLQ アラートの email 購読が未確認のままだと通知が届かないため、
@@ -272,12 +326,29 @@ def _deploy_pipeline(context: Context, *, openpath=None, skip_frontend=False):
272
326
  )
273
327
  @removed_skip_check_existing
274
328
  def deploy(stage: str, openpath, skip_frontend, yes):
329
+ run_deploy(stage, openpath=openpath, skip_frontend=skip_frontend, yes=yes)
330
+
331
+
332
+ def run_deploy(
333
+ stage: str,
334
+ *,
335
+ openpath=None,
336
+ skip_frontend: bool = False,
337
+ yes: bool = False,
338
+ before_lambda_update: Callable[[], None] | None = None,
339
+ ):
340
+ """`pocket deploy` の本体。`pocket django deploy` からも呼ばれる。"""
275
341
  from pocket_cli.cli.aws_auth import check_aws_credentials
276
342
 
277
343
  interaction.set_assume_yes(yes)
278
344
  check_aws_credentials()
279
345
  context = Context.from_toml(stage=stage)
280
- _deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
346
+ _deploy_pipeline(
347
+ context,
348
+ openpath=openpath,
349
+ skip_frontend=skip_frontend,
350
+ before_lambda_update=before_lambda_update,
351
+ )
281
352
 
282
353
 
283
354
  @click.command()
@@ -295,6 +366,21 @@ def promote(stage: str, commit_hash, openpath, skip_frontend, yes):
295
366
  `pocket build` で push した image に :<stage> タグを移し、
296
367
  インフラ/Lambda を更新する。image build は行わない (build once の昇格)。
297
368
  """
369
+ run_promote(
370
+ stage, commit_hash, openpath=openpath, skip_frontend=skip_frontend, yes=yes
371
+ )
372
+
373
+
374
+ def run_promote(
375
+ stage: str,
376
+ commit_hash: str,
377
+ *,
378
+ openpath=None,
379
+ skip_frontend: bool = False,
380
+ yes: bool = False,
381
+ before_lambda_update: Callable[[], None] | None = None,
382
+ ):
383
+ """`pocket promote` の本体。`pocket django promote` からも呼ばれる。"""
298
384
  from pocket_cli.cli.aws_auth import check_aws_credentials
299
385
 
300
386
  interaction.set_assume_yes(yes)
@@ -304,7 +390,12 @@ def promote(stage: str, commit_hash, openpath, skip_frontend, yes):
304
390
  raise click.ClickException("container がこの stage に設定されていません。")
305
391
  for c_ctx in context.container.values():
306
392
  c_ctx.promote_commit_hash = commit_hash
307
- _deploy_pipeline(context, openpath=openpath, skip_frontend=skip_frontend)
393
+ _deploy_pipeline(
394
+ context,
395
+ openpath=openpath,
396
+ skip_frontend=skip_frontend,
397
+ before_lambda_update=before_lambda_update,
398
+ )
308
399
 
309
400
 
310
401
  def _get_deploy_url(context: Context) -> str | None:
@@ -16,8 +16,10 @@ from pocket.django.utils import get_storages, resolve_django_container
16
16
  from pocket.utils import echo
17
17
  from pocket_cli.cli import interaction
18
18
  from pocket_cli.cli.build_cli import build
19
+ from pocket_cli.cli.deploy_cli import run_deploy, run_promote
19
20
  from pocket_cli.cli.removed_flags import removed_skip_check_existing
20
21
  from pocket_cli.resources.container import Container
22
+ from pocket_cli.resources.s3 import S3
21
23
 
22
24
 
23
25
  @click.group()
@@ -92,23 +94,65 @@ def _update_dotenv(jinja2_env: Environment):
92
94
  )
93
95
  @removed_skip_check_existing
94
96
  def deploy(stage: str, openpath, yes, skip_migrate):
95
- from pocket_cli.cli.deploy_cli import deploy as pocket_deploy
96
-
97
97
  # pocket deploy を実行(インフラ + SPA フロントエンド)
98
- ctx = click.Context(pocket_deploy)
99
- ctx.invoke(
100
- pocket_deploy,
101
- stage=stage,
98
+ static_handled: list[bool] = []
99
+ run_deploy(
100
+ stage,
102
101
  openpath=None,
103
102
  skip_frontend=False,
104
103
  yes=yes,
104
+ before_lambda_update=lambda: static_handled.append(
105
+ _deploystatic_before_lambda_update(stage)
106
+ ),
107
+ )
108
+ _django_post_deploy(
109
+ stage,
110
+ yes=yes,
111
+ openpath=openpath,
112
+ skip_migrate=skip_migrate,
113
+ static_handled=any(static_handled),
105
114
  )
106
- _django_post_deploy(stage, yes=yes, openpath=openpath, skip_migrate=skip_migrate)
107
115
 
108
116
 
109
- def _django_post_deploy(stage: str, *, yes: bool, openpath, skip_migrate: bool = False):
117
+ def _deploystatic_before_lambda_update(stage: str) -> bool:
118
+ """static を Lambda 更新より先に upload する。deploystatic を済ませたら True。
119
+
120
+ Lambda が新しい版の URL を出し始めた後に upload すると、その間に読まれた
121
+ ファイルは upload 前の内容が新しい URL のキャッシュに入り、ブラウザに長期間残る
122
+ (KN1676)。collectstatic の失敗も Lambda を更新する前に分かる。
123
+
124
+ bucket が未作成 (初回 deploy) の場合と、staticfiles が S3 でない場合は False を
125
+ 返し、従来どおり deploy 後の `_django_post_deploy` に任せる。確認に No と
126
+ 答えた場合も True を返す (deploy 後にもう一度聞かない)。
127
+ """
128
+ context = Context.from_toml(stage=stage)
129
+ if _staticfiles_publish_mode(context) == "command":
130
+ return False
131
+ c = resolve_django_container(context)
132
+ storage = c.django.storages.get("staticfiles") if c and c.django else None
133
+ if not storage or storage.store != "s3":
134
+ return False
135
+ if not context.s3 or not S3(context.s3).exists():
136
+ return False
137
+ if interaction.confirm("deploystatic?", default=True):
138
+ collectstatic_locally(stage, link=_staticfiles_link(context))
139
+ upload_collected_staticfiles(stage)
140
+ return True
141
+
142
+
143
+ def _django_post_deploy(
144
+ stage: str,
145
+ *,
146
+ yes: bool,
147
+ openpath,
148
+ skip_migrate: bool = False,
149
+ static_handled: bool = False,
150
+ ):
110
151
  """deploy / promote 共通の Django 固有後処理 (collectstatic + migrate + URL)。
111
152
 
153
+ static_handled=True は、Lambda 更新の前に deploystatic を済ませた
154
+ (`_deploystatic_before_lambda_update`) ことを表し、ここでは繰り返さない。
155
+
112
156
  skip_migrate=True なら migrate は確認ごと省く。`-y` は「聞かれたことに全部
113
157
  yes」の意味を保ちたいので、非対話で migrate を外す手段はフラグ側に置く
114
158
  (DB が到達不能でもインフラ更新だけ通したい、という状況が実在する)。
@@ -116,7 +160,9 @@ def _django_post_deploy(stage: str, *, yes: bool, openpath, skip_migrate: bool =
116
160
  if yes:
117
161
  interaction.set_assume_yes(True)
118
162
  context = Context.from_toml(stage=stage)
119
- if _staticfiles_publish_mode(context) == "command":
163
+ if static_handled:
164
+ pass
165
+ elif _staticfiles_publish_mode(context) == "command":
120
166
  echo.info(
121
167
  'staticfiles is publish = "command": skipping deploystatic. '
122
168
  "Publish with `pocket django deploystatic --stage %s`." % stage
@@ -163,18 +209,24 @@ def promote(stage: str, commit_hash, openpath, yes, skip_migrate):
163
209
  を更新する。image build は行わない (build once の昇格)。静的アセットは deploy 時
164
210
  と同様にローカルでビルドして upload する。
165
211
  """
166
- from pocket_cli.cli.deploy_cli import promote as pocket_promote
167
-
168
- ctx = click.Context(pocket_promote)
169
- ctx.invoke(
170
- pocket_promote,
171
- stage=stage,
172
- commit_hash=commit_hash,
212
+ static_handled: list[bool] = []
213
+ run_promote(
214
+ stage,
215
+ commit_hash,
173
216
  openpath=None,
174
217
  skip_frontend=False,
175
218
  yes=yes,
219
+ before_lambda_update=lambda: static_handled.append(
220
+ _deploystatic_before_lambda_update(stage)
221
+ ),
222
+ )
223
+ _django_post_deploy(
224
+ stage,
225
+ yes=yes,
226
+ openpath=openpath,
227
+ skip_migrate=skip_migrate,
228
+ static_handled=any(static_handled),
176
229
  )
177
- _django_post_deploy(stage, yes=yes, openpath=openpath, skip_migrate=skip_migrate)
178
230
 
179
231
 
180
232
  # 旧コマンドは同じ Click command の別名として維持する。
@@ -279,9 +279,18 @@ class CloudFront:
279
279
  "managed_assets: %d ファイルをアップロードしました" % len(uploaded_keys)
280
280
  )
281
281
 
282
- def upload(self, *, skip_build: bool = False):
282
+ def upload(
283
+ self, *, skip_build: bool = False, routes: list[RouteContext] | None = None
284
+ ):
285
+ """uploadable な route を build して S3 へ同期する。
286
+
287
+ routes を渡すとその route だけを対象にする (deploy が versioning route を
288
+ Lambda 更新より先に upload するため)。
289
+ """
290
+ if routes is None:
291
+ routes = self.context.uploadable_routes
283
292
  changed_routes: list[RouteContext] = []
284
- for route in self.context.uploadable_routes:
293
+ for route in routes:
285
294
  if route.build_cmd and not skip_build:
286
295
  echo.info("ビルド実行: %s" % route.build_cmd)
287
296
  try:
@@ -253,7 +253,12 @@ Resources:
253
253
  - DELETE
254
254
  # {% else %}
255
255
  DefaultCacheBehavior:
256
+ # {% if default_route.is_deploy_hash %}
257
+ CachePolicyId:
258
+ Ref: DeployHashCachePolicy{{ default_route.yaml_key }}
259
+ # {% else %}
256
260
  CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" # Managed-CachingOptimized
261
+ # {% endif %}
257
262
  TargetOriginId: "{{ resource_prefix }}{{ default_route.name }}"
258
263
  ViewerProtocolPolicy: "redirect-to-https"
259
264
  # {% if default_route.is_spa or default_route.versioning or basic_auth or has_redirect_from %}
@@ -262,6 +267,10 @@ Resources:
262
267
  - EventType: viewer-request
263
268
  FunctionARN:
264
269
  Fn::GetAtt: UrlFallbackFunction{{ default_route.yaml_key }}.FunctionMetadata.FunctionARN
270
+ # {% elif default_route.is_deploy_hash %}
271
+ - EventType: viewer-request
272
+ FunctionARN:
273
+ Fn::GetAtt: DeployHashStripFunction{{ default_route.yaml_key }}.FunctionMetadata.FunctionARN
265
274
  # {% elif basic_auth or has_redirect_from %}
266
275
  - EventType: viewer-request
267
276
  FunctionARN:
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "magic-pocket-cli"
3
- version = "0.41.0"
3
+ version = "0.41.1"
4
4
  description = "CLI and deploy tools for magic-pocket."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
7
  # lib は同 minor に束縛する。0.x では minor が breaking の単位のため、
8
8
  # 上限なしだと cli を pin した消費者にも lib の破壊的変更が届いてしまう
9
- "magic-pocket>=0.41.0,<0.42",
9
+ "magic-pocket>=0.41.1,<0.42",
10
10
  "click>=8.1.7",
11
11
  "jinja2>=3.1.3",
12
12
  "python-on-whales>=0.68.0",