taler-deployment

Deployment scripts and configuration files
Log | Files | Refs | README

commit c9b1f77872c69a65bfff0144140fb182d1d635d0
parent 68b5b13f1a6705857cdd3fc0cf4df2c6f67c42fe
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 13:00:47 +0200

packaging/ng: initialize stable publications from empty snapshots

Diffstat:
Mpackaging/ng/server-side/README.md | 18+++++++++---------
Mpackaging/ng/taler-pkg | 58+++++++++++++++++++++++++++-------------------------------
Mpackaging/ng/testing/test_publishing.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 107 insertions(+), 55 deletions(-)

diff --git a/packaging/ng/server-side/README.md b/packaging/ng/server-side/README.md @@ -1,12 +1,12 @@ # Aptly repository layout The package repository on `taler-packaging@taler.net` has one mutable testing -repo and one bootstrap repo for each supported distribution: +repo for each supported distribution: -| Build distro | Testing repo | Stable bootstrap repo | Publish prefix | -| --- | --- | --- | --- | -| `debian-trixie` | `taler-debian-trixie-testing` | `taler-debian-trixie-stable` | `apt/debian` | -| `ubuntu-noble` | `taler-ubuntu-noble-testing` | `taler-ubuntu-noble-stable` | `apt/ubuntu` | +| Build distro | Testing repo | Publish prefix | +| --- | --- | --- | +| `debian-trixie` | `taler-debian-trixie-testing` | `apt/debian` | +| `ubuntu-noble` | `taler-ubuntu-noble-testing` | `apt/ubuntu` | These prefixes use aptly's default filesystem endpoint. Aptly therefore publishes them below its default publish directory as @@ -24,10 +24,10 @@ Run the initialization from `packaging/ng`: ``` The command creates anything missing for both distributions. It directly -publishes each testing repo, creates an initial snapshot from each stable -bootstrap repo, and publishes that snapshot as stable. Rerunning the command -validates the existing topology and otherwise does nothing. It deliberately -refuses to replace incompatible publications. +publishes each testing repo, creates an empty initial snapshot, and publishes +that snapshot as stable. Rerunning the command validates the existing topology +and otherwise does nothing. It deliberately refuses to replace incompatible +publications. Normal uploads go only to the testing repos. `taler-pkg promote DISTRO` snapshots the complete testing repo and atomically switches the stable diff --git a/packaging/ng/taler-pkg b/packaging/ng/taler-pkg @@ -48,10 +48,6 @@ class PublishingConfig: return f"apt/{self.vendor}" @property - def stable_repo(self): - return f"taler-{self.vendor}-{self.codename}-stable" - - @property def testing_repo(self): return f"taler-{self.vendor}-{self.codename}-testing" @@ -61,7 +57,7 @@ class PublishingConfig: @property def initial_snapshot(self): - return f"{self.stable_repo}-initial" + return f"taler-{self.vendor}-{self.codename}-stable-initial" publishing_configs = { @@ -415,17 +411,25 @@ def validate_publication(pubcfg, publication, *, stable): if stable: if source[1] != "snapshot": raise ValueError(f"stable publication {distribution} is not a snapshot") - origin = snapshot_origin(source[0]) - if origin not in {pubcfg.stable_repo, pubcfg.testing_repo}: - raise ValueError( - f"stable publication {distribution} has unexpected source {origin}" - ) + validate_stable_snapshot(pubcfg, source[0]) elif source != (pubcfg.testing_repo, "local"): raise ValueError( f"testing publication {distribution} has unexpected source {source[0]}" ) +def validate_stable_snapshot(pubcfg, snapshot): + if snapshot == pubcfg.initial_snapshot: + if snapshot_packages(snapshot): + raise ValueError(f"initial snapshot {snapshot} is not empty") + else: + origin = snapshot_origin(snapshot) + if origin != pubcfg.testing_repo: + raise ValueError( + f"stable publication {pubcfg.codename} has unexpected source {origin}" + ) + + def publish_options(distribution): return [ f"-architectures={','.join(sorted(archs))}", @@ -445,20 +449,16 @@ def initialize(cfg): changed = False for pubcfg in publishing_configs.values(): - for repo, distribution in ( - (pubcfg.stable_repo, pubcfg.codename), - (pubcfg.testing_repo, pubcfg.testing_distribution), - ): - if repo not in repos: - remote_aptly( - "repo", - "create", - f"-distribution={distribution}", - "-component=main", - repo, - ) - repos.add(repo) - changed = True + if pubcfg.testing_repo not in repos: + remote_aptly( + "repo", + "create", + f"-distribution={pubcfg.testing_distribution}", + "-component=main", + pubcfg.testing_repo, + ) + repos.add(pubcfg.testing_repo) + changed = True testing_key = (pubcfg.prefix, pubcfg.testing_distribution) if testing_key not in publications: @@ -486,16 +486,12 @@ def initialize(cfg): "snapshot", "create", pubcfg.initial_snapshot, - "from", - "repo", - pubcfg.stable_repo, + "empty", ) snapshots.add(pubcfg.initial_snapshot) changed = True - elif snapshot_origin(pubcfg.initial_snapshot) != pubcfg.stable_repo: - raise ValueError( - f"snapshot {pubcfg.initial_snapshot} has an unexpected source" - ) + else: + validate_stable_snapshot(pubcfg, pubcfg.initial_snapshot) remote_aptly( "publish", "snapshot", diff --git a/packaging/ng/testing/test_publishing.py b/packaging/ng/testing/test_publishing.py @@ -47,7 +47,9 @@ class PublishingConfigTests(unittest.TestCase): self.assertEqual("taler-debian-trixie-testing", debian.testing_repo) self.assertEqual("trixie-testing", debian.testing_distribution) self.assertEqual("apt/ubuntu", ubuntu.prefix) - self.assertEqual("taler-ubuntu-noble-stable", ubuntu.stable_repo) + self.assertEqual( + "taler-ubuntu-noble-stable-initial", ubuntu.initial_snapshot + ) with self.assertRaisesRegex(ValueError, "unsupported publishing distro"): TALER_PKG.publishing_config("debian-bookworm") @@ -147,9 +149,7 @@ class InitializeTests(unittest.TestCase): "snapshot", "create", "taler-ubuntu-noble-stable-initial", - "from", - "repo", - "taler-ubuntu-noble-stable", + "empty", ), calls, ) @@ -164,13 +164,19 @@ class InitializeTests(unittest.TestCase): ), calls, ) + self.assertFalse( + any( + item.args[:2] == ("repo", "create") + and item.args[-1].endswith("-stable") + for item in calls + ) + ) self.assertIn("initialized", stdout.getvalue()) def test_complete_initialization_is_a_noop(self): repos = "\n".join( - repo + pubcfg.testing_repo for pubcfg in TALER_PKG.publishing_configs.values() - for repo in (pubcfg.stable_repo, pubcfg.testing_repo) ) snapshots = "debian-snapshot\nubuntu-snapshot\n" publications = ( @@ -228,9 +234,8 @@ class InitializeTests(unittest.TestCase): def test_reuses_initial_snapshots_when_only_stable_publish_is_missing(self): repos = "\n".join( - repo + pubcfg.testing_repo for pubcfg in TALER_PKG.publishing_configs.values() - for repo in (pubcfg.stable_repo, pubcfg.testing_repo) ) snapshots = "\n".join( pubcfg.initial_snapshot for pubcfg in TALER_PKG.publishing_configs.values() @@ -255,13 +260,9 @@ class InitializeTests(unittest.TestCase): publication(prefix, distribution, pubcfg.testing_repo, "local") ) if args[:2] == ("snapshot", "show"): - pubcfg = next( - item - for item in TALER_PKG.publishing_configs.values() - if item.initial_snapshot == args[2] - ) return result( - f"Description: Snapshot from local repo [{pubcfg.stable_repo}]\n" + "Description: Snapshot from local repo [legacy-stable-repo]\n" + "Packages:\n" ) return result() @@ -281,7 +282,7 @@ class InitializeTests(unittest.TestCase): wrong = publication( pubcfg.prefix, pubcfg.testing_distribution, - pubcfg.stable_repo, + "unexpected-repo", "local", ) with self.assertRaisesRegex(ValueError, "unexpected source"): @@ -289,6 +290,61 @@ class InitializeTests(unittest.TestCase): pubcfg, TALER_PKG.parse_publication(wrong), stable=False ) + def test_accepts_empty_initial_snapshot_for_stable_publication(self): + pubcfg = TALER_PKG.publishing_config("debian-trixie") + initial = publication( + pubcfg.prefix, + pubcfg.codename, + pubcfg.initial_snapshot, + "snapshot", + ) + + with ( + patch.object(TALER_PKG, "snapshot_packages", return_value=[]), + patch.object(TALER_PKG, "snapshot_origin") as snapshot_origin, + ): + TALER_PKG.validate_publication( + pubcfg, TALER_PKG.parse_publication(initial), stable=True + ) + + snapshot_origin.assert_not_called() + + def test_rejects_nonempty_initial_snapshot(self): + pubcfg = TALER_PKG.publishing_config("debian-trixie") + initial = publication( + pubcfg.prefix, + pubcfg.codename, + pubcfg.initial_snapshot, + "snapshot", + ) + + with ( + patch.object( + TALER_PKG, "snapshot_packages", return_value=["package_1_amd64"] + ), + self.assertRaisesRegex(ValueError, "initial snapshot .* is not empty"), + ): + TALER_PKG.validate_publication( + pubcfg, TALER_PKG.parse_publication(initial), stable=True + ) + + def test_rejects_stable_snapshot_with_wrong_origin(self): + pubcfg = TALER_PKG.publishing_config("debian-trixie") + stable = publication( + pubcfg.prefix, + pubcfg.codename, + "unexpected-snapshot", + "snapshot", + ) + + with ( + patch.object(TALER_PKG, "snapshot_origin", return_value="unexpected-repo"), + self.assertRaisesRegex(ValueError, "unexpected source"), + ): + TALER_PKG.validate_publication( + pubcfg, TALER_PKG.parse_publication(stable), stable=True + ) + class PublishTests(unittest.TestCase): def test_uploads_only_newer_packages_and_updates_testing(self):