Isolate publication fixtures and bind CI receipts to immutable image
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
7c51a7b185
commit
bb193b2e87
4 changed files with 57 additions and 3 deletions
|
|
@ -10,6 +10,9 @@ on:
|
||||||
- ".forgejo/workflows/publish-image.yaml"
|
- ".forgejo/workflows/publish-image.yaml"
|
||||||
- "Containerfile"
|
- "Containerfile"
|
||||||
- "deploy/**"
|
- "deploy/**"
|
||||||
|
- "docs/architecture/**"
|
||||||
|
- "docs/adr/**"
|
||||||
|
- "docs/evidence/*served-publication-history/**"
|
||||||
- "publication.json"
|
- "publication.json"
|
||||||
- "source-inventory.config.json"
|
- "source-inventory.config.json"
|
||||||
- "source-inventory.json"
|
- "source-inventory.json"
|
||||||
|
|
@ -114,11 +117,15 @@ jobs:
|
||||||
docker push "${IMAGE}:main"
|
docker push "${IMAGE}:main"
|
||||||
|
|
||||||
PUBLICATION_DIGEST=$(docker run --rm --entrypoint sha256sum \
|
PUBLICATION_DIGEST=$(docker run --rm --entrypoint sha256sum \
|
||||||
"${IMAGE}:${SOURCE_TAG}" /usr/share/nginx/html/publication-manifest.json \
|
"${IMAGE}@${IMAGE_DIGEST}" /usr/share/nginx/html/publication-manifest.json \
|
||||||
| awk '{print $1}')
|
| awk '{print $1}')
|
||||||
SOURCE_INVENTORY_DIGEST=$(docker run --rm --entrypoint sha256sum \
|
SOURCE_INVENTORY_DIGEST=$(docker run --rm --entrypoint sha256sum \
|
||||||
"${IMAGE}:${SOURCE_TAG}" /usr/share/nginx/html/source-inventory.json \
|
"${IMAGE}@${IMAGE_DIGEST}" /usr/share/nginx/html/source-inventory.json \
|
||||||
| awk '{print $1}')
|
| awk '{print $1}')
|
||||||
|
SOURCE_LOCK_DIGEST=$(docker run --rm --entrypoint sha256sum \
|
||||||
|
"${IMAGE}@${IMAGE_DIGEST}" /usr/share/nginx/html/source-lock.json \
|
||||||
|
| awk '{print $1}')
|
||||||
|
echo "source_lock_digest=${SOURCE_LOCK_DIGEST}"
|
||||||
echo "published=${IMAGE}:${SOURCE_TAG}"
|
echo "published=${IMAGE}:${SOURCE_TAG}"
|
||||||
echo "image_digest=${IMAGE_DIGEST}"
|
echo "image_digest=${IMAGE_DIGEST}"
|
||||||
echo "publication_manifest_digest=${PUBLICATION_DIGEST}"
|
echo "publication_manifest_digest=${PUBLICATION_DIGEST}"
|
||||||
|
|
@ -133,6 +140,7 @@ jobs:
|
||||||
echo "- image: \`${IMAGE}@${IMAGE_DIGEST}\`"
|
echo "- image: \`${IMAGE}@${IMAGE_DIGEST}\`"
|
||||||
echo "- publication manifest: \`${PUBLICATION_DIGEST}\`"
|
echo "- publication manifest: \`${PUBLICATION_DIGEST}\`"
|
||||||
echo "- source inventory: \`${SOURCE_INVENTORY_DIGEST}\`"
|
echo "- source inventory: \`${SOURCE_INVENTORY_DIGEST}\`"
|
||||||
|
echo "- source lock: \`${SOURCE_LOCK_DIGEST}\`"
|
||||||
echo "- source set: \`${SOURCE_SET_DIGEST}\`"
|
echo "- source set: \`${SOURCE_SET_DIGEST}\`"
|
||||||
echo
|
echo
|
||||||
echo "Promotion requires an explicit paired digest update in rapp-policy-nexus and railiance-apps."
|
echo "Promotion requires an explicit paired digest update in rapp-policy-nexus and railiance-apps."
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ RUN python3 -m unittest discover -s tests -p 'test_*.py' \
|
||||||
--lock /workspace/_sources/source-lock.json \
|
--lock /workspace/_sources/source-lock.json \
|
||||||
--report build/source-inventory.json \
|
--report build/source-inventory.json \
|
||||||
&& test "$(python3 -c 'import json; print(json.load(open("/workspace/_sources/source-lock.json"))["source_set_digest"])')" = "$SOURCE_SET_DIGEST" \
|
&& test "$(python3 -c 'import json; print(json.load(open("/workspace/_sources/source-lock.json"))["source_set_digest"])')" = "$SOURCE_SET_DIGEST" \
|
||||||
|
&& cp /workspace/_sources/source-lock.json build/source-lock.json \
|
||||||
&& python3 tools/verify_release.py build \
|
&& python3 tools/verify_release.py build \
|
||||||
&& python3 tools/check_currency.py publication.json
|
&& python3 tools/check_currency.py publication.json
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@ from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from unittest import mock
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[1]
|
ROOT = Path(__file__).parents[1]
|
||||||
sys.path.insert(0, str(ROOT / "tools"))
|
sys.path.insert(0, str(ROOT / "tools"))
|
||||||
from fetch_sources import _archive_url, _request
|
from fetch_sources import _archive_url, _request, fetch
|
||||||
|
|
||||||
|
|
||||||
class FetchSourcesAuthenticationTest(unittest.TestCase):
|
class FetchSourcesAuthenticationTest(unittest.TestCase):
|
||||||
|
|
@ -53,5 +58,37 @@ class FetchSourcesAuthenticationTest(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_locks_workflow_self_sha_and_exact_resolved_upstream_archive(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
config = root / "config.json"
|
||||||
|
config.write_text(json.dumps({"schema_version": 1, "repositories": {
|
||||||
|
"policy-nexus": {"local": True},
|
||||||
|
"owner": {"remote": "https://forgejo.coulomb.social/coulomb/owner.git", "branch": "main"},
|
||||||
|
}}))
|
||||||
|
self_sha, owner_sha = "b" * 40, "a" * 40
|
||||||
|
with mock.patch("fetch_sources._revision", return_value=owner_sha) as resolve, \
|
||||||
|
mock.patch("fetch_sources.urllib.request.urlopen", return_value=io.BytesIO(b"archive")) as opened, \
|
||||||
|
mock.patch("fetch_sources._extract"):
|
||||||
|
lock = fetch(config, root / "sources", self_sha)
|
||||||
|
self.assertEqual(lock["repositories"]["policy-nexus"]["revision"], self_sha)
|
||||||
|
self.assertEqual(lock["repositories"]["owner"]["revision"], owner_sha)
|
||||||
|
self.assertEqual(resolve.call_count, 1)
|
||||||
|
self.assertEqual(opened.call_args.args[0].full_url,
|
||||||
|
"https://forgejo.coulomb.social/coulomb/owner/archive/" + owner_sha + ".tar.gz")
|
||||||
|
canonical = json.dumps({"owner": owner_sha, "policy-nexus": self_sha},
|
||||||
|
sort_keys=True, separators=(",", ":")).encode()
|
||||||
|
self.assertEqual(lock["source_set_digest"], hashlib.sha256(canonical).hexdigest())
|
||||||
|
self.assertEqual(json.loads((root / "sources/source-lock.json").read_text()), lock)
|
||||||
|
|
||||||
|
def test_mutable_self_reference_refused_before_fetch(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
with self.assertRaisesRegex(ValueError, "clean 40-hex"), \
|
||||||
|
mock.patch("fetch_sources.urllib.request.urlopen") as opened:
|
||||||
|
fetch(root / "config.json", root / "sources", "main")
|
||||||
|
opened.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,14 @@ class PublicationTest(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class HistoricalRevisionTest(unittest.TestCase):
|
class HistoricalRevisionTest(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
# Fixtures are independent repositories, not members of the CI source lock.
|
||||||
|
self.source_root_env = mock.patch.dict(
|
||||||
|
os.environ, {"POLICY_NEXUS_SOURCE_ROOT": ""}, clear=False
|
||||||
|
)
|
||||||
|
self.source_root_env.start()
|
||||||
|
self.addCleanup(self.source_root_env.stop)
|
||||||
|
|
||||||
def prepare(self, root):
|
def prepare(self, root):
|
||||||
manifest = _fixture(root)
|
manifest = _fixture(root)
|
||||||
data = json.loads(manifest.read_text())
|
data = json.loads(manifest.read_text())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue