Per-cycle deploy report covering AZ-487 (JWT baseline) + AZ-488 (UAV tile batch upload). Lists all 12 cycle-2 commits already pushed to origin/dev, recaps Steps 11-15 gate outcomes, flags three operator-gated risks (R1 consumer Bearer-token coordination, R2 JWT_SECRET prod-distinct verification, R3 GPS-permission claim provisioning), documents rollback (image flip; zero schema change), and lists deferred follow-ups (PT-07/PT-08 harness + run-perf script JWT-attach, F1 doc-folder choice). Advances autodev pointer to Step 17 (Retrospective). Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
Deploy Report — Cycle 2 (AZ-487 + AZ-488)
Date: 2026-05-11 Cycle: 2 Scope: JWT validation baseline (AZ-487) + UAV tile batch upload endpoint with 5-rule quality gate (AZ-488).
What is shipping
Code changes (committed to dev, pushed)
| Commit | Subject |
|---|---|
42a3cc7 |
[AZ-487] [AZ-488] Cycle 2 Step 9: JWT baseline + UAV upload task specs |
8e15e53 |
chore: cycle 2 step 9 task plan artifacts + step 10 state |
96cd3c4 |
[AZ-487] JWT validation baseline (HS256, all endpoints) |
753be43 |
[AZ-487] fix: resolve CS0104 ambiguity in AuthN tests |
f64d0d7 |
[AZ-487] fix: JWT factory + tests now pass on net8.0 |
11b7074 |
[AZ-487] fix: integration-test JWT factory handles negative lifetime |
1802d32 |
[AZ-488] UAV tile batch upload + 5-rule quality gate |
dc3dabe |
[AZ-488] fix: seed UavUploadTests coordinate counter from wall-clock |
98cdcd1 |
[AZ-487] [AZ-488] docs: cycle 2 test-spec sync |
e3cd388 |
[AZ-487] [AZ-488] docs: cycle 2 doc sync (task mode) |
5214a4a |
[AZ-487] [AZ-488] security: cycle 2 delta audit (PASS_WITH_WARNINGS) |
cbbb26b |
[AZ-487] [AZ-488] chore: cycle 2 Step 15 skip + record JWT-attach script rot |
All 12 commits on dev, pushed to origin/dev as of this report.
Database migration
None this cycle. AZ-487 ships zero DDL; AZ-488 reuses the AZ-484 tile-storage schema (source, captured_at, 5-column unique index) — UAV rows insert into the existing table via ITileRepository.InsertAsync with source='uav'.
Configuration changes (operator must verify before promoting)
| Setting | Was | Now | Source |
|---|---|---|---|
JWT_SECRET (env var) |
unset | must be ≥ 32 bytes, distinct from DEV placeholder | AZ-487 — required for API to start. App throws InvalidOperationException at startup on missing or short value. |
Jwt:Secret (appsettings) |
n/a | empty in appsettings.json; DEV-ONLY-… placeholder in appsettings.Development.json |
AZ-487. Env var overrides config. |
UavQuality:* (appsettings) |
n/a | shipped defaults (5 KiB–5 MiB, 7-day age, MaxBatchSize=100, variance=10) | AZ-488. Tunable per-env without code change. |
docker-compose.yml → api.environment |
— | JWT_SECRET=${JWT_SECRET} line added |
AZ-487 |
docker-compose.tests.yml → integration-tests.environment |
— | same JWT_SECRET=${JWT_SECRET} so test runner can mint matching tokens |
AZ-487 |
.env.example |
(no JWT line) | JWT_SECRET= placeholder line |
AZ-487 |
Kestrel MaxRequestBodySize |
default (30 MB) | MaxBatchSize × MaxBytes (500 MB worst case) |
AZ-488 — see Program.cs |
FormOptions.MultipartBodyLengthLimit / ValueLengthLimit |
default | raised to envelope cap | AZ-488 |
Documentation, test-spec, audit, leftover artifacts (all committed in the commits above)
_docs/02_document/contracts/api/uav-tile-upload.mdv1.0.0 (new, frozen) — AZ-488._docs/02_document/architecture.md,glossary.md,data_model.md,module-layout.md,modules/api_program.md,modules/common_configs.md,modules/common_dtos.md,modules/tests_unit.md,modules/tests_integration.md,components/03_tile_downloader/description.md,ripple_log_cycle2.md— Step 13 (Update Docs)._docs/02_document/tests/blackbox-tests.md,security-tests.md,resource-limit-tests.md,traceability-matrix.md,performance-tests.md— Step 12 (Test-Spec Sync) + Step 10 PT-08 entry._docs/05_security/(5 files, cycle-2 deltas appended) — Step 14 (Security Audit) — PASS_WITH_WARNINGS._docs/_process_leftovers/2026-05-11_perf-pt07-harness.md(updated) — PT-08 follow-on + scripts/run-performance-tests.sh JWT-attach script rot._docs/03_implementation/batch_01_cycle2_report.md,batch_02_cycle2_report.md,reviews/batch_01_cycle2_review.md,reviews/batch_02_cycle2_review.md— Step 10 (Implement) per-batch + review reports._docs/02_tasks/done/AZ-487_jwt_validation_baseline.md,AZ-488_uav_tile_upload.md(moved from todo/)._docs/02_tasks/_dependencies_table.md(statuses updated toDone (In Testing)).
Pre-deploy gate recap
| Gate | Outcome |
|---|---|
| Step 11 — Run Tests | PASS — full integration suite (scripts/run-tests.sh --full) green against the post-cycle-2 build. Includes the new JwtIntegrationTests (5 scenarios) + UavUploadTests (7 scenarios) plus all 213 baseline unit tests and the cycle-1 AZ-484 integration tests. Fixed mid-step: AZ-488 integration UavUploadTests._coordinateCounter was reset on every process start, colliding with persisted Postgres data across docker-compose runs — counter now seeded from wall-clock seconds. |
| Step 12 — Test-Spec Sync | PASS — appended cycle-2 ACs (AZ-487 AC-1..AC-8 + AZ-488 AC-1..AC-10), NFRs, restrictions to traceability matrix; added SEC-05..SEC-11, BT-13..BT-18, RL-05..RL-07. Coverage 47/47 ACs, 8/8 restrictions. |
| Step 13 — Update Docs | PASS — module-layout, common_configs, common_dtos, tests_unit, tests_integration refreshed; ripple_log_cycle2.md generated (no unexpected ripple). Earlier doc work (architecture, glossary, data_model, modules/api_program, components/03_tile_downloader, contracts/api/uav-tile-upload) was already committed during Step 10. |
| Step 14 — Security Audit | PASS_WITH_WARNINGS — 0 Critical, 0 High. 2 new Medium (F-AUTH-2 iss/aud not validated; F-UAV-1 / F-DEPS-UAV ImageSharp decode exposure widened — both bounded by existing mitigations and tracked as follow-ups). 4 new Low + 1 Informational, all accepted or folded into existing cycle-1 remediations. OWASP A01 / A07 moved from N/A to PASS_WITH_WARNINGS. |
| Step 15 — Performance Test | SKIPPED (option B at the user gate). scripts/run-performance-tests.sh PT-01..PT-06 currently 401 against the post-AZ-487 build because it attaches no Bearer token; PT-07 + PT-08 remain Deferred per the existing leftover. Script-rot + perf-harness work tracked in _docs/_process_leftovers/2026-05-11_perf-pt07-harness.md. |
dotnet format whitespace --verify-no-changes |
Implicitly passed via Step 11 (the run-tests.sh invocation runs format check ahead of tests; the cycle-2 commits all came through that gate). |
Cycle-2-specific operational risks (the deploy operator must act on these)
R1 — Every existing API client BREAKS the instant AZ-487 lands
gps-denied-onboard, mission planner UI, and any other satellite-provider consumer currently call the API without an Authorization header. The moment the AZ-487 image is promoted, every such call returns HTTP 401.
Operator action (BEFORE promoting beyond dev):
- Confirm with
gps-denied-onboardteam that their build attachesAuthorization: Bearer <admin-API-token>to every outbound call to satellite-provider. - Confirm the same with the mission planner UI team.
- Stage the deploy through
devfirst; run an end-to-end probe from each consumer before promoting tostage/prod. - No fallback / bypass flag exists by design (rejected during AZ-487 planning).
R2 — JWT_SECRET must be set to a real production value
The API throws InvalidOperationException at startup if JWT_SECRET is missing or shorter than 32 bytes (caught by SEC-08 + unit AddSatelliteJwt_ThrowsOnMissingSecret). HOWEVER: an operator who copies appsettings.Development.json verbatim into prod (or who sets JWT_SECRET to the literal DEV-ONLY placeholder) would still pass the 32-byte gate. The placeholder is plainly published in this repo on every clone.
Operator action: the deploy pipeline (or the operator running the manual promote) must verify JWT_SECRET is set, is ≥ 32 bytes, AND is distinct from the DEV-ONLY-DO-NOT-USE-IN-PROD-… literal in appsettings.Development.json. Recorded as a cycle-2 security recommendation in _docs/05_security/security_report.md.
R3 — UAV upload consumers need the GPS permission claim
gps-denied-onboard (or any client that posts to /api/satellite/upload) must have its admin-API-issued JWT include permissions: ["GPS"] (or a single string permissions: "GPS"). Tokens with any other permission shape return HTTP 403 (SEC-10 / AZ-488 AC-6).
Operator action: coordinate with the admin team to confirm UAV-producer service accounts hold the GPS permission. If permissions is missing from those accounts' issued tokens, every UAV upload returns 403 even with a valid signature.
R4 — Postgres data volume persistence across docker-compose runs
Discovered mid-Step 11: the local docker-compose.yml Postgres uses a named volume. Tile rows persist across docker-compose down / up cycles. The AZ-488 integration tests now seed coordinates from a wall-clock counter so they don't collide with prior runs — but operators doing manual test loops on a single host should either explicitly docker-compose down -v or accept that prior tiles will remain in the table.
This is not new behavior introduced by cycle 2 — it just became observable when AZ-488 integration tests started inserting rows. No production change required.
Rollback plan
This deploy ships zero schema changes, so rollback is purely an image-version flip plus an operator-side config rollback:
- Re-deploy the pre-cycle-2 image (
registry.../azaion/satellite-provider:<previous-tag>— last cycle-1 deploy commit was1860965). - Optional: remove the
JWT_SECRET=${JWT_SECRET}line from the deployeddocker-compose.yml(the previous image does not read it; harmless to leave). - No DB rollback needed —
tilestable is identical before and after cycle 2. - Inform consumers that the auth requirement is being temporarily lifted; they may keep attaching the token (harmless) or strip it.
- If a rollback is necessary BECAUSE the UAV upload endpoint mis-behaved, the pre-cycle-2 endpoint was a 501 stub — UAV producers must again accept that uploads don't persist until a fix is shipped.
Post-deploy verification
After the cycle-2 image is deployed and the API is bound:
- JWT smoke:
curl -s -o /dev/null -w "%{http_code}\n" "$API_URL/api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18" # Expected: 401 curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $VALID_TOKEN" \ "$API_URL/api/satellite/tiles/latlon?Latitude=47.461747&Longitude=37.647063&ZoomLevel=18" # Expected: 200 - UAV upload smoke:
curl -X POST -H "Authorization: Bearer $VALID_GPS_TOKEN" -F 'metadata=@m.json' -F 'files=@tile.jpg' "$API_URL/api/satellite/upload"— expect HTTP 200 withitems[0].status == "accepted". - Swagger Bearer button: open
/swagger, confirm the green Authorize button is present (AZ-487 AC-7). - No regression in AZ-484 reads:
GET /api/satellite/tiles/latlon?...for a cell that has bothgoogle_mapsanduavrows — expect the row with the highercaptured_at. Validated by integration testMultiSourceCoexistence_AZ484_Cycle2; do a single live spot-check on prod data. - Tail Serilog for the first hour: alert on any unhandled exception inside the auth middleware or the upload handler (both wrap their failure paths in structured logging).
CI/CD path
.woodpecker/02-build-push.yml builds and pushes on push to dev, stage, main. All cycle-2 commits are on dev and pushed to origin/dev, so the dev-tier image is building / has built automatically.
Promote to stage / main only after the consumer-coordination items in R1 + R3 are confirmed and the JWT-secret check in R2 is part of the promote runbook.
Push policy: per git-workflow.mdc, this autodev did NOT push beyond dev. Manual operator action required for stage / main promotion.
Security caveats carried into this deploy
The cycle-2 audit (Step 14) flagged 2 new Medium findings — both bounded by mitigations and tracked as follow-ups, NOT blockers:
- F-AUTH-2 —
iss/audnot validated. Coordinate with admin team to define the values; flipValidateIssuer/ValidateAudiencetotruein a small follow-up PBI when ready. - F-UAV-1 / F-DEPS-UAV — ImageSharp 3.1.11 now decodes attacker-controlled JPEGs. Today's mitigations (magic-byte gate, size cap, scoped
try/catch) are sufficient against current advisories. Subscribe to GHSA forSixLabors.ImageSharp; patch within 7 days of any new CVE.
Cycle-1 carry-overs (S1, S2, S4, D1, I3, I5) are unchanged — still flagged in _docs/05_security/security_report.md as the pre-public-network hardening backlog.
What's still open for cycle 2 (NOT blockers)
- PT-07 + PT-08 perf harness (
_docs/_process_leftovers/2026-05-11_perf-pt07-harness.md) — Deferred since cycle 1; cycle-2 NFRs piled on but no harness work landed. Replay at the next cycle's Step 15. scripts/run-performance-tests.shJWT-attach (same leftover) — script is currently broken end-to-end against the cycle-2 build; not blocking because Step 15 is skipped.- F1 carry-over — task specs reference
_docs/02_document/components/01_web_api/description.mdwhich doesn't exist. The relevant content went intomodules/api_program.mdandarchitecture.mdfor now. Needs an operator decision on whether to create the stub folder or formalize "WebApi has nocomponents/*folder" as the convention. Surfaced in both cycle-2 code reviews as a Low finding.