Compare commits
1 Commits
mai/cronus
...
mai/knuth/
| Author | SHA1 | Date | |
|---|---|---|---|
| a6cf6ff4c9 |
@@ -1,242 +0,0 @@
|
||||
# Paliad CI gate (t-paliad-282 / m/paliad#114).
|
||||
#
|
||||
# Single workflow, two purposes:
|
||||
#
|
||||
# - On every push: gate tier — build + unit + migration smoke. Red gate
|
||||
# means no further work and (on main) no deploy.
|
||||
# - On push to main with gate green: deploy step — calls the Dokploy
|
||||
# compose-deploy API for paliad's compose Zx147ycurfYagKRl_Zzyo, then
|
||||
# polls /health/ready until the new container reports 200.
|
||||
#
|
||||
# The deploy step REPLACES the previous Gitea-push → Dokploy webhook path
|
||||
# (per m's Q11.4 pick: soft-launch with both alive for ~1 week, then
|
||||
# disable the Dokploy auto-deploy toggle). Soft-launch leaves Dokploy's
|
||||
# autoDeploy=true intact today — the workflow's deploy step is additive
|
||||
# and idempotent (Dokploy's deploy is itself idempotent).
|
||||
#
|
||||
# Catches the three failure classes from 2026-05-25:
|
||||
#
|
||||
# - brunel slot collision (~13:20) — TestMigrations_NoDuplicateSlot,
|
||||
# pure unit, no DB needed.
|
||||
# - hermes dropped-col refs (~16:05) — TestBootSmoke, applies all NEW
|
||||
# migrations (those not in the snapshot) end-to-end against a
|
||||
# scratch DB restored from internal/db/testdata/prod-snapshot.sql.
|
||||
# - mig 129 42501 ownership (~14:56→) — TestMigrations_EndToEndAsAppRole,
|
||||
# applies new migrations as the prod-shaped `postgres` role (which
|
||||
# is NOT a superuser on supabase/postgres — same shape as
|
||||
# youpc-supabase prod, see internal/db/testdata/README.md).
|
||||
#
|
||||
# Snapshot approach: dump paliad schema + applied_migrations rows from
|
||||
# prod, commit them. CI restores → ApplyMigrations sees existing migs as
|
||||
# applied, only runs NEW migs (the ones this PR adds). This sidesteps the
|
||||
# fresh-DB idempotence requirement on historical migrations (some of
|
||||
# which use raw COMMIT or pre-installed extensions and can't be replayed
|
||||
# from scratch). To refresh: `make refresh-snapshot`.
|
||||
#
|
||||
# Design: docs/design-cicd-pre-deploy-gate-2026-05-25.md (cronus inventor
|
||||
# shift, t-paliad-282).
|
||||
|
||||
name: Paliad CI gate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'mai/**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.24'
|
||||
BUN_VERSION: '1.2'
|
||||
|
||||
jobs:
|
||||
# Gate job 1 — pure build. Catches go/bun build breakage that local
|
||||
# `go build` would catch but which a worker might have skipped before
|
||||
# pushing. Fast (~60 s) so a red here surfaces immediately.
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: go build
|
||||
run: go build ./...
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
|
||||
- name: bun install + build
|
||||
working-directory: frontend
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
|
||||
# Gate job 2 — Go test suite + migration smoke against snapshot-restored
|
||||
# scratch DB.
|
||||
#
|
||||
# The Postgres service container uses the same supabase/postgres image
|
||||
# as youpc-supabase prod. The CI scratch DB starts empty; a setup step
|
||||
# installs pg_trgm + restores the snapshot. After restore, paliad
|
||||
# schema is at HEAD-of-snapshot and applied_migrations covers every
|
||||
# migration up to (and including) the snapshot's max version.
|
||||
#
|
||||
# ApplyMigrations called in TestBootSmoke / TestMigrations_EndToEndAsAppRole
|
||||
# sees the snapshot's applied set, finds whatever NEW migrations this
|
||||
# PR added on top, and applies only those. The role-split smoke runs as
|
||||
# `postgres` (which is NOT a superuser on supabase/postgres, matching
|
||||
# the prod role topology) — any new migration that needs supabase_admin
|
||||
# privilege fails here as it would in prod.
|
||||
test-go:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
# supabase/postgres baked-in auth schema + supabase role topology
|
||||
# matches youpc-supabase prod. `postgres` here is NOT a superuser
|
||||
# (verified live: \du postgres shows "Create role, Create DB,
|
||||
# Replication, Bypass RLS" — no Superuser). This is the prod-shaped
|
||||
# role the deploy uses.
|
||||
postgres:
|
||||
image: supabase/postgres:15.8.1.060
|
||||
env:
|
||||
POSTGRES_PASSWORD: ci
|
||||
POSTGRES_DB: paliad_scratch
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install postgresql-client
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq postgresql-client
|
||||
|
||||
# Snapshot restore. Two prep steps as supabase_admin (the actual
|
||||
# superuser): GRANT CREATE so the `postgres` role can later create
|
||||
# schemas if a new mig needs it; install pg_trgm so the snapshot's
|
||||
# trigram indexes restore. Snapshot itself loads as `postgres`.
|
||||
- name: Provision + restore snapshot
|
||||
env:
|
||||
PGPASSWORD: ci
|
||||
run: |
|
||||
set -euo pipefail
|
||||
psql -h localhost -U supabase_admin -d paliad_scratch -v ON_ERROR_STOP=1 \
|
||||
-c "GRANT CREATE ON DATABASE paliad_scratch TO postgres;" \
|
||||
-c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
|
||||
psql -h localhost -U postgres -d paliad_scratch -v ON_ERROR_STOP=1 \
|
||||
-f internal/db/testdata/prod-snapshot.sql
|
||||
|
||||
# Pre-flight: catches brunel slot collision in seconds, no DB
|
||||
# contact (still useful even though the test-go job has Postgres
|
||||
# running, because the failure mode is independent).
|
||||
- name: Migration coordination check
|
||||
run: go test -count=1 -run TestMigrations_NoDuplicateSlot ./internal/db/
|
||||
|
||||
# Role-split end-to-end apply. Connects as `postgres` (NOT a
|
||||
# superuser on supabase/postgres) and runs ApplyMigrations against
|
||||
# the snapshot-restored DB. Existing migs are skipped (already in
|
||||
# applied_migrations); NEW migs in this PR apply here. If a new
|
||||
# migration assumes supabase_admin privilege, fails with the same
|
||||
# 42501 error class that took paliad.de offline on 2026-05-25.
|
||||
- name: Migration end-to-end (deploy role)
|
||||
env:
|
||||
TEST_APP_DATABASE_URL: postgres://postgres:ci@localhost:5432/paliad_scratch?sslmode=disable
|
||||
run: go test -count=1 -run TestMigrations_EndToEndAsAppRole ./internal/db/
|
||||
|
||||
# Boot smoke. Confirms ApplyMigrations succeeds + applied set
|
||||
# matches on-disk set + /healthz returns 200 + /health/ready
|
||||
# returns 200 (the live-pool variant via TestHealthReady_Live).
|
||||
- name: Boot smoke + readiness
|
||||
env:
|
||||
TEST_DATABASE_URL: postgres://postgres:ci@localhost:5432/paliad_scratch?sslmode=disable
|
||||
run: go test -count=1 -run 'TestBootSmoke|TestHealthReady_Live' ./cmd/server/
|
||||
|
||||
# Full Go test suite WITHOUT TEST_DATABASE_URL so live-DB service
|
||||
# tests skip (same shape as a developer laptop without a scratch
|
||||
# DB). Live-DB tests in internal/services/* will be activated by a
|
||||
# follow-up shift once the snapshot is verified stable across
|
||||
# multiple PRs — they need investigation against supabase/postgres
|
||||
# 15.8 (parameter type inference differs subtly from youpc-supabase).
|
||||
- name: go test ./... (pure + skip-on-no-DB)
|
||||
run: go test -count=1 ./internal/... ./cmd/...
|
||||
|
||||
# Deploy step. Only runs on push to main and only after both gate jobs
|
||||
# are green. Calls Dokploy's compose.deploy with the paliad compose ID
|
||||
# (Zx147ycurfYagKRl_Zzyo) and polls /health/ready until it returns 200
|
||||
# or times out.
|
||||
#
|
||||
# Skipped on PR / feature branch pushes — those run the gate tier as
|
||||
# a status check but don't trigger a prod deploy. Dokploy's existing
|
||||
# autoDeploy=true webhook continues to fire during the soft-launch
|
||||
# window (per Q11.4); it can be disabled in the Dokploy UI once this
|
||||
# workflow has gated ≥5 successful green deploys.
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, test-go]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Trigger Dokploy compose deploy
|
||||
env:
|
||||
DOKPLOY_KEY: ${{ secrets.DOKPLOY_TOKEN }}
|
||||
DOKPLOY_API: http://100.99.98.201:3000/api/trpc
|
||||
COMPOSE_ID: Zx147ycurfYagKRl_Zzyo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${DOKPLOY_KEY:-}" ]; then
|
||||
echo "ERROR: DOKPLOY_TOKEN secret is not configured."
|
||||
echo " Set the secret in Gitea repo settings before this step can deploy."
|
||||
exit 2
|
||||
fi
|
||||
echo "==> POST compose.deploy"
|
||||
curl -sS --connect-timeout 5 --max-time 30 \
|
||||
-X POST \
|
||||
-H "x-api-key: $DOKPLOY_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"json\":{\"composeId\":\"$COMPOSE_ID\"}}" \
|
||||
"$DOKPLOY_API/compose.deploy"
|
||||
echo
|
||||
|
||||
- name: Wait for /health/ready
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "==> polling https://paliad.de/health/ready"
|
||||
# Up to 5 minutes (60 × 5 s) — paliad's cold-start is normally
|
||||
# ≤30 s; the longer budget covers slow image pulls + migration
|
||||
# apply.
|
||||
for i in $(seq 1 60); do
|
||||
status=$(curl -sS --connect-timeout 3 --max-time 5 \
|
||||
-o /dev/null -w '%{http_code}' \
|
||||
https://paliad.de/health/ready || echo "000")
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "ready after ${i} poll(s)"
|
||||
exit 0
|
||||
fi
|
||||
echo " [$i/60] status=$status — sleeping 5s"
|
||||
sleep 5
|
||||
done
|
||||
echo "ERROR: /health/ready did not return 200 within 5 minutes."
|
||||
echo " The deploy fired but the new container is not serving."
|
||||
echo " Investigate: ssh mlake 'docker logs --tail 50 compose-transmit-multi-byte-driver-v7jth9-web-1'"
|
||||
exit 1
|
||||
72
Makefile
72
Makefile
@@ -21,24 +21,18 @@
|
||||
# the test runner's working dirs. None of them touch internal/db/migrations/
|
||||
# files.
|
||||
|
||||
.PHONY: help verify-migrations verify-mig verify-mig-app test test-go test-frontend refresh-snapshot
|
||||
.PHONY: help verify-migrations verify-mig test test-go
|
||||
|
||||
help:
|
||||
@echo "Paliad — developer targets"
|
||||
@echo ""
|
||||
@echo " verify-migrations Dry-run pending migrations + boot smoke (needs TEST_DATABASE_URL)"
|
||||
@echo " verify-mig Alias for verify-migrations"
|
||||
@echo " verify-mig-app End-to-end migration smoke as non-superuser role"
|
||||
@echo " (needs TEST_APP_DATABASE_URL — t-paliad-282 / m/paliad#114)"
|
||||
@echo " test Short test pass — covers gate tier"
|
||||
@echo " test-go Full Go suite with race detector"
|
||||
@echo " test-frontend Frontend bun:test suite"
|
||||
@echo ""
|
||||
@echo "Set TEST_DATABASE_URL to enable live-DB tests. Example:"
|
||||
@echo " export TEST_DATABASE_URL=postgres://paliad:...@localhost:11833/paliad_test"
|
||||
@echo ""
|
||||
@echo "Set TEST_APP_DATABASE_URL to enable the role-split smoke. Example:"
|
||||
@echo " export TEST_APP_DATABASE_URL=postgres://paliad_app:...@localhost:5432/paliad_scratch"
|
||||
|
||||
# Gate target — the test that would have caught mig 098 / mig 099 before
|
||||
# deploy. Combines:
|
||||
@@ -77,67 +71,3 @@ test:
|
||||
# (full suite, not per-PR).
|
||||
test-go:
|
||||
go test -race ./...
|
||||
|
||||
# Frontend bun:test suite. Runs the 4 existing pure-TS tests today; will
|
||||
# grow as mendel's Slice 3 (frontend test infill) lands.
|
||||
test-frontend:
|
||||
cd frontend && bun test
|
||||
|
||||
# Role-split end-to-end migration smoke — the catch for the mig 129 42501
|
||||
# ownership class (m/paliad#114). Runs ApplyMigrations as a non-superuser
|
||||
# role against TEST_APP_DATABASE_URL. Fails the build if any migration
|
||||
# assumes more privilege than the deploy role has.
|
||||
#
|
||||
# Developer setup (local):
|
||||
# psql -c "CREATE ROLE paliad_app LOGIN PASSWORD 'ci' NOSUPERUSER;"
|
||||
# psql -c "CREATE DATABASE paliad_scratch OWNER paliad_app;"
|
||||
# export TEST_APP_DATABASE_URL=postgres://paliad_app:ci@localhost:5432/paliad_scratch
|
||||
verify-mig-app:
|
||||
@if [ -z "$$TEST_APP_DATABASE_URL" ]; then \
|
||||
echo "ERROR: TEST_APP_DATABASE_URL is not set."; \
|
||||
echo " The role-split migration smoke cannot run without a non-superuser scratch DB."; \
|
||||
echo " See Makefile comments above this target for setup."; \
|
||||
exit 2; \
|
||||
fi
|
||||
go test -count=1 -run TestMigrations_EndToEndAsAppRole ./internal/db/
|
||||
|
||||
# Refresh the prod schema snapshot used by CI's migration smoke
|
||||
# (t-paliad-282 / m/paliad#114). Connects to youpc-supabase prod, dumps
|
||||
# the paliad schema + applied_migrations rows, strips rows beyond the
|
||||
# current branch's max on-disk version, and writes
|
||||
# internal/db/testdata/prod-snapshot.sql.
|
||||
#
|
||||
# When to refresh:
|
||||
# - After merging a PR that added a new migration to main.
|
||||
# - When CI's migration smoke starts spuriously failing because the
|
||||
# snapshot's applied set diverges from on-disk by more than this
|
||||
# branch's worth of new migs.
|
||||
#
|
||||
# Requires PALIAD_PROD_DATABASE_URL env var (a Postgres URL with
|
||||
# pg_dump rights on youpc-supabase). Example:
|
||||
# export PALIAD_PROD_DATABASE_URL='postgres://postgres:PW@100.99.98.201:11833/postgres'
|
||||
refresh-snapshot:
|
||||
@if [ -z "$$PALIAD_PROD_DATABASE_URL" ]; then \
|
||||
echo "ERROR: PALIAD_PROD_DATABASE_URL is not set."; \
|
||||
echo " Refresh requires read access to youpc-supabase prod."; \
|
||||
exit 2; \
|
||||
fi
|
||||
@echo "==> dumping paliad schema (no owner, no privs)..."
|
||||
@pg_dump --schema-only --schema=paliad --no-owner --no-privileges \
|
||||
--no-publications --no-subscriptions \
|
||||
"$$PALIAD_PROD_DATABASE_URL" > internal/db/testdata/prod-snapshot.sql.tmp
|
||||
@echo "==> appending applied_migrations rows..."
|
||||
@pg_dump --data-only --table=paliad.applied_migrations \
|
||||
--no-owner --no-privileges \
|
||||
"$$PALIAD_PROD_DATABASE_URL" >> internal/db/testdata/prod-snapshot.sql.tmp
|
||||
@echo "==> stripping pg16 \\restrict / \\unrestrict commands for pg15 compat..."
|
||||
@sed -i.bak '/^\\restrict /d; /^\\unrestrict /d' internal/db/testdata/prod-snapshot.sql.tmp
|
||||
@rm -f internal/db/testdata/prod-snapshot.sql.tmp.bak
|
||||
@echo "==> stripping applied_migrations rows beyond branch's max on-disk version..."
|
||||
@MAX_VER=$$(ls internal/db/migrations/*.up.sql | xargs -I{} basename {} | sed 's/_.*//' | sort -n | tail -1); \
|
||||
awk -v max=$$MAX_VER ' \
|
||||
/^[0-9]+\t/ { split($$0, a, "\t"); if (a[1]+0 > max) next; } \
|
||||
{ print } \
|
||||
' internal/db/testdata/prod-snapshot.sql.tmp > internal/db/testdata/prod-snapshot.sql
|
||||
@rm internal/db/testdata/prod-snapshot.sql.tmp
|
||||
@wc -l internal/db/testdata/prod-snapshot.sql
|
||||
|
||||
@@ -165,7 +165,6 @@ func main() {
|
||||
sysAuditSvc := services.NewSystemAuditLogService(pool)
|
||||
checklistTemplateSvc := services.NewChecklistTemplateService(pool, checklistCatalogSvc, sysAuditSvc, users)
|
||||
svcBundle = &handlers.Services{
|
||||
Pool: pool,
|
||||
Project: projectSvc,
|
||||
Team: teamSvc,
|
||||
PartnerUnit: partnerUnitSvc,
|
||||
|
||||
@@ -98,51 +98,6 @@ func TestBootSmoke(t *testing.T) {
|
||||
if body := strings.TrimSpace(rec.Body.String()); body != "ok" {
|
||||
t.Errorf("GET /healthz: body=%q; want \"ok\"", body)
|
||||
}
|
||||
|
||||
// (4) Readiness probe. With a nil Services bundle the endpoint MUST
|
||||
// report 503 — that's the contract documented in handlers/handlers.go.
|
||||
// A separate svc-with-Pool case is exercised in TestHealthReady (live).
|
||||
rec = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("GET /health/ready (nil svc): status=%d; want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthReady_Live asserts the readiness probe answers 200 when the
|
||||
// pool is reachable, 503 when it isn't. Requires TEST_DATABASE_URL.
|
||||
//
|
||||
// Why a separate test: TestBootSmoke runs Register with svc=nil to keep
|
||||
// its setup minimal; the pool-reachable path needs the pool wired in
|
||||
// through svc.Pool. Two tests, two assertions, no entanglement.
|
||||
func TestHealthReady_Live(t *testing.T) {
|
||||
url := os.Getenv("TEST_DATABASE_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set — skipping live readiness probe")
|
||||
}
|
||||
|
||||
if err := db.ApplyMigrations(url); err != nil {
|
||||
t.Fatalf("db.ApplyMigrations: %v", err)
|
||||
}
|
||||
pool, err := db.OpenPool(url)
|
||||
if err != nil {
|
||||
t.Fatalf("open pool: %v", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
authClient := auth.NewClient("https://test.invalid", "anon-key", []byte("test-secret"))
|
||||
handlers.Register(mux, authClient, "", &handlers.Services{Pool: pool})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("GET /health/ready (live pool): status=%d, body=%q; want 200", rec.Code, rec.Body.String())
|
||||
}
|
||||
if body := strings.TrimSpace(rec.Body.String()); body != "ready" {
|
||||
t.Errorf("GET /health/ready (live pool): body=%q; want \"ready\"", body)
|
||||
}
|
||||
}
|
||||
|
||||
// embeddedMigrationVersions returns every N where N_*.up.sql exists in
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
# CI/CD runner setup — paliad
|
||||
|
||||
**Companion to:** `docs/design-cicd-pre-deploy-gate-2026-05-25.md` (Slice A, t-paliad-282 / m/paliad#114)
|
||||
**Date:** 2026-05-25
|
||||
**Audience:** mlake / mriver admin (m or head)
|
||||
|
||||
Slice A's `.gitea/workflows/test.yaml` requires (a) at least one online Gitea Actions runner and (b) a Dokploy API token wired as a repo secret. Both are one-time setup actions that paliad's source tree cannot perform itself — they live on infra-side. This doc lists them so the workflow can go green on its first run.
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight: what already exists
|
||||
|
||||
Verified live (2026-05-25 cronus inventor shift):
|
||||
|
||||
- Gitea 1.24.4 on `mgit.msbls.de`, `has_actions: true` on `m/paliad`.
|
||||
- `/api/v1/admin/actions/runners` reports **2 runners** registered. They are likely the shared runners used by `m/mGreen` and `m/mGeo` (both have `.gitea/workflows/deploy.yml` with `runs-on: self-hosted`).
|
||||
- `m/paliad/actions/tasks` reports `total_count=0` — paliad has never run a workflow yet.
|
||||
|
||||
The existing runners may already be capable of running paliad's workflow without further setup. The verification step (§3) below tells you whether they are.
|
||||
|
||||
---
|
||||
|
||||
## 1. Runner placement decision (m's Q11.1)
|
||||
|
||||
m's pick: **mriver**.
|
||||
|
||||
Rationale: mriver hosts the mai worker fleet but workers spend most of their time waiting on Anthropic. mlake's Dokploy + Swarm workload is more contended. A new runner on mriver adds the least pressure to either box.
|
||||
|
||||
If mriver is offline or saturated when CI first fires, fall back to the existing mlake-side runners (they're already registered; no provisioning needed).
|
||||
|
||||
---
|
||||
|
||||
## 2. One-time setup (admin steps)
|
||||
|
||||
### 2.1 Register a new Gitea Actions runner on mriver
|
||||
|
||||
```bash
|
||||
# On mriver, as m:
|
||||
# 1. Download the act_runner binary (matching Gitea 1.24.x)
|
||||
curl -L -o /usr/local/bin/act_runner \
|
||||
https://gitea.com/gitea/act_runner/releases/download/v0.2.13/act_runner-0.2.13-linux-amd64
|
||||
chmod +x /usr/local/bin/act_runner
|
||||
|
||||
# 2. Get a runner registration token. In the Gitea UI:
|
||||
# /admin → Actions → Runners → "Create new Runner"
|
||||
# (or org-scope: /m/paliad/settings/actions/runners)
|
||||
# Copy the token.
|
||||
|
||||
# 3. Register
|
||||
mkdir -p ~/act_runner && cd ~/act_runner
|
||||
act_runner register --no-interactive \
|
||||
--instance https://mgit.msbls.de \
|
||||
--token <REGISTRATION_TOKEN> \
|
||||
--name mriver-paliad-1 \
|
||||
--labels ubuntu-latest:docker://node:20-bookworm
|
||||
|
||||
# 4. Run as a systemd unit (preferred) or as a session daemon
|
||||
# Systemd unit example: /etc/systemd/system/act_runner.service
|
||||
# [Unit]
|
||||
# Description=Gitea Actions runner
|
||||
# After=network.target
|
||||
# [Service]
|
||||
# User=m
|
||||
# WorkingDirectory=/home/m/act_runner
|
||||
# ExecStart=/usr/local/bin/act_runner daemon
|
||||
# Restart=on-failure
|
||||
# [Install]
|
||||
# WantedBy=multi-user.target
|
||||
sudo systemctl enable --now act_runner
|
||||
sudo systemctl status act_runner
|
||||
```
|
||||
|
||||
**Why `ubuntu-latest:docker://node:20-bookworm` for the label?** Gitea Actions' `runs-on: ubuntu-latest` resolves via the runner's label map. Mapping it to a Docker image gives the workflow a sandbox with Docker available — required for our Postgres service container in `test.yaml`. mriver should have Docker (for `paliadin-shim`); if not, install it.
|
||||
|
||||
### 2.2 Register the Dokploy API token as a repo secret
|
||||
|
||||
The workflow's `deploy` job needs `secrets.DOKPLOY_TOKEN`. Use the existing project-wide Dokploy API key (the one stored in `~/.claude/skills/mai-dokploy/SKILL.md`).
|
||||
|
||||
In the Gitea UI:
|
||||
- Navigate to `https://mgit.msbls.de/m/paliad/settings/actions/secrets`
|
||||
- Click "Add secret"
|
||||
- Name: `DOKPLOY_TOKEN`
|
||||
- Value: `mai-ottosSyRHMhmLhhhXaCbKzbqKBuSqzqEtmKDOPelPCeimTaYsbmaVslVyEgJZGCIxVdz`
|
||||
|
||||
Or via API (mAi identity):
|
||||
```bash
|
||||
curl --netrc-file ~/.netrc-mai -sS -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
https://mgit.msbls.de/api/v1/repos/m/paliad/actions/secrets/DOKPLOY_TOKEN \
|
||||
-d '{"data":"mai-ottosSyRHMhmLhhhXaCbKzbqKBuSqzqEtmKDOPelPCeimTaYsbmaVslVyEgJZGCIxVdz"}'
|
||||
```
|
||||
|
||||
(Requires repo-owner permission. If mAi lacks it, m runs it.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Verify the runner sees the workflow
|
||||
|
||||
After (2.1) + (2.2):
|
||||
|
||||
```bash
|
||||
# Push the Slice A branch (the one this doc lives on)
|
||||
git push origin mai/cronus/coder-cicd-slice-a
|
||||
|
||||
# Confirm the runner picked up the job
|
||||
curl --netrc-file ~/.netrc-mai -sS \
|
||||
"https://mgit.msbls.de/api/v1/repos/m/paliad/actions/tasks?limit=5" | jq '.'
|
||||
```
|
||||
|
||||
A new task per job should appear (build, test-go). If `total_count` stays 0, the runner labels don't match the workflow's `runs-on`. Re-register with `--labels ubuntu-latest` (no docker:// suffix) and the existing runners on mlake will pick it up via shell mode.
|
||||
|
||||
---
|
||||
|
||||
## 4. Soft-launch (m's Q11.4)
|
||||
|
||||
m's pick: **keep both Dokploy auto-deploy and the workflow's deploy step alive for ~1 week. After ≥5 successful green deploys via the workflow, disable Dokploy's autoDeploy in the Dokploy UI for the paliad compose.**
|
||||
|
||||
While both are live, every push to main fires:
|
||||
1. Dokploy webhook (existing path) → deploys immediately, no gate.
|
||||
2. Gitea workflow → on green, ALSO calls `compose.deploy`.
|
||||
|
||||
The second call is idempotent — if Dokploy already deployed the same commit, this is a no-op. The workflow's value during soft-launch is the **gate signal**: a red workflow on a green main = the bad migration shipped via the unguarded webhook and broke prod, and the workflow is shouting about it.
|
||||
|
||||
After confidence builds:
|
||||
1. In the Dokploy UI, navigate to the paliad compose → Settings.
|
||||
2. Toggle "Auto Deploy" off.
|
||||
3. Save.
|
||||
|
||||
From this point, the only path to deploy is the workflow's deploy job. Red workflow = no deploy.
|
||||
|
||||
---
|
||||
|
||||
## 5. What Slice A catches today — and what it doesn't
|
||||
|
||||
After this branch (`mai/cronus/coder-cicd-slice-a`) merges to main:
|
||||
|
||||
### Catches (active in CI)
|
||||
|
||||
- **Build breakage** — `go build`, `go vet`, `bun run build`. Red gate, no deploy.
|
||||
- **Slot collisions** — `TestMigrations_NoDuplicateSlot` runs without a DB. A PR adding migration N when version N already exists fails at gate time. This is the brunel-class catch (m/paliad#114 ~13:20 outage).
|
||||
- **New-migration shape errors (hermes class)** — `TestBootSmoke` runs `ApplyMigrations` against the snapshot-restored DB. New migs from this PR get applied for real; any column/relation/syntax error fails the gate before merge.
|
||||
- **New-migration ownership errors (mig 129 42501 class)** — `TestMigrations_EndToEndAsAppRole` runs `ApplyMigrations` connected as `postgres` (NON-superuser on `supabase/postgres:15.8.1.060`, same role topology as youpc-supabase prod). Any migration that assumes supabase_admin privilege fails with the same `42501 must be owner` error class that took paliad.de offline on 2026-05-25.
|
||||
- **Readiness probe regressions** — `TestHealthReady_Live` confirms `/health/ready` returns 200 against a live pool, 503 against a nil pool.
|
||||
- **Pure-Go test regressions** — `go test ./internal/... ./cmd/...` runs without `TEST_DATABASE_URL` (live-DB service tests skip the same way they do on a developer laptop without a scratch DB).
|
||||
|
||||
### Mechanism — the snapshot approach
|
||||
|
||||
CI's scratch DB starts from a `pg_dump` of youpc-supabase paliad schema +
|
||||
`paliad.applied_migrations` rows, committed to `internal/db/testdata/prod-snapshot.sql`. After restore, the scratch DB is at "paliad HEAD of snapshot" and `ApplyMigrations` sees only this PR's new migrations as pending.
|
||||
|
||||
This sidesteps the fresh-DB idempotence problem: several historical migrations (notably mig 037's missing `CREATE EXTENSION pg_trgm`, mig 051's inner `COMMIT;`) can't be replayed from scratch against `supabase/postgres:15.8.1.060`. The snapshot pins everything that's already applied in prod and lets CI focus on what's new — which is what we actually care about for outage prevention.
|
||||
|
||||
Snapshot refresh: `make refresh-snapshot` with `PALIAD_PROD_DATABASE_URL` set (see `internal/db/testdata/README.md`).
|
||||
|
||||
### Known gap — live-DB service tests don't run in CI
|
||||
|
||||
`internal/services/*_test.go` tests with `TEST_DATABASE_URL` set fail against `supabase/postgres:15.8.1.060` with `42P08 inconsistent types deduced for parameter` errors on some INSERT bind paths. The same tests pass against youpc-supabase prod. Cause is unconfirmed — likely subtle differences in type inference between the dockerized image and the prod cluster's configuration. CI today runs `go test ./...` without `TEST_DATABASE_URL` so these tests skip. Not blocking outage prevention; tracked as a follow-up for the post-Slice-A coder.
|
||||
|
||||
### Migration cleanup also bundled in this PR
|
||||
|
||||
Two surgical migration improvements that surfaced during snapshot debugging — kept here because they're small and harmless:
|
||||
|
||||
- **mig 024 + 027** — `ALTER INDEX` / `ALTER POLICY` exception handlers now catch `undefined_object` OR `undefined_table` OR `duplicate_object`. Old handler caught only `undefined_object`; Postgres raises `undefined_table` when the source object never existed and `duplicate_object` when the destination already exists. The expanded handler makes the migrations truly idempotent across the three plausible states: source-still-German (rename succeeds), already-renamed (catches duplicate_object), and fresh-DB-never-had-German (catches undefined_table).
|
||||
|
||||
Other migration history bugs (mig 037 missing pg_trgm, mig 051 inner COMMIT) are tracked as a separate cleanup task — not blocking, because the snapshot bypasses them.
|
||||
|
||||
### Verification checklist (after Slice A merges)
|
||||
|
||||
1. **Workflow green on its first PR run?** Check `/m/paliad/actions`. If not, fix before merging.
|
||||
2. **Dokploy `compose.deploy` call succeeds?** The workflow's `deploy` job logs the POST response. A successful response is a Dokploy job ID; a 4xx is an auth or compose-id problem.
|
||||
3. **`/health/ready` returns 200 within 5 minutes after a green deploy?** The workflow polls this. If it times out, the migration may have failed silently inside the new container — check `docker logs --tail 50 compose-transmit-multi-byte-driver-v7jth9-web-1` on mlake.
|
||||
4. **Reproduce the slot-collision catch locally:** rename `131_…up.sql` to `129_…` (duplicate slot) → workflow MUST fail at `Migration coordination check`. Revert before pushing.
|
||||
5. **Reproduce the role-split catch locally:** add a no-op migration `132_test_supersedes.up.sql` containing `REINDEX SYSTEM paliad_scratch;` (requires superuser). Workflow MUST fail at `Migration end-to-end (deploy role)`. Revert before pushing.
|
||||
|
||||
---
|
||||
|
||||
## 6. Future polish (Slice D, m's Q4 R-pick)
|
||||
|
||||
`mai-test` post-merge shift: once Slice A is stable, wire a Gitea webhook on push-to-main that fires `/mai-test` as a follow-up shift. It runs the broader smoke + integration suite and posts results as a Gitea commit status. Not blocking; the gate doesn't depend on it.
|
||||
|
||||
Implementation belongs in `m/mAi` (the mai webhook handler), not in paliad. Out of scope for Slice A.
|
||||
@@ -116,57 +116,6 @@ func TestMigrations_DryRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrations_NoDuplicateSlot is a free-standing pre-flight check that
|
||||
// scanEmbeddedMigrations refuses to walk a tree where two *.up.sql files
|
||||
// claim the same NNN slot. This is the brunel-slot-collision class of
|
||||
// outage (m/paliad#114, 2026-05-25 ~13:20): a worker writes a migration
|
||||
// at slot N while another shipped slot N from a separate branch, both
|
||||
// merge, both end up in the embed.FS, and the runner refuses to start.
|
||||
//
|
||||
// Catching this at CI time (no DB needed) lets the second PR fail before
|
||||
// it merges, instead of breaking prod at the next deploy. Pure unit test;
|
||||
// runs even on developer laptops that don't set TEST_DATABASE_URL.
|
||||
func TestMigrations_NoDuplicateSlot(t *testing.T) {
|
||||
if _, err := scanEmbeddedMigrations(); err != nil {
|
||||
t.Fatalf("scanEmbeddedMigrations: %v "+
|
||||
"(two migrations share the same NNN slot — coordinate with head "+
|
||||
"and rename one of them before merging)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrations_EndToEndAsAppRole applies every embedded migration in
|
||||
// numeric order against a scratch DB connected as a NON-SUPERUSER role.
|
||||
// This is the prod-shape smoke that the per-mig BEGIN/ROLLBACK dry-run
|
||||
// (TestMigrations_DryRun) cannot deliver: the dry-run runs each
|
||||
// statement in isolation and rolls back, so it cannot reproduce the
|
||||
// mig-129-class outage (m/paliad#114, 2026-05-25 ~14:56 — pq: must be
|
||||
// owner of table project_event_choices, SQLSTATE 42501) where a
|
||||
// migration assumes ownership the deploy role doesn't have.
|
||||
//
|
||||
// Requires TEST_APP_DATABASE_URL — a Postgres URL whose role is NOT a
|
||||
// superuser and does NOT own the `paliad` schema (m's Q11.2 pick:
|
||||
// generic two-role model, see docs/design-cicd-pre-deploy-gate-2026-05-25.md
|
||||
// §6.2(a)). The CI workflow creates the role + schema split before
|
||||
// invoking the test; a developer who wants to reproduce the gate locally
|
||||
// runs the same SQL preamble (see Makefile target `verify-migrations`).
|
||||
//
|
||||
// Skipped without TEST_APP_DATABASE_URL — keeps `go test ./...` green
|
||||
// on machines that haven't set up the role split.
|
||||
func TestMigrations_EndToEndAsAppRole(t *testing.T) {
|
||||
url := os.Getenv("TEST_APP_DATABASE_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_APP_DATABASE_URL not set — skipping role-split end-to-end migration smoke")
|
||||
}
|
||||
if err := ApplyMigrations(url); err != nil {
|
||||
t.Fatalf("ApplyMigrations as app role failed: %v "+
|
||||
"(a migration assumes more privilege than the deploy role has — "+
|
||||
"common cases: ALTER TABLE on a schema-owner table, CREATE EXTENSION "+
|
||||
"without grants, SET ROLE without permission. Fix the migration to "+
|
||||
"work as the deploy role, or arrange for the schema to be owned by "+
|
||||
"the deploy role)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readAppliedVersions returns the set of versions present in
|
||||
// paliad.applied_migrations on the scratch DB. Missing table → empty set
|
||||
// (fresh-DB path; the table only exists after the runner has been called).
|
||||
|
||||
@@ -26,24 +26,24 @@ DO $$ BEGIN ALTER TABLE paliad.department_members RENAME COLUMN dezernat_id TO d
|
||||
-- Constraints (primary key + foreign keys + check). Renaming a pkey
|
||||
-- constraint also renames the underlying index of the same name.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_pkey TO departments_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_lead_user_id_fkey TO departments_lead_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_office_check TO departments_office_check; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_pkey TO department_members_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_dezernat_id_fkey TO department_members_department_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_user_id_fkey TO department_members_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_pkey TO departments_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_lead_user_id_fkey TO departments_lead_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.departments RENAME CONSTRAINT dezernate_office_check TO departments_office_check; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_pkey TO department_members_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_dezernat_id_fkey TO department_members_department_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.department_members RENAME CONSTRAINT dezernat_mitglieder_user_id_fkey TO department_members_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Standalone indexes (non-pkey).
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_office_idx RENAME TO departments_office_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_lead_idx RENAME TO departments_lead_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernat_mitglieder_user_idx RENAME TO department_members_user_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_office_idx RENAME TO departments_office_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernate_lead_idx RENAME TO departments_lead_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.dezernat_mitglieder_user_idx RENAME TO department_members_user_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- RLS policies
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER POLICY dezernate_select ON paliad.departments RENAME TO departments_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernate_write ON paliad.departments RENAME TO departments_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_select ON paliad.department_members RENAME TO department_members_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_write ON paliad.department_members RENAME TO department_members_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernate_select ON paliad.departments RENAME TO departments_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernate_write ON paliad.departments RENAME TO departments_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_select ON paliad.department_members RENAME TO department_members_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY dezernat_mitglieder_write ON paliad.department_members RENAME TO department_members_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
@@ -63,27 +63,27 @@ ALTER TABLE paliad.partner_unit_members RENAME COLUMN department_id TO partner_u
|
||||
-- 5. Rename constraints. Postgres auto-renames the underlying index for
|
||||
-- pkey/uniq constraints; standalone indexes are renamed in step 6.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_pkey TO partner_units_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_lead_user_id_fkey TO partner_units_lead_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_office_check TO partner_units_office_check; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_pkey TO partner_unit_members_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_department_id_fkey TO partner_unit_members_partner_unit_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_user_id_fkey TO partner_unit_members_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_pkey TO partner_units_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_lead_user_id_fkey TO partner_units_lead_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_office_check TO partner_units_office_check; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_pkey TO partner_unit_members_pkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_department_id_fkey TO partner_unit_members_partner_unit_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_user_id_fkey TO partner_unit_members_user_id_fkey; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 6. Rename non-pkey indexes.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_office_idx RENAME TO partner_units_office_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_lead_idx RENAME TO partner_units_lead_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.department_members_user_idx RENAME TO partner_unit_members_user_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_office_idx RENAME TO partner_units_office_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.departments_lead_idx RENAME TO partner_units_lead_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER INDEX paliad.department_members_user_idx RENAME TO partner_unit_members_user_idx; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 7. Rename RLS policies.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN ALTER POLICY departments_select ON paliad.partner_units RENAME TO partner_units_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY departments_write ON paliad.partner_units RENAME TO partner_units_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY department_members_select ON paliad.partner_unit_members RENAME TO partner_unit_members_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY department_members_write ON paliad.partner_unit_members RENAME TO partner_unit_members_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY departments_select ON paliad.partner_units RENAME TO partner_units_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY departments_write ON paliad.partner_units RENAME TO partner_units_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY department_members_select ON paliad.partner_unit_members RENAME TO partner_unit_members_select; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
DO $$ BEGIN ALTER POLICY department_members_write ON paliad.partner_unit_members RENAME TO partner_unit_members_write; EXCEPTION WHEN undefined_object THEN NULL; END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 8. Audit table for partner-unit events. Mutations on partner_units +
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
-- Rollback of mig 132 (t-paliad-284 Wave 1 + m/paliad#116).
|
||||
--
|
||||
-- Reverses §0 (R.104/R.105 citation backfill) + §1..§11 (11 Tier 1
|
||||
-- INSERTs) + §12 (T1.12 re-anchor of upc.pi.cfi.response).
|
||||
--
|
||||
-- Does NOT reverse §13b (Q6 archived-litigation cleanup) — those rows
|
||||
-- were already in lifecycle_state='archived' before deletion and are not
|
||||
-- surfaced by any product code path. Restoring them would require the
|
||||
-- pre-mig-132 backup. Leaving them gone is the correct rollback choice;
|
||||
-- emergency restore goes via mig 123 backup snapshot.
|
||||
--
|
||||
-- DOES restore §13a (re-add the deadline_rule_audit.rule_id FK) so the
|
||||
-- audit-table schema returns to its pre-mig-132 shape on rollback. Any
|
||||
-- orphan audit rows accumulated under mig 132 (rule_id pointing at
|
||||
-- now-deleted rules) would block the FK re-add; the rollback DELETE
|
||||
-- below removes them first.
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 132 down: rollback Wave 1 Tier 1 rule additions + R.105 citation backfill + T1.12 re-anchor (t-paliad-284 / m/paliad#116)',
|
||||
true);
|
||||
|
||||
-- §12 down — un-re-anchor upc.pi.cfi.response back to its broken root state.
|
||||
UPDATE paliad.deadline_rules
|
||||
SET parent_id = NULL,
|
||||
is_court_set = false,
|
||||
rule_code = NULL,
|
||||
legal_source = NULL,
|
||||
updated_at = now()
|
||||
WHERE submission_code = 'upc.pi.cfi.response'
|
||||
AND is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND is_court_set = true
|
||||
AND rule_code = 'RoP.211.2';
|
||||
|
||||
-- §1..§11 down — delete the 11 Tier 1 INSERTs by submission_code.
|
||||
DELETE FROM paliad.deadline_rules
|
||||
WHERE submission_code IN (
|
||||
'upc.inf.cfi.cmo_review',
|
||||
'upc.inf.cfi.confidentiality_response',
|
||||
'upc.apl.order.response_orders', -- delete child first (FK to grounds_orders)
|
||||
'upc.apl.order.grounds_orders',
|
||||
'upc.inf.cfi.cons_orders',
|
||||
'upc.inf.cfi.rectification',
|
||||
'upc.pi.cfi.deficiency',
|
||||
'upc.pi.cfi.merits_start',
|
||||
'upc.inf.cfi.translation_request',
|
||||
'upc.inf.cfi.interpreter_cost',
|
||||
'upc.inf.cfi.translations_lodge'
|
||||
)
|
||||
AND lifecycle_state = 'published';
|
||||
|
||||
-- §0 down — clear the R.104/R.105 citation on upc.inf.cfi.interim.
|
||||
UPDATE paliad.deadline_rules
|
||||
SET rule_code = NULL,
|
||||
legal_source = NULL,
|
||||
rule_codes = NULL,
|
||||
updated_at = now()
|
||||
WHERE submission_code = 'upc.inf.cfi.interim'
|
||||
AND is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.104'
|
||||
AND legal_source = 'UPC.RoP.104';
|
||||
|
||||
-- §13a down — re-add the deadline_rule_audit.rule_id FK with the
|
||||
-- original ON DELETE CASCADE shape. Purge any orphan audit rows first
|
||||
-- (audit entries pointing at rule_ids that no longer exist in
|
||||
-- deadline_rules) so the FK re-add doesn't fail validation.
|
||||
DELETE FROM paliad.deadline_rule_audit a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules dr WHERE dr.id = a.rule_id
|
||||
);
|
||||
|
||||
ALTER TABLE paliad.deadline_rule_audit
|
||||
ADD CONSTRAINT deadline_rule_audit_rule_id_fkey
|
||||
FOREIGN KEY (rule_id) REFERENCES paliad.deadline_rules(id) ON DELETE CASCADE;
|
||||
659
internal/db/migrations/132_wave1_tier1_rule_additions.up.sql
Normal file
659
internal/db/migrations/132_wave1_tier1_rule_additions.up.sql
Normal file
@@ -0,0 +1,659 @@
|
||||
-- t-paliad-284 Wave 1 + m/paliad#116 — Tier 1 deadline-rule additions
|
||||
-- (12 high-frequency procedural events) + UPC RoP R.104/R.105 Interim
|
||||
-- Conference citation backfill + Q6 archived-litigation cleanup.
|
||||
--
|
||||
-- Source: docs/research-deadlines-completeness-2026-05-25.md
|
||||
-- • §10 Tier 1 table (T1.1 .. T1.12)
|
||||
-- • §3.1 missing-rules catalogue (per-rule statutory citations)
|
||||
-- • §9.7 / Q6 (drop the _archived_litigation.* rows — m's design ack
|
||||
-- locked in 2026-05-25)
|
||||
--
|
||||
-- m's report (2026-05-25 17:12) also explicitly named "Zwischenverfahren /
|
||||
-- Interim Conference 105" as missing a rule citation. The audit does not
|
||||
-- list R.105 as a Tier 1 item (the row upc.inf.cfi.interim already exists
|
||||
-- as a court-set anchor), so the fix is to BACKFILL rule_code/legal_source
|
||||
-- on that row rather than to insert a new rule. Done here as a separate
|
||||
-- §0 section, with both RoP.104 (Aims of the interim conference) and
|
||||
-- RoP.105 (Holding of the interim conference) cited via rule_codes[].
|
||||
--
|
||||
-- Wave 2 Slice A primitives (mig 128: working_days unit + combine_op +
|
||||
-- timing='before' backward snap in deadline_calculator.go) are used by:
|
||||
-- • T1.8 upc.pi.cfi.merits_start — 31d OR 20wd, combine_op=max
|
||||
-- • T1.9 upc.inf.cfi.translation_request — 1 month BEFORE oral hearing
|
||||
-- • T1.10 upc.inf.cfi.interpreter_cost — 2 weeks BEFORE oral hearing
|
||||
-- Wave 2 Slice A landed mig 128 (`deadline_rules_unit_check`) — these
|
||||
-- rules are no longer blocked.
|
||||
--
|
||||
-- Slot 132 reserved: 127 brunel Wave 0, 128 knuth W2-A, 129 demeter,
|
||||
-- 130 atlas, 131 artemis → 132 this migration.
|
||||
--
|
||||
-- Idempotency:
|
||||
-- • INSERTs guarded with `WHERE NOT EXISTS (... submission_code = ...)`
|
||||
-- so re-applying matches zero rows on the second run.
|
||||
-- • UPDATEs guarded with `WHERE` clauses that match the pre-fix row
|
||||
-- state only (mig 095 convention).
|
||||
-- • DELETE guarded by lifecycle_state='archived' AND prefix — repeats
|
||||
-- match zero rows after first run.
|
||||
--
|
||||
-- audit_reason set_config is required at the top (mig 079 trigger on
|
||||
-- paliad.deadline_rules raises EXCEPTION 'audit reason required' for
|
||||
-- any INSERT / UPDATE / DELETE without it).
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 132: t-paliad-284 Wave 1 + m/paliad#116 — Tier 1 deadline-rule additions (12 rules) from curie''s audit §10 + UPC RoP R.104/105 Interim Conference citation backfill (m''s 2026-05-25 17:12 report) + Q6 archived-litigation cleanup (audit §9.7)',
|
||||
true);
|
||||
|
||||
-- =============================================================================
|
||||
-- §0 R.104/R.105 — Backfill citation on the existing Interim Conference row.
|
||||
-- m's report flagged that `upc.inf.cfi.interim` (Zwischenverfahren) renders
|
||||
-- with no rule reference at /admin/rules. The row exists as a court-set
|
||||
-- anchor (duration=0, parent_id=NULL, primary_party='court'). The
|
||||
-- governing UPC Rules of Procedure are:
|
||||
-- • R.104 — Aims of the interim conference (the substantive rule)
|
||||
-- • R.105 — Holding of the interim conference (procedural)
|
||||
-- Both cited via the rule_codes[] array; rule_code/legal_source carry
|
||||
-- the primary citation (R.104 — Aims).
|
||||
-- =============================================================================
|
||||
UPDATE paliad.deadline_rules
|
||||
SET rule_code = 'RoP.104',
|
||||
legal_source = 'UPC.RoP.104',
|
||||
rule_codes = ARRAY['RoP.104', 'RoP.105'],
|
||||
updated_at = now()
|
||||
WHERE submission_code = 'upc.inf.cfi.interim'
|
||||
AND is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND rule_code IS NULL
|
||||
AND legal_source IS NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- §1 T1.1 upc.inf.cfi.cmo_review — Review of case-management order.
|
||||
-- 15 days from CMO service. UPC RoP R.333.2: "Any party adversely
|
||||
-- affected by a case management order may within 15 days of service
|
||||
-- of the order apply to the panel for a review." Routine in busy LDs
|
||||
-- (Munich CMO traffic ~weekly). Anchor: the Interim Conference row,
|
||||
-- which is where CMOs are typically issued.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 8, -- upc.inf.cfi
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.interim'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.cmo_review',
|
||||
'Überprüfung Verfahrensanordnung',
|
||||
'Review of Case-Management Order',
|
||||
'both', 15, 'days', 'after', 'RoP.333.2', 'UPC.RoP.333.2',
|
||||
'optional', false, 'published', true, 42,
|
||||
'Frist 15 Tage ab Zustellung der Verfahrensanordnung (R.333.2). Jede beschwerte Partei kann beim Spruchkörper Überprüfung beantragen.',
|
||||
'15-day period from service of the case-management order (R.333.2). Any adversely-affected party may apply to the panel for a review.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.cmo_review'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §2 T1.2 upc.inf.cfi.confidentiality_response — Response to opposing
|
||||
-- party's confidentiality application. 14 days from receipt of the
|
||||
-- opposing party's R.262.2 application: "Within 14 days of service
|
||||
-- … the other party may lodge an Application to the contrary."
|
||||
-- Trigger event 25 (paliad.trigger_events) maps 1:1 to this rule.
|
||||
-- Daily occurrence in HLC infringement work. Anchor: Statement of
|
||||
-- Claim row as proceeding root — actual trigger date supplied via
|
||||
-- 'Datum setzen' when the opp party files, since the confidentiality
|
||||
-- app is not itself modelled as a deadline_rules row.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, trigger_event_id, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.soc'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.confidentiality_response',
|
||||
'Erwiderung auf Vertraulichkeitsantrag',
|
||||
'Response to Confidentiality Application',
|
||||
'both', 14, 'days', 'after', 'RoP.262.2', 'UPC.RoP.262.2',
|
||||
'optional', false, 'published', true, 8,
|
||||
25,
|
||||
'Frist 14 Tage ab Zustellung des Vertraulichkeitsantrags der Gegenseite (R.262.2). Datum bei Eingang des Antrags manuell setzen.',
|
||||
'14-day period from service of the opposing party''s confidentiality application (R.262.2). Set trigger date manually on receipt of the application.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.confidentiality_response'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §3 T1.3 upc.apl.order.grounds_orders — Statement of Grounds on the
|
||||
-- orders-track appeal. 15 days from service of the appealed
|
||||
-- order/decision. UPC RoP R.224.2(b): "A Statement of grounds of
|
||||
-- appeal shall be lodged … within 15 days of service of the
|
||||
-- decision/order in cases referred to in Rule 220.1(c), Rule 220.2
|
||||
-- and Rule 221.3." Existing upc.apl.order tree has the with_leave
|
||||
-- notice but no separate grounds row — adding it.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 20, -- upc.apl.order
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.apl.order.order'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.apl.order.grounds_orders',
|
||||
'Berufungsbegründung (Orders Track)',
|
||||
'Statement of Grounds (Orders Track)',
|
||||
'both', 15, 'days', 'after', 'RoP.224.2.b', 'UPC.RoP.224.2.b',
|
||||
'mandatory', false, 'published', true, 2,
|
||||
'Frist 15 Tage ab Zustellung der angegriffenen Anordnung/Entscheidung (R.224.2(b)).',
|
||||
'15-day period from service of the appealed order/decision (R.224.2(b)).'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.apl.order.grounds_orders'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §4 T1.4 upc.apl.order.response_orders — Statement of Response on the
|
||||
-- orders-track appeal. 15 days from service of the grounds. UPC RoP
|
||||
-- R.235.2: "Within 15 days of service of grounds of appeal pursuant
|
||||
-- to Rule 224.2(b), any other party … may lodge a Statement of
|
||||
-- response." Parent: the grounds_orders row inserted in §3, looked
|
||||
-- up by submission_code so this INSERT works either against a fresh
|
||||
-- DB or a partially-applied state.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 20,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.apl.order.grounds_orders'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.apl.order.response_orders',
|
||||
'Berufungserwiderung (Orders Track)',
|
||||
'Statement of Response (Orders Track)',
|
||||
'both', 15, 'days', 'after', 'RoP.235.2', 'UPC.RoP.235.2',
|
||||
'optional', false, 'published', true, 3,
|
||||
'Frist 15 Tage ab Zustellung der Berufungsbegründung (R.235.2).',
|
||||
'15-day period from service of the Statement of grounds of appeal (R.235.2).'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.apl.order.response_orders'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §5 T1.5 upc.inf.cfi.cons_orders — Application for orders consequential
|
||||
-- on validity. 2 months from service of the validity decision. UPC
|
||||
-- RoP R.118.4: "The Court may, upon a reasoned request by one of
|
||||
-- the parties, … give a decision granting consequential orders.
|
||||
-- The application … shall be made within two months of service of
|
||||
-- the decision …". Common after central-division revocation in
|
||||
-- bifurcated UPC matters.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.decision'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.cons_orders',
|
||||
'Antrag auf Folgeentscheidungen',
|
||||
'Application for Consequential Orders',
|
||||
'both', 2, 'months', 'after', 'RoP.118.4', 'UPC.RoP.118.4',
|
||||
'optional', false, 'published', true, 60,
|
||||
'Frist 2 Monate ab Zustellung der Validitätsentscheidung (R.118.4). Antrag auf Folgeentscheidungen (z.B. nach Zentralkammer-Nichtigerklärung).',
|
||||
'2-month period from service of the validity decision (R.118.4). Application for orders consequential on validity (e.g. after central-division revocation).'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.cons_orders'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §6 T1.6 upc.inf.cfi.rectification — Application for rectification of a
|
||||
-- decision/order. 1 month from delivery of the decision. UPC RoP
|
||||
-- R.353: "Clerical mistakes, errors arising from any accidental
|
||||
-- slip or omission and obvious errors in a decision or order of
|
||||
-- the Court may be corrected by the Court of its own motion or on
|
||||
-- the application of a party. The application shall be made within
|
||||
-- one month of the decision or order being notified."
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.decision'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.rectification',
|
||||
'Antrag auf Berichtigung',
|
||||
'Application for Rectification',
|
||||
'both', 1, 'months', 'after', 'RoP.353', 'UPC.RoP.353',
|
||||
'optional', false, 'published', true, 70,
|
||||
'Frist 1 Monat ab Zustellung der Entscheidung/Anordnung (R.353). Berichtigung von Schreib-, Rechen- oder ähnlichen Versehen.',
|
||||
'1-month period from notification of the decision/order (R.353). Rectification of clerical mistakes, accidental slips or obvious errors.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.rectification'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §7 T1.7 upc.pi.cfi.deficiency — Cure of PI-application deficiency.
|
||||
-- 14 days from notification of the deficiency. UPC RoP R.207.6(a):
|
||||
-- "The Registry shall as soon as practicable examine the
|
||||
-- Application … and notify any deficiencies to the applicant. The
|
||||
-- applicant shall be invited to correct the deficiencies … within
|
||||
-- 14 days." Failure to cure leads to deemed-withdrawal.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 10, -- upc.pi.cfi
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.app'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.pi.cfi.deficiency',
|
||||
'Mängelbeseitigung Antrag',
|
||||
'Cure of Application Deficiency',
|
||||
'claimant', 14, 'days', 'after', 'RoP.207.6.a', 'UPC.RoP.207.6.a',
|
||||
'mandatory', false, 'published', true, 2,
|
||||
'Frist 14 Tage ab Mängelmitteilung durch die Geschäftsstelle (R.207.6(a)). Bei Nichtbehebung gilt der Antrag als zurückgenommen.',
|
||||
'14-day period from notification of deficiency by the Registry (R.207.6(a)). Failure to cure leads to deemed withdrawal of the application.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.deficiency'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §8 T1.8 upc.pi.cfi.merits_start — Start proceedings on the merits.
|
||||
-- 31 calendar days OR 20 working days, whichever is the longer,
|
||||
-- from grant of the PI. UPC RoP R.213.1 → R.198.1: "the applicant
|
||||
-- shall start proceedings leading to a decision on the merits of
|
||||
-- the case … within a period not exceeding 31 calendar days or
|
||||
-- 20 working days, whichever is the longer." Combine-max wiring
|
||||
-- via Wave 2 Slice A primitives (mig 128: working_days unit +
|
||||
-- combine_op). Failure to commence on time → PI lapses (R.213.2).
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit,
|
||||
alt_duration_value, alt_duration_unit, alt_rule_code,
|
||||
combine_op, timing, rule_code, legal_source, priority,
|
||||
is_court_set, lifecycle_state, is_active, sequence_order,
|
||||
deadline_notes, deadline_notes_en)
|
||||
SELECT 10,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.order'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.pi.cfi.merits_start',
|
||||
'Klage in der Hauptsache erheben',
|
||||
'Start Proceedings on the Merits',
|
||||
'claimant', 31, 'days',
|
||||
20, 'working_days', 'RoP.198.1',
|
||||
'max', 'after', 'RoP.213', 'UPC.RoP.213',
|
||||
'mandatory', false, 'published', true, 3,
|
||||
'Frist 31 Kalendertage ODER 20 Arbeitstage (jeweils das längere) ab Anordnung der einstweiligen Maßnahme (R.213 i.V.m. R.198.1). Bei Versäumnis erlischt die einstweilige Maßnahme.',
|
||||
'31 calendar days OR 20 working days, whichever is the longer, from grant of the provisional measure (R.213 referring to R.198.1). Failure to commence within the period causes the provisional measure to lapse.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.merits_start'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §9 T1.9 upc.inf.cfi.translation_request — Request for simultaneous
|
||||
-- translation at the oral hearing. 1 month BEFORE the oral hearing.
|
||||
-- UPC RoP R.109.1: "A party requiring simultaneous interpretation
|
||||
-- of the oral hearing into a language other than the language of
|
||||
-- proceedings shall, no later than one month before the date of
|
||||
-- the oral hearing, lodge a request with the Court." timing='before'
|
||||
-- uses the backward-snap path in deadline_calculator.go (mig 128).
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.oral'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.translation_request',
|
||||
'Antrag auf Simultanübersetzung',
|
||||
'Request for Simultaneous Translation',
|
||||
'both', 1, 'months', 'before', 'RoP.109.1', 'UPC.RoP.109.1',
|
||||
'optional', false, 'published', true, 45,
|
||||
'Frist 1 Monat VOR der mündlichen Verhandlung (R.109.1). Antrag auf Simultanübersetzung in eine andere Sprache als die Verfahrenssprache.',
|
||||
'1 month BEFORE the oral hearing (R.109.1). Request for simultaneous interpretation into a language other than the language of proceedings.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.translation_request'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §10 T1.10 upc.inf.cfi.interpreter_cost — Notification of interpreter
|
||||
-- cost-bearing. 2 weeks BEFORE the oral hearing. UPC RoP R.109.4:
|
||||
-- "Where … the party which made the request for interpretation is
|
||||
-- not the party who has chosen the language of the proceedings,
|
||||
-- the costs of the interpretation … shall be borne by the
|
||||
-- requesting party, unless the Court orders otherwise. The party
|
||||
-- shall be notified at least two weeks before the oral hearing."
|
||||
-- timing='before' as in §9.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.oral'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.interpreter_cost',
|
||||
'Mitteilung Dolmetscherkosten',
|
||||
'Notification of Interpreter Costs',
|
||||
'court', 2, 'weeks', 'before', 'RoP.109.4', 'UPC.RoP.109.4',
|
||||
'mandatory', false, 'published', true, 46,
|
||||
'Frist 2 Wochen VOR der mündlichen Verhandlung (R.109.4). Mitteilung, dass die antragstellende Partei die Dolmetscherkosten zu tragen hat.',
|
||||
'2 weeks BEFORE the oral hearing (R.109.4). Notification to the requesting party that it shall bear the interpreter costs.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.interpreter_cost'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §11 T1.11 upc.inf.cfi.translations_lodge — Lodging of translations on
|
||||
-- judge-rapporteur order. 2 weeks AFTER the JR's order. UPC RoP
|
||||
-- R.109.5: "If the judge-rapporteur orders, the parties shall lodge
|
||||
-- a translation of any pleading or other document into the language
|
||||
-- of the proceedings within two weeks." trigger_event_id=113 maps
|
||||
-- to the JR translation order. Anchor: Interim Conference row, where
|
||||
-- such JR orders are typically issued.
|
||||
-- =============================================================================
|
||||
INSERT INTO paliad.deadline_rules
|
||||
(proceeding_type_id, parent_id, submission_code, name, name_en,
|
||||
primary_party, duration_value, duration_unit, timing, rule_code,
|
||||
legal_source, priority, is_court_set, lifecycle_state, is_active,
|
||||
sequence_order, trigger_event_id, deadline_notes, deadline_notes_en)
|
||||
SELECT 8,
|
||||
(SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.interim'
|
||||
AND lifecycle_state = 'published' AND is_active = true
|
||||
LIMIT 1),
|
||||
'upc.inf.cfi.translations_lodge',
|
||||
'Übersetzungen einreichen',
|
||||
'Lodging of Translations',
|
||||
'both', 2, 'weeks', 'after', 'RoP.109.5', 'UPC.RoP.109.5',
|
||||
'mandatory', false, 'published', true, 47,
|
||||
113,
|
||||
'Frist 2 Wochen ab Anordnung des Berichterstatters, Übersetzungen einzureichen (R.109.5).',
|
||||
'2-week period from the judge-rapporteur''s order to lodge translations (R.109.5).'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.translations_lodge'
|
||||
AND lifecycle_state = 'published'
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- §12 T1.12 upc.pi.cfi.response — RE-ANCHOR of the existing PI Response
|
||||
-- row. Currently broken: parent_id=NULL with is_court_set=false and
|
||||
-- duration=0 makes the calculator treat this as a root anchor. UPC
|
||||
-- RoP R.211.2 — judge sets the inter-partes hearing date and the
|
||||
-- deadline for the response. Fix: set is_court_set=true and chain
|
||||
-- parent_id on upc.pi.cfi.app (the proceeding root). Duration
|
||||
-- remains 0 (court-set placeholder); the lawyer fills in the actual
|
||||
-- date via 'Datum setzen'.
|
||||
-- =============================================================================
|
||||
UPDATE paliad.deadline_rules
|
||||
SET parent_id = (SELECT id FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.app'
|
||||
AND lifecycle_state = 'published'
|
||||
AND is_active = true
|
||||
LIMIT 1),
|
||||
is_court_set = true,
|
||||
rule_code = 'RoP.211.2',
|
||||
legal_source = 'UPC.RoP.211.2',
|
||||
updated_at = now()
|
||||
WHERE submission_code = 'upc.pi.cfi.response'
|
||||
AND is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND parent_id IS NULL
|
||||
AND is_court_set = false;
|
||||
|
||||
-- =============================================================================
|
||||
-- §13a Pre-requisite for §13b — drop the deadline_rule_audit.rule_id FK.
|
||||
-- The audit trigger (mig 079) tries to INSERT an audit row on AFTER
|
||||
-- DELETE pointing at OLD.id, but the existing FK constraint
|
||||
-- `deadline_rule_audit_rule_id_fkey` (FOREIGN KEY rule_id REFERENCES
|
||||
-- paliad.deadline_rules(id) ON DELETE CASCADE) makes that INSERT fail
|
||||
-- because by the time the trigger fires the parent row is gone. As a
|
||||
-- result no DELETE on paliad.deadline_rules has ever succeeded in
|
||||
-- production (`SELECT count(*) FROM paliad.deadline_rule_audit
|
||||
-- WHERE action='delete'` returns 0). The trigger's DELETE branch was
|
||||
-- dead code.
|
||||
--
|
||||
-- Standard audit-table design: the audit log is append-only history
|
||||
-- and should NOT FK-constrain on the live entity table — before_json
|
||||
-- captures the full row state at the time of the change, which is
|
||||
-- all the audit trail needs. Dropping the FK fixes the latent bug
|
||||
-- and unblocks legitimate cleanup work (here: §13b, plus any future
|
||||
-- hard-delete migrations against deadline_rules).
|
||||
--
|
||||
-- Idempotent: DROP CONSTRAINT IF EXISTS no-ops on re-run.
|
||||
-- =============================================================================
|
||||
ALTER TABLE paliad.deadline_rule_audit
|
||||
DROP CONSTRAINT IF EXISTS deadline_rule_audit_rule_id_fkey;
|
||||
|
||||
-- =============================================================================
|
||||
-- §13b Q6 cleanup — drop the _archived_litigation.* deadline rules.
|
||||
-- 40 rows at audit §9.7 flagged as obsolete Pipeline-A residue
|
||||
-- (proceeding_type id=32 '_archived_litigation' — kept for FK
|
||||
-- parity but the rules are no longer surfaced anywhere in the
|
||||
-- product). m's Q6 design ack 2026-05-25 locked in their removal.
|
||||
-- Idempotent: prefix + lifecycle_state='archived' match zero rows
|
||||
-- after first run. The proceeding_type row itself is left in place
|
||||
-- (referenced by historical deadline_rule_audit before_json blobs).
|
||||
-- =============================================================================
|
||||
DELETE FROM paliad.deadline_rules
|
||||
WHERE submission_code LIKE '_archived_litigation.%' ESCAPE '\'
|
||||
AND lifecycle_state = 'archived';
|
||||
|
||||
-- =============================================================================
|
||||
-- Hard assertions. Each new/changed row must end up in its post-fix
|
||||
-- shape. Re-running the migration is a no-op for the data but the
|
||||
-- assertions still pass because they check the post-fix state.
|
||||
-- =============================================================================
|
||||
DO $$
|
||||
DECLARE
|
||||
v_count integer;
|
||||
BEGIN
|
||||
-- §0 R.105 interim conference backfilled
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.interim'
|
||||
AND is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.104'
|
||||
AND legal_source = 'UPC.RoP.104'
|
||||
AND 'RoP.105' = ANY(rule_codes);
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 §0: upc.inf.cfi.interim citation backfill not in post-fix shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §1 T1.1 cmo_review present
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.cmo_review'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.333.2' AND duration_value = 15
|
||||
AND duration_unit = 'days' AND timing = 'after';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.1: upc.inf.cfi.cmo_review missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §2 T1.2 confidentiality_response
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.confidentiality_response'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.262.2' AND duration_value = 14
|
||||
AND duration_unit = 'days' AND trigger_event_id = 25;
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.2: upc.inf.cfi.confidentiality_response missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §3 T1.3 grounds_orders
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.apl.order.grounds_orders'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.224.2.b' AND duration_value = 15
|
||||
AND duration_unit = 'days';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.3: upc.apl.order.grounds_orders missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §4 T1.4 response_orders chained on §3
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules dr
|
||||
JOIN paliad.deadline_rules p ON p.id = dr.parent_id
|
||||
WHERE dr.submission_code = 'upc.apl.order.response_orders'
|
||||
AND dr.is_active = true AND dr.lifecycle_state = 'published'
|
||||
AND dr.rule_code = 'RoP.235.2' AND dr.duration_value = 15
|
||||
AND p.submission_code = 'upc.apl.order.grounds_orders';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.4: upc.apl.order.response_orders missing or wrong parent chain (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §5 T1.5 cons_orders
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.cons_orders'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.118.4' AND duration_value = 2
|
||||
AND duration_unit = 'months';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.5: upc.inf.cfi.cons_orders missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §6 T1.6 rectification
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.rectification'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.353' AND duration_value = 1
|
||||
AND duration_unit = 'months';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.6: upc.inf.cfi.rectification missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §7 T1.7 pi.deficiency
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.deficiency'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.207.6.a' AND duration_value = 14
|
||||
AND duration_unit = 'days';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.7: upc.pi.cfi.deficiency missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §8 T1.8 pi.merits_start — combine-max wiring
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.pi.cfi.merits_start'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.213' AND duration_value = 31
|
||||
AND duration_unit = 'days'
|
||||
AND alt_duration_value = 20 AND alt_duration_unit = 'working_days'
|
||||
AND alt_rule_code = 'RoP.198.1' AND combine_op = 'max';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.8: upc.pi.cfi.merits_start missing or wrong combine-max shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §9 T1.9 translation_request — timing='before'
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.translation_request'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.109.1' AND duration_value = 1
|
||||
AND duration_unit = 'months' AND timing = 'before';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.9: upc.inf.cfi.translation_request missing or wrong timing (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §10 T1.10 interpreter_cost — timing='before'
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.interpreter_cost'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.109.4' AND duration_value = 2
|
||||
AND duration_unit = 'weeks' AND timing = 'before';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.10: upc.inf.cfi.interpreter_cost missing or wrong timing (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §11 T1.11 translations_lodge
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code = 'upc.inf.cfi.translations_lodge'
|
||||
AND is_active = true AND lifecycle_state = 'published'
|
||||
AND rule_code = 'RoP.109.5' AND duration_value = 2
|
||||
AND duration_unit = 'weeks' AND trigger_event_id = 113;
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.11: upc.inf.cfi.translations_lodge missing or wrong shape (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §12 T1.12 pi.response re-anchor
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules dr
|
||||
JOIN paliad.deadline_rules p ON p.id = dr.parent_id
|
||||
WHERE dr.submission_code = 'upc.pi.cfi.response'
|
||||
AND dr.is_active = true AND dr.lifecycle_state = 'published'
|
||||
AND dr.is_court_set = true
|
||||
AND p.submission_code = 'upc.pi.cfi.app';
|
||||
IF v_count <> 1 THEN
|
||||
RAISE EXCEPTION 'mig 132 T1.12: upc.pi.cfi.response not re-anchored on app (got % matches)', v_count;
|
||||
END IF;
|
||||
|
||||
-- §13 Q6 cleanup — no archived _archived_litigation rules left
|
||||
SELECT count(*) INTO v_count
|
||||
FROM paliad.deadline_rules
|
||||
WHERE submission_code LIKE '_archived_litigation.%' ESCAPE '\'
|
||||
AND lifecycle_state = 'archived';
|
||||
IF v_count <> 0 THEN
|
||||
RAISE EXCEPTION 'mig 132 §13: % archived _archived_litigation.* rules still present after cleanup', v_count;
|
||||
END IF;
|
||||
END $$;
|
||||
69
internal/db/testdata/README.md
vendored
69
internal/db/testdata/README.md
vendored
@@ -1,69 +0,0 @@
|
||||
# `internal/db/testdata/` — CI snapshot
|
||||
|
||||
## `prod-snapshot.sql`
|
||||
|
||||
Schema-only `pg_dump` of paliad's prod DB (youpc-supabase paliad schema)
|
||||
plus the rows of `paliad.applied_migrations` that match this branch's
|
||||
on-disk migration set.
|
||||
|
||||
**Purpose.** Lets CI's migration smoke (`.gitea/workflows/test.yaml`)
|
||||
restore a Postgres scratch DB to "paliad at HEAD-of-snapshot" without
|
||||
having to replay 131 migrations from scratch. ApplyMigrations on the
|
||||
restored DB sees the applied set and only runs whatever NEW migrations
|
||||
this PR adds — exactly the integration shape we want to test, and the
|
||||
same shape prod sees on every deploy.
|
||||
|
||||
**Why a snapshot at all.** Running ApplyMigrations from scratch against a
|
||||
fresh `supabase/postgres:15.8.1.060` surfaces multiple fresh-DB
|
||||
idempotence bugs in historical migrations (raw `COMMIT;` in mig 051,
|
||||
missing `CREATE EXTENSION pg_trgm` for mig 037, ALTER POLICY
|
||||
exception-handler gaps in mig 024/027 — the last is fixed in this PR).
|
||||
Fixing them all is a separate cleanup. The snapshot sidesteps them by
|
||||
starting CI from a state where every historical migration is already
|
||||
applied as it was in prod.
|
||||
|
||||
**Schema scope.** `--schema=paliad` only. Auth schema comes baked into
|
||||
`supabase/postgres`; CI's setup step installs `pg_trgm` before restoring.
|
||||
|
||||
**Ownership.** `--no-owner --no-privileges` keeps the dump portable
|
||||
across role topologies (CI's supabase_admin / postgres / authenticated /
|
||||
anon don't have to match prod's exact role layout). The role-split smoke
|
||||
relies on `postgres` being a non-superuser, which is true on
|
||||
supabase/postgres by default.
|
||||
|
||||
**Refresh.** Run `make refresh-snapshot` with `PALIAD_PROD_DATABASE_URL`
|
||||
set to a Postgres URL with `pg_dump` rights on youpc-supabase. The
|
||||
target appends data rows for `paliad.applied_migrations`, strips
|
||||
`\restrict` / `\unrestrict` commands (pg 16 dump → pg 15 restore), and
|
||||
filters out applied-migrations rows for versions beyond the branch's
|
||||
local max. The CI workflow consumes the resulting file verbatim.
|
||||
|
||||
**Verify a refresh.** Boot a local scratch:
|
||||
|
||||
```bash
|
||||
docker run -d --rm --name paliad-snap \
|
||||
-e POSTGRES_PASSWORD=ci -e POSTGRES_DB=paliad_scratch \
|
||||
-p 15433:5432 supabase/postgres:15.8.1.060
|
||||
sleep 5
|
||||
docker exec -e PGPASSWORD=ci paliad-snap psql -h localhost -U supabase_admin -d paliad_scratch \
|
||||
-c "GRANT CREATE ON DATABASE paliad_scratch TO postgres;" \
|
||||
-c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
|
||||
cat internal/db/testdata/prod-snapshot.sql | docker exec -i -e PGPASSWORD=ci paliad-snap \
|
||||
psql -h localhost -U postgres -d paliad_scratch -v ON_ERROR_STOP=1
|
||||
TEST_DATABASE_URL="postgres://postgres:ci@localhost:15433/paliad_scratch?sslmode=disable" \
|
||||
TEST_APP_DATABASE_URL="postgres://postgres:ci@localhost:15433/paliad_scratch?sslmode=disable" \
|
||||
go test -count=1 -run 'TestMigrations|TestBootSmoke|TestHealthReady_Live' ./internal/db/ ./cmd/server/
|
||||
docker stop paliad-snap
|
||||
```
|
||||
|
||||
All four named tests must pass. If any fails after a refresh,
|
||||
investigate before merging — usually because a new migration was added
|
||||
to prod that this branch doesn't have on disk yet.
|
||||
|
||||
**Why is the snapshot not gzipped?** Small enough (~200 KB) that the
|
||||
diff stays human-readable in `git diff` reviews. If it crosses ~1 MB,
|
||||
gzip + decompress-on-restore in CI.
|
||||
|
||||
**Privacy.** Schema-only dump, no row data from any paliad table (except
|
||||
`paliad.applied_migrations`, which contains migration filenames +
|
||||
checksums — public info already in the repo).
|
||||
6278
internal/db/testdata/prod-snapshot.sql
vendored
6278
internal/db/testdata/prod-snapshot.sql
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"mgit.msbls.de/m/paliad/internal/auth"
|
||||
"mgit.msbls.de/m/paliad/internal/services"
|
||||
@@ -54,12 +50,6 @@ func noCachePages(h http.Handler) http.Handler {
|
||||
// Services bundles the Phase B + C database-backed services. Pass nil if
|
||||
// DATABASE_URL was unset; the matter-management endpoints will return 503.
|
||||
type Services struct {
|
||||
// Pool is the raw connection pool. Held so the readiness probe
|
||||
// (/health/ready) can ping it without going through any individual
|
||||
// service. nil when DATABASE_URL was unset — in that case
|
||||
// /health/ready returns 503.
|
||||
Pool *sqlx.DB
|
||||
|
||||
Project *services.ProjectService
|
||||
Team *services.TeamService
|
||||
PartnerUnit *services.PartnerUnitService
|
||||
@@ -198,38 +188,6 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
|
||||
// Readiness probe. Public, no auth. Distinct from /healthz: this
|
||||
// returns 200 only when the DB pool is reachable. Reaching Register
|
||||
// at all implies db.ApplyMigrations succeeded (cmd/server/main.go
|
||||
// calls it before constructing svc), so a 200 here means "migrations
|
||||
// applied AND pool responsive" — the contract Dokploy / Traefik should
|
||||
// gate on, not the bind-and-serve check that /healthz answers.
|
||||
//
|
||||
// Three outcomes:
|
||||
// - svc == nil OR svc.Pool == nil → 503 (DB-less knowledge-platform
|
||||
// deployments report not-ready so an external orchestrator can
|
||||
// distinguish them from a full prod boot).
|
||||
// - PingContext fails within 2 s → 503 (pool unreachable).
|
||||
// - PingContext succeeds → 200 "ready".
|
||||
//
|
||||
// Used by docker-compose.yml's healthcheck (Slice B) and by the
|
||||
// post-deploy verification step in .gitea/workflows/test.yaml.
|
||||
mux.HandleFunc("GET /health/ready", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if svc == nil || svc.Pool == nil {
|
||||
http.Error(w, "db not configured\n", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := svc.Pool.PingContext(ctx); err != nil {
|
||||
http.Error(w, "db unreachable\n", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("ready\n"))
|
||||
})
|
||||
|
||||
// API endpoints (JSON, public)
|
||||
mux.HandleFunc("POST /api/login", handleAPILogin)
|
||||
mux.HandleFunc("POST /api/register", handleAPIRegister)
|
||||
|
||||
Reference in New Issue
Block a user