From f1e3fd429ac38cd6d44daa68af2799fff2097c91 Mon Sep 17 00:00:00 2001 From: Mustafa Zeydani Date: Thu, 20 Aug 2026 01:20:48 +0300 Subject: [PATCH 1/2] fix(deploy): use approved API operations --- devops/production/bin/backup-postgres.sh | 7 ++--- devops/production/bin/deploy.sh | 29 +++++-------------- .../bin/test_production_deploy_contract.py | 17 +++++++++++ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/devops/production/bin/backup-postgres.sh b/devops/production/bin/backup-postgres.sh index a15fbac..7d70c3a 100755 --- a/devops/production/bin/backup-postgres.sh +++ b/devops/production/bin/backup-postgres.sh @@ -6,7 +6,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SERVER_SERVICES_ROOT="/opt/syr/services/staging" DOCKER_WRAPPER="${SERVER_SERVICES_ROOT}/bin/docker" BACKUP_DIR="/opt/syr/backups/production/opensyria/postgres" -POSTGRES_CONTAINER="infra-postgres" DATABASE_NAME="opensyria_datasets_production" LOCK_FILE="${ROOT_DIR}/.backup.lock" TEMP_DUMP="" @@ -68,12 +67,10 @@ main() { && ! -e "${final_recovery}" && ! -L "${final_recovery}" ]] \ || fail "Backup timestamp collision: ${timestamp}" - docker_cmd exec "${POSTGRES_CONTAINER}" sh -ceu \ - 'exec pg_dump --format=custom --no-owner --no-acl --exclude-extension=postgis --username="$POSTGRES_USER" --dbname="$1"' \ - sh "${DATABASE_NAME}" > "${TEMP_DUMP}" + docker_cmd dump-opensyria-database > "${TEMP_DUMP}" [[ -s "${TEMP_DUMP}" ]] || fail "PostgreSQL produced an empty backup" - docker_cmd exec -i "${POSTGRES_CONTAINER}" pg_restore --list < "${TEMP_DUMP}" >/dev/null + docker_cmd validate-postgres-dump < "${TEMP_DUMP}" >/dev/null digest="$(sha256sum "${TEMP_DUMP}" | cut -d ' ' -f 1)" [[ "${digest}" =~ ^[0-9a-f]{64}$ ]] || fail "Could not calculate backup checksum" printf '%s %s\n' "${digest}" "$(basename "${final_dump}")" > "${TEMP_CHECKSUM}" diff --git a/devops/production/bin/deploy.sh b/devops/production/bin/deploy.sh index fd19596..715847b 100755 --- a/devops/production/bin/deploy.sh +++ b/devops/production/bin/deploy.sh @@ -9,6 +9,7 @@ INFISICAL_LOGIN_HELPER="${SERVER_SERVICES_ROOT}/bin/infisical-login" COMPOSE_FILE="${ROOT_DIR}/docker-compose.app.yml" COMPOSE_ENV_FILE="${ROOT_DIR}/.deploy.env" RUNTIME_ENV_FILE="${ROOT_DIR}/env/api.env" +API_RELEASE_PROBE="fetch('http://127.0.0.1:3000/health/ready').then(async (response)=>{const payload=await response.json();const data=payload.data??{};if(!response.ok||payload.success!==true)throw new Error('readiness failed');if(data.database?.status!=='up'||!data.database?.release)throw new Error('pinned read model is unavailable');if(!(data.database?.recordCount>0))throw new Error('pinned read model is empty');if(typeof data.app?.release!=='string'||!data.app.release)throw new Error('application release is unavailable');process.stdout.write(data.app.release)}).catch((error)=>{console.error(error.message);process.exit(1)})" INFISICAL_CONFIG_FILE="${ROOT_DIR}/.infisical.env" STATE_DIR="${ROOT_DIR}/state" ACTIVE_SLOT_FILE="${STATE_DIR}/active-slot" @@ -217,29 +218,15 @@ wait_for_service_health() { verify_direct_release() { local slot="$1" local expected_release="$2" - local service + local service actual_release service="$(service_for_slot "${slot}")" - compose exec -T -e EXPECTED_RELEASE="${expected_release}" "${service}" node - <<'NODE' -fetch('http://127.0.0.1:3000/health/ready') - .then(async (response) => { - const payload = await response.json() - const data = payload.data ?? {} - if (!response.ok || payload.success !== true) throw new Error('readiness failed') - if (data.app?.release !== process.env.EXPECTED_RELEASE) { - throw new Error('application release mismatch') - } - if (data.database?.status !== 'up' || !data.database?.release) { - throw new Error('pinned read model is unavailable') - } - if (!(data.database?.recordCount > 0)) throw new Error('pinned read model is empty') - }) - .then(() => process.exit(0)) - .catch((error) => { - console.error(error.message) - process.exit(1) - }) -NODE + actual_release="$(compose exec -T "${service}" node -e "${API_RELEASE_PROBE}")" \ + || return 1 + if [[ "${actual_release}" != "${expected_release}" ]]; then + echo "Expected ${service} release ${expected_release}, got ${actual_release:-missing}." >&2 + return 1 + fi } verify_private_route() { diff --git a/devops/production/bin/test_production_deploy_contract.py b/devops/production/bin/test_production_deploy_contract.py index a59a60b..bf50a1f 100644 --- a/devops/production/bin/test_production_deploy_contract.py +++ b/devops/production/bin/test_production_deploy_contract.py @@ -3,6 +3,7 @@ DEPLOY_SCRIPT = Path(__file__).with_name("deploy.sh") +BACKUP_SCRIPT = Path(__file__).with_name("backup-postgres.sh") class ProductionDeployContractTest(unittest.TestCase): @@ -21,6 +22,22 @@ def test_uses_approved_container_diagnostics(self) -> None: self.assertNotIn("docker_cmd inspect", script) self.assertNotIn("compose ps -q", script) + def test_uses_fixed_api_release_probe(self) -> None: + script = DEPLOY_SCRIPT.read_text(encoding="utf-8") + + self.assertIn( + 'compose exec -T "${service}" node -e "${API_RELEASE_PROBE}"', + script, + ) + self.assertNotIn("compose exec -T -e EXPECTED_RELEASE", script) + + def test_uses_typed_postgres_backup_operations(self) -> None: + script = BACKUP_SCRIPT.read_text(encoding="utf-8") + + self.assertIn("docker_cmd dump-opensyria-database", script) + self.assertIn("docker_cmd validate-postgres-dump", script) + self.assertNotIn("docker_cmd exec", script) + if __name__ == "__main__": unittest.main() From 9db8ee6a94e71c625c00c91737cf76773b1008b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:22:25 +0000 Subject: [PATCH 2/2] chore(main): release 0.3.4 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ package.json | 2 +- src/tools/swagger/swagger.setup.ts | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6969ddf..18cac80 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.3" + ".": "0.3.4" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3be07..52418d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ * update vulnerable runtime dependencies, constrain patched transitives, and audit the complete graph during validation * share production image layers, refresh and retry registry pulls, and allow bounded cold ARM pulls to finish +## [0.3.4](https://github.com/Open-Syria/datasets-api/compare/v0.3.3...v0.3.4) (2026-08-19) + + +### Bug Fixes + +* **deploy:** use approved API operations ([d9290b8](https://github.com/Open-Syria/datasets-api/commit/d9290b8fa16864efaba51641428a6948bbe640c3)) +* **deploy:** use approved API operations ([f1e3fd4](https://github.com/Open-Syria/datasets-api/commit/f1e3fd429ac38cd6d44daa68af2799fff2097c91)) + ## [0.3.3](https://github.com/Open-Syria/datasets-api/compare/v0.3.2...v0.3.3) (2026-08-19) diff --git a/package.json b/package.json index 74d0c5e..b14ff9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@opensyria/datasets-api", - "version": "0.3.3", + "version": "0.3.4", "description": "Read-only datasets API for OpenSyria.", "author": "OpenSyria", "private": true, diff --git a/src/tools/swagger/swagger.setup.ts b/src/tools/swagger/swagger.setup.ts index 17aee65..0d34298 100644 --- a/src/tools/swagger/swagger.setup.ts +++ b/src/tools/swagger/swagger.setup.ts @@ -379,7 +379,7 @@ export async function setupSwagger(app: NestFastifyApplication, appConfig: AppCo const documentConfig = new DocumentBuilder() .setTitle(API_DOCUMENT_TITLE) .setDescription(API_DOCUMENT_DESCRIPTION) - .setVersion('0.3.3') // x-release-please-version + .setVersion('0.3.4') // x-release-please-version .setOpenAPIVersion('3.1.0') .addServer(appConfig.url);