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:
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):