diff --git a/.gitea/workflows/release-images.yaml b/.gitea/workflows/release-images.yaml index 2f4bc49..06c873d 100644 --- a/.gitea/workflows/release-images.yaml +++ b/.gitea/workflows/release-images.yaml @@ -47,7 +47,7 @@ jobs: --output release/release-manifest.json python3 infra/deploy/validate_release.py release/release-manifest.json \ --policy infra/deploy/release-policy.json - sha256sum release/release-manifest.json > release/release-manifest.json.sha256 + (cd release && sha256sum release-manifest.json > release-manifest.json.sha256) - name: Upload verified release descriptor uses: https://gitea.com/actions/gitea-upload-artifact@055b8b3f04a4a7ed853f2c6ab04256f83e4874dc with: @@ -59,5 +59,5 @@ jobs: retention-days: 90 - name: Confirm uploaded release descriptor run: | - sha256sum -c release/release-manifest.json.sha256 + (cd release && sha256sum -c release-manifest.json.sha256) echo 'Release descriptor and checksum are available as this run artifact.' diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d2aec..655e4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ - Добавлен ручной выпуск production-образов только из защищённой `main`: rootless Kaniko, digest references и schema-v3 descriptor с SHA-256 в Gitea - artifact. Подготовлены owner-run cutover и rollback с проверкой backup, - image IDs, RU/KY canaries и recovery journal; автодеплой не включён. CI + artifact; sidecar проверяется после извлечения из корня. Подготовлены + owner-run cutover и rollback с проверкой backup, image IDs, RU/KY canaries и + recovery journal; автодеплой не включён. CI проверяет synthetic artifact и завершает шаг ошибкой при отсутствии файлов. - Frontend `0.3.3` / Backend `0.13.2`: управление пользователями отображается согласно правам аккаунта, подписи ролей локализованы, повторяющиеся действия diff --git a/docs/operations/CI_RUNNER.md b/docs/operations/CI_RUNNER.md index 2b4ae34..79a2a76 100644 --- a/docs/operations/CI_RUNNER.md +++ b/docs/operations/CI_RUNNER.md @@ -92,7 +92,10 @@ synthetic marker with one-day retention to verify that endpoint without registry credentials. Runner cache remains disabled and unchanged. The release artifact requests 90-day retention, subject to the instance's configured cap; the owner must confirm retention covers the rollback window. The workflow has -not yet been run with registry credentials. +not yet been run with registry credentials. The sidecar is generated from the +`release` directory using the manifest basename, so the downloaded artifact can +be verified from its extraction root with +`sha256sum -c release-manifest.json.sha256`. The observed Gitea 1.25.5 run API returns `run_attempt: 0` (the legacy sentinel), so release image tags, artifact names, and schema v3 descriptors use the unique diff --git a/docs/operations/CURRENT_STATE.md b/docs/operations/CURRENT_STATE.md index 31a0280..505d850 100644 --- a/docs/operations/CURRENT_STATE.md +++ b/docs/operations/CURRENT_STATE.md @@ -47,6 +47,11 @@ conflicts with this dated recheck. and contained the expected 51-byte `marker.txt`. The API artifact listing available to this principal returned no entries, but the downloaded file verifies artifact availability. No live release or VPS change was made. +- The release descriptor SHA-256 sidecar now records `release-manifest.json` + relative to the `release` directory, matching the flattened layout observed + for downloaded Gitea artifacts. A local synthetic regression verifies the + checksum from the artifact extraction root; the credentialed release + workflow remains unrun. - Owner-only outstanding evidence: sanitized container/volume and protected file metadata; current backup and separate-target restore verification; live corpus/index counts and RU/KY canaries; Cloudflare TLS mode; registry diff --git a/infra/deploy/test_release_workflow.py b/infra/deploy/test_release_workflow.py index f68c845..9a7c13f 100644 --- a/infra/deploy/test_release_workflow.py +++ b/infra/deploy/test_release_workflow.py @@ -1,5 +1,6 @@ import os import subprocess +import tempfile import unittest from pathlib import Path @@ -21,7 +22,7 @@ class ReleaseWorkflowTests(unittest.TestCase): self.assertIn("if-no-files-found: error", workflow) self.assertNotIn("github.run_attempt", workflow) self.assertIn("${{ github.run_id }}", workflow) - self.assertIn("sha256sum -c release/release-manifest.json.sha256", workflow) + self.assertIn("(cd release && sha256sum -c release-manifest.json.sha256)", workflow) self.assertNotIn("docker compose", workflow) def test_pr_ci_has_small_synthetic_artifact_upload_without_registry_secrets(self): @@ -35,6 +36,44 @@ class ReleaseWorkflowTests(unittest.TestCase): self.assertIn("artifact-smoke/marker.txt", workflow) self.assertNotIn("REGISTRY_PASSWORD", workflow) + def test_release_checksum_survives_flattened_artifact_layout(self): + workflow = (ROOT / ".gitea/workflows/release-images.yaml").read_text(encoding="utf-8") + generate = "(cd release && sha256sum release-manifest.json > release-manifest.json.sha256)" + verify = "(cd release && sha256sum -c release-manifest.json.sha256)" + self.assertIn(generate, workflow) + self.assertIn(verify, workflow) + + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + release = root / "release" + release.mkdir() + (release / "release-manifest.json").write_text( + "synthetic manifest\n", encoding="utf-8" + ) + + generated = subprocess.run( + ["bash", "-e", "-c", generate], cwd=root, text=True, capture_output=True + ) + self.assertEqual(generated.returncode, 0, generated.stderr) + verified = subprocess.run( + ["bash", "-e", "-c", verify], cwd=root, text=True, capture_output=True + ) + self.assertEqual(verified.returncode, 0, verified.stderr) + + extracted = root / "artifact-root" + extracted.mkdir() + (extracted / "release-manifest.json").write_bytes((release / "release-manifest.json").read_bytes()) + (extracted / "release-manifest.json.sha256").write_bytes( + (release / "release-manifest.json.sha256").read_bytes() + ) + downloaded = subprocess.run( + ["sha256sum", "-c", "release-manifest.json.sha256"], + cwd=extracted, + text=True, + capture_output=True, + ) + self.assertEqual(downloaded.returncode, 0, downloaded.stderr) + def test_pr_build_remains_no_push_and_push_action_has_no_engine_socket(self): build_action = (ROOT / ".gitea/actions/kaniko-build/action.yml").read_text(encoding="utf-8") push_action = (ROOT / ".gitea/actions/kaniko-push/release-build.sh").read_text(encoding="utf-8")