Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
APP_NAME=YGIT
APP_ENV=development
APP_VERSION=0.1.0
SOURCE_COMMIT=
YGIT_DEPLOYMENT_CONFIG_VERSION=1.0.0
APP_BASE_URL=http://localhost:8000
API_PREFIX=/api/v1
DEBUG=true

DATABASE_URL=postgresql+asyncpg://ygit:ygit_password@postgres:5432/ygit
# Docker Compose PostgreSQL settings.
# Production values are Coolify-managed secrets and must match DATABASE_URL.
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
DATABASE_URL=postgresql+asyncpg://ygit:change-me@postgres:5432/ygit
REDIS_URL=redis://redis:6379/0

SESSION_SECRET=change-me-in-production-use-at-least-32-chars
Expand Down
13 changes: 12 additions & 1 deletion CONTRACT_MANIFEST.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@
"backend_ci_specification": "docs/ci/BACKEND_CI_SPECIFICATION.md",
"backend_ci_implementation_plan": "docs/ci/BACKEND_CI_IMPLEMENTATION_PLAN.md",
"backend_ci_testing_and_rollback": "docs/ci/BACKEND_CI_TESTING_AND_ROLLBACK_SPECIFICATION.md",
"phase0_completion_record": "docs/phase0/PHASE0_COMPLETION_RECORD.md"
"phase0_completion_record": "docs/phase0/PHASE0_COMPLETION_RECORD.md",
"coolify_production_redeploy_configuration_specification": "docs/deployment/COOLIFY_PRODUCTION_REDEPLOY_CONFIGURATION_SPECIFICATION.md"
},
"release_gate": {
"new_migration": null,
Expand Down Expand Up @@ -492,5 +493,15 @@
"deployment_performed": false,
"coolify_redeploy_performed": false,
"branch_protection_enabled": false
},
"deployment_configuration": {
"version": "1.0.0",
"status": "approved_for_controlled_redeploy",
"topology": "coolify_docker_compose",
"compose_path": "/docker-compose.yml",
"exact_commit_required": true,
"head_selector_forbidden": true,
"provider_mode_first_redeploy": "disabled",
"runtime_environment": "YGIT_DEPLOYMENT_CONFIG_VERSION"
}
}
19 changes: 17 additions & 2 deletions LIVE_DEPLOYMENT_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ It does not declare production readiness before real PostgreSQL, Redis, GitHub,
- Keep `WORKER_PROVIDER_EXECUTION_MODE=disabled` during the first redeploy and infrastructure checks.
- Enable `cloudflare` only after the disabled-mode checks pass.
- Record the deployed Git commit before every redeploy.
- Pin the exact verified hardening merge commit; `HEAD` is prohibited.
- Set `SOURCE_COMMIT` to the same exact 40-character commit SHA.
- Set `YGIT_DEPLOYMENT_CONFIG_VERSION=1.0.0`.
- Set `KEYCLOAK_POST_LOGOUT_REDIRECT_URI=https://ygit.net`.
- Store `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` only in Coolify secret storage.
- Remove any user-defined `COOLIFY_BRANCH` override and allow Coolify to provide plain `main`.
- Revert provider mode to `disabled` immediately when provider behavior is uncertain.
- Do not integrate AG-001 during this validation. AG-001 remains reserved for future App Engine work.

Expand All @@ -41,7 +47,8 @@ Then run on the production application container or an equivalent environment co
```bash
python scripts/live_readiness.py \
--mode pre-redeploy \
--expected-provider-mode disabled
--expected-provider-mode disabled \
--expected-config-version 1.0.0
```

Required result:
Expand All @@ -58,15 +65,21 @@ This checks required configuration presence, PostgreSQL `SELECT 1`, Redis `PING`
Redeploy commit:

```text
<record the Batch 3-R2 runtime-image packaging commit SHA here>
<VERIFIED_HARDENING_MERGE_SHA>
```

Keep:

```text
SOURCE_COMMIT=<VERIFIED_HARDENING_MERGE_SHA>
YGIT_DEPLOYMENT_CONFIG_VERSION=1.0.0
WORKER_PROVIDER_EXECUTION_MODE=disabled
GITHUB_APP_WEBHOOK_ENABLED=false
KEYCLOAK_POST_LOGOUT_REDIRECT_URI=https://ygit.net
```

Coolify must provide `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` as secrets. The `DATABASE_URL` value must use the same database identity.

Confirm the API and worker use the same Git commit and production environment.

## Phase 3 — Post-Redeploy Disabled-Mode Smoke
Expand All @@ -77,6 +90,7 @@ Run:
python scripts/live_readiness.py \
--mode post-redeploy \
--expected-provider-mode disabled \
--expected-config-version 1.0.0 \
--base-url https://ygit.net
```

Expand Down Expand Up @@ -118,6 +132,7 @@ Run:
python scripts/live_readiness.py \
--mode post-redeploy \
--expected-provider-mode cloudflare \
--expected-config-version 1.0.0 \
--base-url https://ygit.net
```

Expand Down
8 changes: 8 additions & 0 deletions VERSION.json
Original file line number Diff line number Diff line change
Expand Up @@ -353,5 +353,13 @@
"azure_devops"
]
}
},
"deployment_configuration": {
"version": "1.0.0",
"schema_version": 1,
"topology": "coolify_docker_compose",
"compose_path": "/docker-compose.yml",
"provider_mode_default": "disabled",
"migration_head": "0012_notification_engine"
}
}
205 changes: 205 additions & 0 deletions backend/tests/test_coolify_production_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[2]
COMPOSE = ROOT / "docker-compose.yml"
ENV_EXAMPLE = ROOT / ".env.example"
VERSION = ROOT / "VERSION.json"
MANIFEST = ROOT / "CONTRACT_MANIFEST.json"
RUNBOOK = ROOT / "LIVE_DEPLOYMENT_RUNBOOK.md"


def compose_text() -> str:
return COMPOSE.read_text(encoding="utf-8")


def test_hard_coded_postgres_credentials_are_removed() -> None:
source = compose_text()

assert "POSTGRES_PASSWORD: ygit_password" not in source
assert "POSTGRES_USER: ygit" not in source
assert "POSTGRES_DB: ygit" not in source


def test_postgres_compose_variables_are_required() -> None:
source = compose_text()

assert (
'${POSTGRES_USER:?POSTGRES_USER is required}'
in source
)
assert (
'${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}'
in source
)
assert (
'${POSTGRES_DB:?POSTGRES_DB is required}'
in source
)


def test_postgres_healthcheck_uses_runtime_database_identity() -> None:
source = compose_text()

assert 'pg_isready -U \\"$$POSTGRES_USER\\"' in source
assert '-d \\"$$POSTGRES_DB\\"' in source
assert "pg_isready -U ygit -d ygit" not in source


def test_compose_service_topology_is_unchanged() -> None:
source = compose_text()
service_names = set(
re.findall(
r"^ ([a-z0-9-]+):$",
source,
flags=re.MULTILINE,
)
)

assert service_names == {
"ygit-api",
"ygit-worker",
"ygit-migrate",
"postgres",
"redis",
}


def test_compose_runtime_and_migration_commands_are_unchanged() -> None:
source = compose_text()

assert (
"command: uvicorn backend.app.main:app "
"--host 0.0.0.0 --port 8000"
in source
)
assert "command: python -m backend.workers.main" in source
assert "command: alembic upgrade head" in source
assert source.count("condition: service_completed_successfully") == 2


def test_compose_persistence_and_health_contract_is_preserved() -> None:
source = compose_text()

assert source.count("ygit_postgres_data") == 2
assert source.count("ygit_redis_data") == 2
assert "pg_isready" in source
assert 'test: ["CMD", "redis-cli", "ping"]' in source
assert "--appendonly" in source


@pytest.mark.parametrize(
"missing_name",
[
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_DB",
],
)
def test_docker_compose_config_fails_closed_when_postgres_value_is_missing(
tmp_path: Path,
missing_name: str,
) -> None:
docker = shutil.which("docker")
if docker is None:
pytest.skip("Docker Compose CLI is unavailable")

compose_copy = tmp_path / "docker-compose.yml"
compose_copy.write_text(
compose_text(),
encoding="utf-8",
)
(tmp_path / ".env").write_text("", encoding="utf-8")

environment = os.environ.copy()
environment.update(
{
"POSTGRES_USER": "ygit_test",
"POSTGRES_PASSWORD": "not-a-production-secret",
"POSTGRES_DB": "ygit_test",
}
)
environment.pop(missing_name, None)

result = subprocess.run( # noqa: S603
[
docker,
"compose",
"-f",
str(compose_copy),
"config",
],
cwd=tmp_path,
env=environment,
capture_output=True,
text=True,
check=False,
)

assert result.returncode != 0
assert missing_name in (result.stdout + result.stderr)


def test_environment_example_preserves_safe_defaults() -> None:
source = ENV_EXAMPLE.read_text(encoding="utf-8")

assert "SOURCE_COMMIT=" in source
assert "YGIT_DEPLOYMENT_CONFIG_VERSION=1.0.0" in source
assert "POSTGRES_USER=" in source
assert "POSTGRES_PASSWORD=" in source
assert "POSTGRES_DB=" in source
assert "WORKER_PROVIDER_EXECUTION_MODE=disabled" in source
assert "GITHUB_APP_WEBHOOK_ENABLED=false" in source
assert "GITHUB_OAUTH_CLIENT_ID" not in source
assert "GITHUB_OAUTH_CLIENT_SECRET" not in source
assert "ygit_password" not in source


def test_deployment_configuration_registries_align() -> None:
version = json.loads(VERSION.read_text(encoding="utf-8"))
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
version_config = version["deployment_configuration"]
manifest_config = manifest["deployment_configuration"]

assert version_config == {
"version": "1.0.0",
"schema_version": 1,
"topology": "coolify_docker_compose",
"compose_path": "/docker-compose.yml",
"provider_mode_default": "disabled",
"migration_head": "0012_notification_engine",
}
assert manifest_config["version"] == version_config["version"]
assert manifest_config["topology"] == version_config["topology"]
assert manifest_config["compose_path"] == version_config["compose_path"]
assert manifest_config["exact_commit_required"] is True
assert manifest_config["head_selector_forbidden"] is True
assert manifest_config["provider_mode_first_redeploy"] == "disabled"
assert (
manifest_config["runtime_environment"]
== "YGIT_DEPLOYMENT_CONFIG_VERSION"
)


def test_runbook_locks_controlled_redeploy_identity() -> None:
source = RUNBOOK.read_text(encoding="utf-8")

assert source.count("--expected-config-version 1.0.0") == 3
assert "<VERIFIED_HARDENING_MERGE_SHA>" in source
assert "`HEAD` is prohibited" in source
assert "SOURCE_COMMIT" in source
assert "YGIT_DEPLOYMENT_CONFIG_VERSION=1.0.0" in source
assert (
"KEYCLOAK_POST_LOGOUT_REDIRECT_URI=https://ygit.net"
in source
)
assert "WORKER_PROVIDER_EXECUTION_MODE=disabled" in source
assert "Coolify secret storage" in source
Loading
Loading