Skip to content

fix(CC-ERATE-000056B-FIX): force deterministic port binding in validation script#52

Open
steven-dracker wants to merge 11 commits intomainfrom
feature/fix-postgres-validation-port-binding
Open

fix(CC-ERATE-000056B-FIX): force deterministic port binding in validation script#52
steven-dracker wants to merge 11 commits intomainfrom
feature/fix-postgres-validation-port-binding

Conversation

@steven-dracker
Copy link
Copy Markdown
Owner

Summary

Fixes the health-check failure in scripts/validate-postgres-import.sh where the app was binding to port 5075 while the script was probing port 5080.

Root cause

ASPNETCORE_URLS was exported to the shell environment before dotnet run, but launchSettings.json (applicationUrl: http://localhost:5075) was still winning despite --no-launch-profile being present. Inlining the variable directly on the process invocation makes it the definitively first-class value for that process.

Changes (surgical — script only)

  • BASE_URL changed from http://localhost:5080http://127.0.0.1:5080
  • ASPNETCORE_URLS moved from export to inline on dotnet run invocation
  • Health-check curl hardened to http://127.0.0.1:${APP_PORT}/health
  • Script logs Forcing ASPNETCORE_URLS=http://127.0.0.1:5080 for observability
  • All subsequent ${BASE_URL} references inherit the 127.0.0.1 address

No application code changes

Zero changes to Program.cs, appsettings.json, migrations, or any project file.

🤖 Generated with Claude Code

steven-dracker and others added 11 commits March 30, 2026 09:39
Introduces DatabaseProvider config key ("Sqlite" | "Postgres") alongside
Npgsql.EntityFrameworkCore.PostgreSQL 8.0.11. Startup branches on provider:
SQLite continues to use Migrate(); Postgres uses EnsureCreated() to bypass
SQLite-specific PRAGMA migrations. Provider name is logged at startup.
Default remains SQLite — no behaviour change unless DatabaseProvider=Postgres.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard SQLite-specific PRAGMA journal_mode=WAL in migration
  20260315000001 with ActiveProvider.Contains("Sqlite") check
- Add Npgsql.EntityFrameworkCore.PostgreSQL 8.0.11 to Infrastructure
  project for migration design-time support
- Remove EnsureCreated() from startup — both providers now use Migrate()
- Update startup log: "Database provider: {Provider} — applying migrations"

SQLite path: unchanged, Migrate() runs as before.
Postgres path: Migrate() runs cleanly without hitting PRAGMA.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ASPNETCORE_URLS was being overridden by launchSettings.json (port 5075)
when the app started via `dotnet run`. Adding --no-launch-profile causes
ASPNETCORE_URLS=http://localhost:5080 to be respected, matching the
health-check poll target in the script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…validation script

Three bugs fixed:
- ImportJob.Status is an int (2=Succeeded), not the string "Completed"
- ImportJob has no recordsInserted field — idempotency now proven via
  psql COUNT(*) before/after rerun (count must be stable)
- Row count spot-check was querying "Entities" table; correct table
  for EPC entity import is "EpcEntities"

Also adds explicit "Row write confirmed: N records" assertion to
fail fast if the import completed but wrote zero rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion script

Two changes:
1. Inline env vars directly on the dotnet invocation (VAR=val dotnet run ...)
   rather than exporting them beforehand — prevents any inherited-env override
   from winning over the script's intent.
2. Use 127.0.0.1 consistently (BASE_URL, ASPNETCORE_URLS, health-check curl)
   rather than 'localhost' — eliminates any IPv4/IPv6 resolution ambiguity.

Script now logs "Forcing ASPNETCORE_URLS=http://127.0.0.1:5080" before
startup, making the intended binding explicit in output.

Root cause: ASPNETCORE_URLS export was set in the shell environment but
launchSettings.json (applicationUrl: http://localhost:5075) was still
winning during `dotnet run` despite --no-launch-profile being present in
a prior commit. Inlining the var on the process invocation eliminates
the ambiguity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…re startup

Two issues when re-running the script:
1. A leftover dotnet process from a prior run holds port 5080 — the new
   app never starts, health check hits the stale process, and passes
   immediately (0s wait).
2. /tmp/erate-pg-app.log retains output from the prior run — the
   provider log grep finds nothing (prior run had no Postgres config).

Fix: lsof-kill any process on APP_PORT before launching, and truncate
the log file so the provider check only reads from the current process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lsof -ti returns one PID per line. kill "$STALE_PID" with a quoted
newline-containing variable passes the whole string as one invalid arg
— both processes survive and the health check hits the stale one.

Fix: pipe through xargs so each PID is passed as a separate argument.
Also replace fixed sleep(2) with a poll loop that waits until lsof
confirms the port is actually free (up to 10s, SIGKILL escalation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace -sf (silent+fail-on-HTTP-error) with separate body/status capture
so the actual response is always visible on failure. Adds --max-time 600
to both import and rerun curls — the EPC entity download is ~100k rows
and can exceed curl's default operation time.

On failure now shows: HTTP status, curl exit code, elapsed time, response
body, and app log tail — making the root cause diagnosable from script output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLite migrations used .Annotation("Sqlite:Autoincrement", true) for
integer primary keys. Postgres ignores this annotation and creates a
plain INTEGER NOT NULL with no sequence, causing null constraint
violations on every INSERT.

Fix: add .Annotation("Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) alongside the
Sqlite annotation on all 13 Id columns across 11 migration files.
This tells Postgres to create the column as GENERATED BY DEFAULT AS
IDENTITY (equivalent to SERIAL), while SQLite behavior is unchanged.

Affected migrations:
- InitialCreate (Applicants, ImportJobs)
- AddEpcEntity (EpcEntities)
- AddFundingCommitments (FundingCommitments)
- AddServiceProviders (ServiceProviders)
- AddForm471Applications (Form471Applications)
- AddEntity (Entities)
- AddDisbursements (Disbursements)
- AddApplicantYearCommitmentSummary
- AddApplicantYearDisbursementSummary
- AddApplicantYearRiskSummary
- AddConsultantTables (ConsultantApplications, ConsultantFrnStatuses)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The default HttpClient timeout of 100 seconds is too short for large
USAC CSV downloads (EPC entities, funding commitments, disbursements).
Downloads were failing with "Connection reset by peer" after ~106s.

Sets a 30-minute timeout on the UsacCsvClient HttpClient registration.
This resolves TD-001 (HttpClient timeout handling).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Updated CLAUDE.md to reflect long pause state
- Updated chatgpt-primer.md for future context restoration
- Added CC-ERATE-000056B handoff document
- No application code changes included (Program.cs intentionally excluded)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant