Merge pull request 'feat(cutover): asset versioning + rollback playbook hardening (#188)' (#216) from feat/issue-188-cutover-hardening into main
All checks were successful
CI / test-and-quality (push) Successful in 2m38s
All checks were successful
CI / test-and-quality (push) Successful in 2m38s
This commit was merged in pull request #216.
This commit is contained in:
47
docs/ISSUE-188-SPA-CUTOVER-HARDENING-ARTIFACT.md
Normal file
47
docs/ISSUE-188-SPA-CUTOVER-HARDENING-ARTIFACT.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Issue #188 Artifact — SPA cutover hardening (asset versioning + rollback)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
Acceptance for `[READY][SPA][P11] Cutover hardening`:
|
||||||
|
1. Dokumenteret strategi for cache-busting/versionering af SPA assets i Django staticfiles/reverse proxy setup.
|
||||||
|
2. Konfigurerbar rollback-procedure for `USE_SPA_UI` (trin-for-trin, target <10 min).
|
||||||
|
3. Smoke-artefakt for både SPA on/off i samme release-vindue.
|
||||||
|
4. Ingen gameplay-ændringer.
|
||||||
|
|
||||||
|
## 1) Asset versioning/cache-busting strategi
|
||||||
|
Implementeret i SPA shell render-path:
|
||||||
|
|
||||||
|
- Konfiguration i `partyhub/settings.py`:
|
||||||
|
- `WPP_SPA_ASSET_BASE` (eksisterende)
|
||||||
|
- `WPP_SPA_ASSET_VERSION` (ny)
|
||||||
|
- `lobby/ui_views.py` injicerer `spa_asset_version` til template-context.
|
||||||
|
- `lobby/templates/lobby/spa_shell.html` appender `?v={{ spa_asset_version }}` på:
|
||||||
|
- `styles.css`
|
||||||
|
- `main.js`
|
||||||
|
|
||||||
|
Effekt:
|
||||||
|
- Ny release-version (fx SHA/tag) kan tvinge cache-miss i browser/proxy uden ændring af route.
|
||||||
|
- Rollback kan pege på tidligere stabil version-token med samme mekanisme.
|
||||||
|
|
||||||
|
## 2) Rollback playbook (`USE_SPA_UI`) — target <10 min
|
||||||
|
Dokumenteret i `docs/spa-cutover-flag.md`:
|
||||||
|
|
||||||
|
1. Sæt `USE_SPA_UI=false`.
|
||||||
|
2. Sæt `WPP_SPA_ASSET_VERSION` til sidste stabile release-token.
|
||||||
|
3. Deploy/reload app-processer.
|
||||||
|
4. Verificér legacy routes (`/lobby/ui/host` + `/lobby/ui/player`).
|
||||||
|
5. Kør hurtig smoke sanity.
|
||||||
|
6. Log trigger/timestamp/resultat i smoke artifact.
|
||||||
|
|
||||||
|
## 3) Smoke artifact for SPA OFF/ON i samme release-vindue
|
||||||
|
Dokumenteret i:
|
||||||
|
- `docs/UI_SMOKE.md` (sektion: "Samme release-vindue: SPA OFF + ON verifikation")
|
||||||
|
- `docs/STAGING_GAMEPLAY_SMOKE_ARTIFACT.md` (template udvidet med release-window check + `WPP_SPA_ASSET_VERSION`)
|
||||||
|
|
||||||
|
Krav:
|
||||||
|
- OFF-pass (legacy) og ON-pass (SPA) køres i samme deploy/release-vindue.
|
||||||
|
- Begge passes logges i samme artifact med UTC timestamps og version-token.
|
||||||
|
|
||||||
|
## Non-goals bekræftet
|
||||||
|
- Ingen gameplay-regler ændret.
|
||||||
|
- Ingen API-kontrakter ændret.
|
||||||
|
- Ingen UX-redesign; kun drift/cutover-hardening.
|
||||||
@@ -23,11 +23,14 @@ Formål: levere et lille, ensartet evidensformat for release-nær gameplay-smoke
|
|||||||
- Active category/questions present: <yes/no>
|
- Active category/questions present: <yes/no>
|
||||||
- Participants: host + <N> players
|
- Participants: host + <N> players
|
||||||
- `USE_SPA_UI`: <on/off>
|
- `USE_SPA_UI`: <on/off>
|
||||||
|
- `WPP_SPA_ASSET_VERSION`: <release-token/sha>
|
||||||
- UI route used:
|
- UI route used:
|
||||||
- OFF (legacy): `/lobby/ui/host` + `/lobby/ui/player`
|
- OFF (legacy): `/lobby/ui/host` + `/lobby/ui/player`
|
||||||
- ON (SPA shell): `/lobby/ui/host/<spa-path>` + `/lobby/ui/player`
|
- ON (SPA shell): `/lobby/ui/host/<spa-path>` + `/lobby/ui/player`
|
||||||
|
|
||||||
#### Checks (PASS/FAIL)
|
#### Checks (PASS/FAIL)
|
||||||
|
0. Same release-window verification
|
||||||
|
- OFF + ON smoke kørt i samme release-vindue: <pass/fail>
|
||||||
1. Cutover route sanity
|
1. Cutover route sanity
|
||||||
- Flag OFF serves legacy UI templates: <pass/fail>
|
- Flag OFF serves legacy UI templates: <pass/fail>
|
||||||
- Flag ON serves SPA shell on expected path(s): <pass/fail>
|
- Flag ON serves SPA shell on expected path(s): <pass/fail>
|
||||||
|
|||||||
@@ -27,6 +27,18 @@
|
|||||||
- Next-round/final leaderboard sanity er PASS.
|
- Next-round/final leaderboard sanity er PASS.
|
||||||
- Ingen nye blocker-regressioner i host/player kerneflow.
|
- Ingen nye blocker-regressioner i host/player kerneflow.
|
||||||
|
|
||||||
|
## Samme release-vindue: SPA OFF + ON verifikation
|
||||||
|
Kør begge checks i samme release-vindue (samme deploy/artifact version):
|
||||||
|
|
||||||
|
1. **OFF-pass (legacy)**
|
||||||
|
- `USE_SPA_UI=false`
|
||||||
|
- Verificér legacy routes + fuld runde.
|
||||||
|
2. **ON-pass (SPA)**
|
||||||
|
- `USE_SPA_UI=true`
|
||||||
|
- Behold samme release artifact og kun toggl flag/version-token ved behov.
|
||||||
|
- Verificér SPA shell routes + fuld runde.
|
||||||
|
3. Dokumentér begge pass i samme smoke-artifact med UTC timestamps og `WPP_SPA_ASSET_VERSION`.
|
||||||
|
|
||||||
## Rollback check points
|
## Rollback check points
|
||||||
Skift straks tilbage til `USE_SPA_UI=false` hvis en gate fejler:
|
Skift straks tilbage til `USE_SPA_UI=false` hvis en gate fejler:
|
||||||
1. Verificér legacy routes (`/lobby/ui/host` + `/lobby/ui/player`) fungerer igen.
|
1. Verificér legacy routes (`/lobby/ui/host` + `/lobby/ui/player`) fungerer igen.
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ Sæt env var pr. miljø:
|
|||||||
Backward compatibility under cutover:
|
Backward compatibility under cutover:
|
||||||
- Hvis `USE_SPA_UI` ikke er sat, bruges `WPP_SPA_ENABLED` som fallback.
|
- Hvis `USE_SPA_UI` ikke er sat, bruges `WPP_SPA_ENABLED` som fallback.
|
||||||
|
|
||||||
|
## Static asset versioning/cache-busting (hardening)
|
||||||
|
Formål: sikre at browser/proxy/CDN hurtigt henter ny SPA bundle i release-vinduet uden at kræve hard refresh.
|
||||||
|
|
||||||
|
- `WPP_SPA_ASSET_BASE` peger fortsat på build-output (`/static/frontend/angular/browser`).
|
||||||
|
- `WPP_SPA_ASSET_VERSION` injiceres i SPA shell URLs som query-param (`?v=<version>`).
|
||||||
|
- Anbefalet værdi for `WPP_SPA_ASSET_VERSION`: release-tag eller kort commit SHA.
|
||||||
|
- Ved rollback sættes `WPP_SPA_ASSET_VERSION` til den tidligere kendte stabile release-værdi.
|
||||||
|
|
||||||
|
Eksempel (staging/prod env):
|
||||||
|
|
||||||
|
```env
|
||||||
|
USE_SPA_UI=true
|
||||||
|
WPP_SPA_ASSET_BASE=/static/frontend/angular/browser
|
||||||
|
WPP_SPA_ASSET_VERSION=rel-2026-03-01-bb82357
|
||||||
|
```
|
||||||
|
|
||||||
## Staging rollout-checkliste (`USE_SPA_UI`)
|
## Staging rollout-checkliste (`USE_SPA_UI`)
|
||||||
1. **Baseline (flag OFF)**
|
1. **Baseline (flag OFF)**
|
||||||
- Bekræft at staging kører med `USE_SPA_UI=false`.
|
- Bekræft at staging kører med `USE_SPA_UI=false`.
|
||||||
@@ -27,15 +43,21 @@ Backward compatibility under cutover:
|
|||||||
4. **Post-cutover dokumentation**
|
4. **Post-cutover dokumentation**
|
||||||
- Log evidens med commit/head SHA, UTC timestamp og gate-status.
|
- Log evidens med commit/head SHA, UTC timestamp og gate-status.
|
||||||
|
|
||||||
## Rollback-checkpoints (staging)
|
## Rollback playbook (`USE_SPA_UI`) — mål: <10 min
|
||||||
Rollback til legacy (`USE_SPA_UI=false`) udføres straks hvis et checkpoint fejler:
|
Rollback til legacy (`USE_SPA_UI=false`) udføres straks hvis et checkpoint fejler:
|
||||||
- Forkert route/shell for valgt flag i cutover route sanity.
|
- Forkert route/shell for valgt flag i cutover route sanity.
|
||||||
- Gameplay smoke kan ikke gennemføres til scoreboard/final leaderboard.
|
- Gameplay smoke kan ikke gennemføres til scoreboard/final leaderboard.
|
||||||
- Kritisk regression i host/player flow under smoke.
|
- Kritisk regression i host/player flow under smoke.
|
||||||
|
|
||||||
Efter rollback:
|
Trin-for-trin:
|
||||||
- Bekræft legacy routes virker igen (`/lobby/ui/host` + `/lobby/ui/player`).
|
1. Sæt `USE_SPA_UI=false` i deploy-env.
|
||||||
- Log rollback-trigger + repro + blocker-link i smoke artifact.
|
2. Sæt `WPP_SPA_ASSET_VERSION` til sidste stabile release-token.
|
||||||
|
3. Deploy/reload app-processer.
|
||||||
|
4. Verificér legacy routes: `/lobby/ui/host` + `/lobby/ui/player`.
|
||||||
|
5. Kør hurtig smoke sanity (join/start/scoreboard path).
|
||||||
|
6. Log UTC tid, trigger, release-token og resultat i smoke artifact.
|
||||||
|
|
||||||
|
Target: rollback + sanity-verifikation inden for 10 minutter.
|
||||||
|
|
||||||
## Verifikation
|
## Verifikation
|
||||||
- Flag OFF: `UiScreenTests.test_legacy_templates_are_used_when_spa_flag_is_off`
|
- Flag OFF: `UiScreenTests.test_legacy_templates_are_used_when_spa_flag_is_off`
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WPP SPA Shell</title>
|
<title>WPP SPA Shell</title>
|
||||||
<link rel="stylesheet" href="{{ spa_asset_base }}/styles.css">
|
<link rel="stylesheet" href="{{ spa_asset_base }}/styles.css?v={{ spa_asset_version|urlencode }}">
|
||||||
</head>
|
</head>
|
||||||
<body data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">
|
<body data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">
|
||||||
<app-root data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">Indlæser Angular app-shell…</app-root>
|
<app-root data-wpp-shell-route="{{ shell_route }}" data-wpp-shell-kind="{{ shell_kind }}">Indlæser Angular app-shell…</app-root>
|
||||||
<script type="module" src="{{ spa_asset_base }}/main.js"></script>
|
<script type="module" src="{{ spa_asset_base }}/main.js?v={{ spa_asset_version|urlencode }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1022,7 +1022,8 @@ class UiScreenTests(TestCase):
|
|||||||
self.assertContains(response, "<app-root")
|
self.assertContains(response, "<app-root")
|
||||||
self.assertContains(response, "data-wpp-shell-route=\"/host\"")
|
self.assertContains(response, "data-wpp-shell-route=\"/host\"")
|
||||||
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
self.assertContains(response, "data-wpp-shell-kind=\"host\"")
|
||||||
self.assertContains(response, "/static/frontend/angular/browser/main.js")
|
self.assertContains(response, "/static/frontend/angular/browser/main.js?v=dev")
|
||||||
|
self.assertContains(response, "/static/frontend/angular/browser/styles.css?v=dev")
|
||||||
|
|
||||||
@override_settings(USE_SPA_UI=True)
|
@override_settings(USE_SPA_UI=True)
|
||||||
def test_host_screen_deeplink_preserves_spa_path_when_feature_flag_enabled(self):
|
def test_host_screen_deeplink_preserves_spa_path_when_feature_flag_enabled(self):
|
||||||
@@ -1056,7 +1057,17 @@ class UiScreenTests(TestCase):
|
|||||||
self.assertContains(response, "<app-root")
|
self.assertContains(response, "<app-root")
|
||||||
self.assertContains(response, "data-wpp-shell-route=\"/player\"")
|
self.assertContains(response, "data-wpp-shell-route=\"/player\"")
|
||||||
self.assertContains(response, "data-wpp-shell-kind=\"player\"")
|
self.assertContains(response, "data-wpp-shell-kind=\"player\"")
|
||||||
self.assertContains(response, "/static/frontend/angular/browser/main.js")
|
self.assertContains(response, "/static/frontend/angular/browser/main.js?v=dev")
|
||||||
|
|
||||||
|
@override_settings(USE_SPA_UI=True, WPP_SPA_ASSET_VERSION="release-2026-03-01")
|
||||||
|
def test_spa_shell_uses_configured_asset_version_for_cache_busting(self):
|
||||||
|
self.client.login(username="host_ui", password="secret123")
|
||||||
|
|
||||||
|
response = self.client.get(reverse("lobby:host_screen"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "/static/frontend/angular/browser/styles.css?v=release-2026-03-01")
|
||||||
|
self.assertContains(response, "/static/frontend/angular/browser/main.js?v=release-2026-03-01")
|
||||||
|
|
||||||
|
|
||||||
class SessionDetailRoundQuestionTests(TestCase):
|
class SessionDetailRoundQuestionTests(TestCase):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def _render_spa_shell(request, shell_route: str, shell_kind: str):
|
|||||||
"shell_route": shell_route,
|
"shell_route": shell_route,
|
||||||
"shell_kind": shell_kind,
|
"shell_kind": shell_kind,
|
||||||
"spa_asset_base": settings.WPP_SPA_ASSET_BASE,
|
"spa_asset_base": settings.WPP_SPA_ASSET_BASE,
|
||||||
|
"spa_asset_version": getattr(settings, "WPP_SPA_ASSET_VERSION", "dev"),
|
||||||
"lobby_i18n": lobby_i18n_catalog(),
|
"lobby_i18n": lobby_i18n_catalog(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ if USE_SPA_UI_RAW is None:
|
|||||||
USE_SPA_UI_RAW = env('WPP_SPA_ENABLED', 'false')
|
USE_SPA_UI_RAW = env('WPP_SPA_ENABLED', 'false')
|
||||||
USE_SPA_UI = USE_SPA_UI_RAW.lower() == 'true'
|
USE_SPA_UI = USE_SPA_UI_RAW.lower() == 'true'
|
||||||
WPP_SPA_ASSET_BASE = env('WPP_SPA_ASSET_BASE', '/static/frontend/angular/browser').rstrip('/')
|
WPP_SPA_ASSET_BASE = env('WPP_SPA_ASSET_BASE', '/static/frontend/angular/browser').rstrip('/')
|
||||||
|
# Cache-busting token for SPA shell static asset URLs (querystring versioning).
|
||||||
|
# Set to release id / commit SHA in deploy env for deterministic invalidation.
|
||||||
|
WPP_SPA_ASSET_VERSION = env('WPP_SPA_ASSET_VERSION', 'dev')
|
||||||
|
|
||||||
CHANNEL_REDIS_HOST = env('CHANNEL_REDIS_HOST', '127.0.0.1')
|
CHANNEL_REDIS_HOST = env('CHANNEL_REDIS_HOST', '127.0.0.1')
|
||||||
CHANNEL_REDIS_PORT = int(env('CHANNEL_REDIS_PORT', '6379'))
|
CHANNEL_REDIS_PORT = int(env('CHANNEL_REDIS_PORT', '6379'))
|
||||||
|
|||||||
Reference in New Issue
Block a user