Cycle 7 is a pure-quality cycle: no migrations, no new endpoints, no new env vars, unchanged container image base. The full shipping payload is a contract MAJOR bump (tile-inventory.md 1.0.0 -> 2.0.0, breaking) plus a new strict-validation surface across the inventory endpoint. Deploy report covers: - 3 cycle-7 commits (task adoption, implementation, sync) + this one + the pending close commit. - Zero migrations; tiles schema unchanged from cycle 6. - Postgres host-port move 5432 -> 5433 (dev-only sibling-conflict avoidance; staging/prod unaffected). - Two NuGet additions (FluentValidation 12.0.0 + .DependencyInjectionExtensions 12.0.0), both CVE-clean. - 5 verification gates: tests PASS, test-spec sync PASS, docs PASS, security PASS_WITH_WARNINGS (3 Low), perf PASS (9/9 incl. PT-09 v2 smoke). - 4 NEW cycle-7 recommended follow-ups (D-AZ795-1 FV bump; F-AZ795-1/2 message sanitisation; implementation-report exit-gate contract; AZ-795 child-task sweep for remaining public endpoints). - Zero new process leftovers; cycle 5/6 carry-overs unchanged. Step 16.5 (Release) skipped per the cycle-2-to-6 convention; the operator runbook in this deploy report serves as the release record. User-confirmed via Choose A/B/C at the Step 16.5 gate. Co-authored-by: Cursor <cursoragent@cursor.com>
21 KiB
Deploy Report — Cycle 7 (AZ-794 + AZ-795 + AZ-796)
Date: 2026-05-22 Cycle: 7 Scope: Three-task cycle delivering the API quality follow-up scope adopted from gps-denied-onboard's AZ-777 Phase 1 Jetson probe:
- AZ-794 — rename inventory body fields
tileZoom/tileX/tileY → z/x/y(OSM / slippy-map convention). - AZ-795 — epic + shared infra for strict input validation across all public endpoints: FluentValidation 12.0.0 wiring + global
ProblemDetailsexception handler +JsonSerializerOptions.UnmappedMemberHandling.Disallow. - AZ-796 — first concrete per-endpoint child of AZ-795: strict validation for
POST /api/satellite/tiles/inventory(9 validation rules); reference-implementation pattern for sibling per-endpoint tasks.
Cycle 7 is a pure-quality cycle: no new endpoints, no new persisted state, no migrations, no new env vars, no container-image changes. The full payload is a contract bump (tile-inventory.md 1.0.0 → 2.0.0 — major because of the field rename) plus a new strict validation surface across the inventory endpoint.
What is shipping
Code changes (committed to dev)
| Commit | Subject |
|---|---|
dceaddc |
[AZ-794] [AZ-795] [AZ-796] Adopt cycle 7 tasks (API quality follow-up) — Step 9 task-adoption commit, autodev state advanced to Step 10. |
865dfdb |
[AZ-794] [AZ-795] [AZ-796] Strict input validation + z/x/y rename — Step 10 implementation in a single batch. |
| pending this commit | [AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-16 sync (test-spec / docs / security / perf / deploy) |
The two no-ticket commits 19c0371 ([no-ticket] Sync .cursor with suite root) and 7d3ba1c (Enhance .cursor documentation and workflows) preceded cycle 7's task-adoption commit and are repo-plumbing changes (.cursor/ skill + rule alignment with the suite root); they do not affect the running api and are not in cycle 7's tracker scope.
All commits are on dev but the cycle-7 sync commit (this one) has not yet been pushed to origin/dev as of this report. Operator runbook step 1 below covers the push.
Database migrations
None. Cycle 7 ships zero migration files. The _docs/02_document/contracts/data-access/tile-storage.md v2.0.0 schema from cycle 6 is unchanged; the inventory endpoint reads from the same tiles table via the same tiles_leaflet_path covering index that cycle 6 introduced.
pgcrypto: still required, still installed automatically by migration 014 from cycle 5 — cycle 7 does not touch the extension surface.
Configuration changes (operator must verify before promoting)
| Setting | Was | Now | Source |
|---|---|---|---|
| No new env vars introduced. | — | — | Cycle 7 carries forward the cycle-6 env contract verbatim (JWT_SECRET ≥ 32B, JWT_ISSUER, JWT_AUDIENCE, GOOGLE_MAPS_API_KEY). |
docker-compose.yml Postgres host port |
5432:5432 |
5433:5432 (host-side bind only) |
Dev-only sibling-project conflict avoidance (a sibling-suite Postgres was already binding 5432 on the dev workstation; moving the host-side bind to 5433 lets both projects run in parallel). In-container port is unchanged (5432) — the api service still resolves postgres:5432 over the compose network. appsettings.Development.json, README.md, AGENTS.md, architecture.md, and _docs/02_document/deployment/containerization.md all aligned with the new host-side number. Staging/prod unaffected — they don't use docker-compose. |
appsettings.Development.json Postgres connection |
Host=localhost;Port=5432 |
Host=localhost;Port=5433 |
Aligns the .NET launch profile (i.e. dotnet run on host, NOT inside docker-compose) with the new host-side bind. Compose-internal connections (api ↔ postgres on the compose network) are unaffected. |
Container image (api service) |
mcr.microsoft.com/dotnet/aspnet:10.0 |
unchanged | No Dockerfile, no .woodpecker/*.yml changes this cycle. |
| Dev TLS dev-cert plumbing (cycle-6 addition) | TLS+ALPN with ./certs/api.{pfx,crt}, update-ca-certificates in test container |
unchanged | Cycle 7 reuses cycle-6's TLS plumbing verbatim. The new scripts/probe_inventory_validation.sh reuses --insecure for the dev cert and reads JWT from env. |
SatelliteProvider.Api.csproj NuGet packages |
(cycle-6 baseline) | + FluentValidation 12.0.0, + FluentValidation.DependencyInjectionExtensions 12.0.0 |
New dependencies for AZ-795's shared validation infra. Both packages have no known CVEs at 12.0.0 (NuGet audit clean, GitHub Security Advisories clean — _docs/05_security/dependency_scan_cycle7.md records the audit trace). Minor bump to 12.1.1 is the only recommended hardening (Low severity D-AZ795-1; bug fixes only, no security-driven advisory). |
Contract changes (consumer-visible)
| Contract | Version | Change | Action for consumers |
|---|---|---|---|
POST /api/satellite/tiles/inventory (tile-inventory.md) |
1.0.0 → 2.0.0 (MAJOR) | Field rename: request body and response payload renamed tileZoom/tileX/tileY → z/x/y (OSM / slippy-map convention; aligns the body shape with the existing URL convention on GET /api/satellite/tiles/{z}/{x}/{y}). Strict validation: HTTP 400 + RFC 7807 ValidationProblemDetails on any of: missing tiles / locationHashes, both arrays present (XOR violation), array empty, array exceeds 5000 entries, any z outside [0, 22], any x outside [0, 2^z), any y outside [0, 2^z), any locationHash not 36-char lowercase UUID. Unknown fields rejected: any body containing a member not declared on the request DTO (e.g. legacy tileZoom, typo'd Z) is rejected via JsonSerializerOptions.UnmappedMemberHandling.Disallow at the deserializer layer (HTTP 400 before the validator runs). |
Sibling repo onboarding (gps-denied-onboard AZ-777 follow-up): any client carrying the legacy tileZoom/tileX/tileY body MUST switch to z/x/y. Any client expecting silent coercion of malformed bodies MUST handle the new 400 path. The Authorization: Bearer … header continues to be required (cycle-6 contract). Sibling-repo tasks for the per-endpoint sweep across the rest of the public API will follow as more AZ-795 children land. |
_docs/02_document/contracts/api/error-shape.md |
(existing baseline, AZ-353 sanitization) | No version bump. Cycle 7 confirms the error shape is RFC 7807-compatible (type/title/status/detail/extensions.errors for ValidationProblemDetails) and that 5xx errors continue to be sanitized via the cycle-6 baseline GlobalExceptionHandler. Two Low findings (F-AZ795-1, F-AZ795-2) note that JsonException.Message and BadHttpRequestException.Message may surface internal .NET type/parameter names in 400 detail strings — auth-gated, no security impact in dev — documented for sanitization in a future cycle. |
Consumers parsing the 400 shape get a stable RFC 7807 envelope; no breaking change to the error contract itself. |
tile-storage.md (data-access contract) |
unchanged at 2.0.0 | Cycle 7 does not touch the schema. The cycle-6 v2.0.0 contract from the AZ-503+AZ-505 joint freeze is preserved verbatim. | No action. |
Container image
- Source:
SatelliteProvider.Api/Dockerfilemulti-stage build, basemcr.microsoft.com/dotnet/aspnet:10.0— unchanged from cycle 5/6. - No new mounts in
docker-compose.yml: the cycle-6 dev-cert mounts (./certs/api.pfx:/app/certs/api.pfx:ro) and the cycle-6 tests-container CA-trust mount remain unchanged. - Verification on dev workstation (local):
docker compose up -d --buildsucceeded for the cycle-7 Step 15 perf run (this session). API healthy onhttps://localhost:18980(swagger 200; anonymous POST/api/satellite/tiles/inventoryreturns 401; v2 schema{"tiles":[{"z":18,"x":...,"y":...}]}returns 200; legacy schema{"tiles":[{"tileZoom":18,...}]}returns 400 — verified viascripts/probe_inventory_validation.sh). - Verification on CI: pending — the cycle-7 sync commit (this one) has not been pushed yet. Operator action: after push, confirm the next Woodpecker
01-test+02-build-pushruns ondevsucceed before promoting. Note that the cycle-7 .NET build usesmcr.microsoft.com/dotnet/sdk:10.0(unchanged) and the integration test container still resolves the dev cert viascripts/run-tests.sh'sensure_dev_certblock; no new CI secret is required. - Multi-arch: unchanged from cycle 6 (
aspnet:10.0is multi-arch by Microsoft).
Verification gates passed in this cycle
| Gate | Result | Evidence |
|---|---|---|
| Step 11 — Functional test suite | PASS | Unit suite 311 tests green (including the new 16-test InventoryRequestValidatorTests covering all 9 rules + the new GlobalExceptionHandlerTests); integration suite green (including the new TileInventoryValidationTests + the TileInventoryTests payload-rename refactor + the IdempotentPostTests adjacent fix where strict deserialization uncovered a long-silent PascalCase fallback bug). The implementation report for this cycle landed at the commit-message level — there is no separate _docs/03_implementation/implementation_report_*_cycle7.md file; cycle 7 was a single-batch cycle and the 865dfdb commit body documents the implementation summary inline (the cycle-7 test-spec sync correctly notes this retrospectively as a process gap to address in cycle 8). |
| Step 12 — Test-Spec Sync | PASS | _docs/02_document/tests/traceability-matrix.md extended with 12 cycle-7 AC rows (AZ-794 ×3, AZ-795 ×3, AZ-796 ×6) + Coverage Summary update; blackbox-tests.md BT-27 added for the AZ-796 9-rule validation surface. |
| Step 13 — Update Docs | PASS | _docs/02_document/architecture.md already carried § 9 Input Validation (AZ-795) from the implementation commit; module-layout updated with cycle-7 file list; tests_unit.md documents the new InventoryRequestValidatorTests + ValidatorTestModuleInitializer; tests_integration.md documents TileInventoryValidationTests + ProblemDetailsAssertions; glossary.md gained entries for "Validation Problem Details", "FluentValidation", "Unmapped Member Handling"; system-flows.md F8 (Inventory Bulk Lookup) expanded with deserializer + validator gates and 13-row Validation Surface table; data_parameters.md § Tile Inventory documents the v2 input schema + constraints; _docs/02_document/ripple_log_cycle7.md captures the doc-side ripple decisions. |
| Step 14 — Security Audit | PASS_WITH_WARNINGS (3 Low findings) | _docs/05_security/security_report_cycle7.md (consolidated) + per-phase reports dependency_scan_cycle7.md, static_analysis_cycle7.md, owasp_review_cycle7.md, infrastructure_review_cycle7.md. Findings: D-AZ795-1 (Low) FluentValidation 12.0.0 → 12.1.1 is a recommended bug-fix bump (no CVE driving it); F-AZ795-1 (Low) JsonException.Message in the 400 detail string may leak the offending .NET type name on deserialization failure (auth-gated, dev-shown only); F-AZ795-2 (Low) BadHttpRequestException.Message similarly may leak the parameter name on malformed-form-input cases (auth-gated). None are blocking; remediation is a sanitizer pass in a follow-up cycle. Architectural wins: mass-assignment prevention (Disallow), uniform 4xx contract (RFC 7807), auth-before-validation order confirmed in Program.cs. |
| Step 15 — Performance Test | PASS | _docs/06_metrics/perf_2026-05-22_cycle7.md. 8/8 scripted scenarios PASS (PT-01..PT-08), exit 0, single default-parameter run. Additionally, a cycle-7 PT-09 smoke probe (/tmp/pt09_smoke.sh, 20 sequential 2500-tile-batch calls using the new z/x/y schema, all-miss path) measured min=27ms, median=44ms, p95=73ms, max=86ms — 13.7× under the AZ-505 AC-4 1000 ms p95 budget. The canonical PT-09 (TileInventoryTests.PerformanceBudget_AC4, all-hit seeded 2500 rows) remains the authoritative gate and is exercised by the integration suite. AZ-794 / AZ-795 / AZ-796 added ≤ 10 ms of validator overhead on a 2500-item batch — well within noise band relative to the cycle-6 PT-09 number (p95=66ms). |
Outstanding leftovers (status this cycle)
_docs/_process_leftovers/is empty as of cycle 7 entry (cycle 6 closed the long-standing perf-harness leftover). Cycle 7 adds zero new leftovers.- Implementation-report process gap (NEW): cycle 7's Step 10 did not produce the expected
_docs/03_implementation/implementation_report_*_cycle7.mdartifact. Thetest-specskill'scycle-updatemode worked around it by reading the task specs + the865dfdbcommit body as the implementation summary. Recommendation: surface as a Step-17 retro lesson; either tighten the implement-skill exit gate (require the report artifact before marking Step 10 complete) or update the test-spec / docs skills' resume protocol to formally consume the commit body when the report is absent.
Recommended follow-up PBIs (out of cycle-7 scope, surfaced for backlog)
| ID | Estimate | Title | Why |
|---|---|---|---|
| (TBD) | 1 SP | Bump FluentValidation 12.0.0 → 12.1.1 |
D-AZ795-1 Low finding. Bug-fix-only release per the FluentValidation 12.x changelog (no CVE driving). Trivial package bump; pairs well with the unchanged Microsoft.IdentityModel.Tokens follow-up below. |
| (TBD) | 2 SP | Sanitize JsonException.Message + BadHttpRequestException.Message before surfacing in ValidationProblemDetails.detail |
F-AZ795-1 + F-AZ795-2 Low findings. Replace the raw Exception.Message with a static string ("Request body is not valid JSON" / "Form value could not be bound") so the 400 path emits no internal .NET type / parameter names. Auth-gated, no security impact in dev — but the production contract should not leak this. |
| (TBD) | 2-3 SP per endpoint | Strict validation sweep for sibling public endpoints (POST /api/satellite/request, POST /api/satellite/route, POST /api/satellite/upload, GET /api/satellite/tiles/latlon, etc.) |
AZ-795 epic continuation. AZ-796 is the reference implementation; the remaining child tasks reuse the same InventoryRequestValidator + ValidationEndpointFilter pattern. Estimate per endpoint depends on the number of validation rules and DTO complexity; expect 2-3 SP each. |
| (TBD) | 1 SP | Implementation-report exit gate for the implement skill |
NEW process gap surfaced in cycle 7 — Step 10 completed without writing _docs/03_implementation/implementation_report_*_cycle7.md. The downstream skills (test-spec cycle-update, document task mode) compensate via task-spec + commit-body reading, but the report artifact is part of the autodev contract. Tighten the implement-skill exit gate to require the report file. |
| (TBD) | 3 SP (recheck per cycle) | Bump Microsoft.IdentityModel.Tokens / System.IdentityModel.Tokens.Jwt 7.0.3 → 7.1.2+ |
Carry-over from cycles 3-6 (NU1902 moderate severity advisory). Unchanged from cycle 6. |
| (TBD) | 1 SP | Bump Microsoft.NET.Test.Sdk 17.8.0 → 17.13.0+ |
Carry-over D2-cy4 (transitive NuGet.Frameworks flag). Unchanged from cycles 4-6. |
| (TBD) | 3 SP | Migrate WithOpenApi(...) callsites to ASP.NET Core 10 minimal-API metadata extensions |
Carry-over from cycles 4-6 (ASPDEPR002 warnings). Unchanged from cycles 4-6. |
| (TBD) | 1 SP (recheck per cycle) | Serilog.AspNetCore 8.0.3 → 10.x |
Carry-over from cycles 4-6. Unchanged from cycle 6 — no 10.x line published as of cycle 7 entry; re-check at cycle-8 start. |
| (TBD) | 2 SP | Inventory endpoint estimatedBytes field |
Deferred per AZ-505 Outcome bullet 1 — unchanged from cycle 6 carry-over. |
| (TBD) | 5 SP | HTTP/3 / QUIC dev listener | Deferred per AZ-505 Excluded list — unchanged from cycle 6 carry-over. |
| (TBD) | 1 SP | Deployment runbook: ingress TLS termination + HTTP/2 forwarding | Carry-over from cycle 6 — unchanged. |
| (TBD) | 1 SP | tile-storage.md consumer audit (post v2.0.0) |
Carry-over from cycle 6 — unchanged. |
Admin team iss/aud confirmation (carried from cycle 3) remains OPEN as a long-standing ops-side gap; still required before promoting beyond dev. Unchanged from cycle 6.
Operator runbook for promoting to staging / production
- Push the cycle-7 sync commit + this deploy report to
origin/dev. Confirm Woodpecker01-testruns green ondev(no new CI secret required; dev-cert plumbing is unchanged from cycle 6). - No migration in this cycle. The
tilesschema is unchanged.pgcryptoalready installed since cycle 5; no new extension dependency. - Deploy the new
dev-arm(and amd64) image. The image base and Dockerfile are unchanged from cycle 6; the only build-output difference is the inclusion of the two newFluentValidation12.0.0 assemblies. Container startup performance / cold-start latency is unaffected (FluentValidation registration is one-shot at DI build time). - Smoke-test (production) — note that cycle 7 introduces a breaking contract change on the inventory endpoint; the smoke must use the v2 schema:
/swagger(expect 200/301),/api/satellite/region/<random>(expect 401, JWT enforcement) — unchanged from cycle 6.- v2 inventory body (positive case):
POST /api/satellite/tiles/inventorywith a freshly-minted JWT, body{"tiles":[{"z":18,"x":158485,"y":91707}]}— expect 200 with one entry whosepresentfield reflects whether that tile exists in the target environment. - Legacy body (negative case): same endpoint with body
{"tiles":[{"tileZoom":18,"tileX":158485,"tileY":91707}]}— expect 400 with an RFC 7807ValidationProblemDetailsenvelope. This confirms the new strict deserializer is active in the deployed image. - Validator surface (negative case): same endpoint with body
{"tiles":[{"z":99,"x":0,"y":0}]}— expect 400 with the validator surface namingzas out-of-range. - Cycle-6 smoke (
POST /api/satellite/tiles/uav) unchanged.
- Verify HTTP/2 negotiation against the production ingress (one-off, not a regression test): unchanged from cycle 6 (
curl --http2 -sv https://<prod-host>/api/satellite/region/<id>should log* Using HTTP2and a Bearer-rejected 401). - No env-var change to coordinate. Cycle 7 doesn't introduce any new app config. The dev-only Postgres host-port move (5432 → 5433) is
docker-compose.yml-only and never reaches a non-dev environment. - Consumer coordination: notify all known consumers of the inventory endpoint of the v2 contract bump BEFORE the production deploy. Today's known consumers:
- gps-denied-onboard
TileDownloader(sibling repo): AZ-777 Phase 1 — the originating ticket already flagged the v2 schema; coordinate the cut-over flag flip with the onboard team. The onboard side'sc11.use_bulk_list_endpoint=trueflag (introduced in cycle 6) must also know which schema variant to emit; this is the onboard-side AZ-777 follow-up. - Any direct curl / Postman clients the ops team uses for smoke tests: the v1 body shape MUST be updated.
- gps-denied-onboard
- Roll-forward plan: if a regression appears post-deploy, the rollback target is the cycle-6 close
dev-armtag (built fromaf66135). The cycle-7 changes are pure-code (no migration, no schema change), so rolling back is safe and idempotent. Note that any consumer that already migrated to the v2 schema will receive an unexpected 200 from the rolled-back image withtileZoom:0, tileX:0, tileY:0echoed back (the cycle-6 silent-coercion bug). Coordinate any rollback with the same consumer set notified in step 7. - Outstanding ops-side gap (long-standing, NOT new in cycle 7): admin team
iss/audconfirmation before promoting beyonddev. Unchanged from cycles 3-6 runbooks.
Differences vs. cycle 6 deploy
- NEW: a contract MAJOR bump (
tile-inventory.md1.0.0 → 2.0.0) — cycle 6 only added the inventory contract; cycle 7 is the first revision to it. - NEW: strict input validation surface — FluentValidation 12.0.0 + global ProblemDetails handler +
UnmappedMemberHandling.Disallow. Cycle 6 had no validation layer beyond model-binding's silent coercion. - NEW: two NuGet additions (
FluentValidation12.0.0,FluentValidation.DependencyInjectionExtensions12.0.0). - NEW: a dev-only host-port move (Postgres
5432 → 5433) for sibling-project conflict avoidance. Compose-internal traffic unchanged. Staging/prod unaffected. - NEW: cycle-7 security audit ran (Step 14: PASS_WITH_WARNINGS with 3 Low findings) — cycle 6's Step 14 was skipped by the user.
- NEW (process): cycle-7 Step 10 shipped without an explicit
implementation_report_*_cycle7.mdartifact; downstream skills compensated by reading the task specs + commit body. Recommended as a Step 17 retro lesson. - UNCHANGED: container image base (
aspnet:10.0), CI image (sdk:10.0), all env vars, all multi-arch tags, the cycle-4-and-earlier carry-over follow-up PBIs, the dev TLS cert plumbing, the cycle-6 Leaflet covering index (tiles_leaflet_path),pgcryptoextension state. - NO MIGRATION: cycle 6 shipped migration
015_AddTilesLeafletPathIndex.sql; cycle 7 ships none. Thetilesschema is unchanged. - NO NEW ENDPOINTS: cycle 6 added one new endpoint; cycle 7 modifies the contract of an existing endpoint without adding new routes.
- NO HTTP/2 / TLS LAYER CHANGES: cycle 6 introduced TLS+ALPN to the dev listener; cycle 7 leaves the listener untouched and reuses cycle-6's plumbing for the smoke / validation probe (
scripts/probe_inventory_validation.sh).