Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 8: [PATCH 08/17] chore(roots): integrate v31 identity and maintenance tooling

ci/test/test_release_version.py

Before: /dev/null · After: ci/test/test_release_version.py

File metadata
new file mode 100755
index 0000000000000000000000000000000000000000..16a74304f99c229923ddb9ddc9cf85cc0ae1ad1a

Download original patch · Release notes

Diff rows 1–167 of 167. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.

ci/test/test_release_version.py ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026-present The Bitcoin Roots developers
3
+ # Distributed under the MIT software license, see the accompanying
4
+ # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
+
6
+ from pathlib import Path
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import unittest
13
+ import zipfile
14
+
15
+
16
+ ROOT = Path(__file__).resolve().parents[2]
17
+ BUILD_CONFIG = ROOT / "CMakeLists.txt"
18
+ ARCHIVE_TOOL = ROOT / "ci/release/archive.py"
19
+ RELEASE_WORKFLOW = ROOT / ".github/workflows/release.yml"
20
+
21
+
22
+ class ReleaseVersionTest(unittest.TestCase):
23
+ def configure_release_version(self, tag=None):
24
+ build_config = BUILD_CONFIG.read_text(encoding="utf-8")
25
+ version_start = re.search(r'^set\(CLIENT_VERSION_STRING ', build_config, re.MULTILINE)
26
+ self.assertIsNotNone(version_start, "Release version configuration must be executable CMake")
27
+ version_block = build_config[version_start.start():].split('#=============================', 1)[0]
28
+ with tempfile.TemporaryDirectory(prefix="roots-release-version-") as directory:
29
+ source = Path(directory)
30
+ metadata = "\n".join(
31
+ re.search(rf"set\({name} [^)]+\)", build_config).group(0)
32
+ for name in ("CLIENT_VERSION_MAJOR", "CLIENT_VERSION_MINOR", "CLIENT_VERSION_RC")
33
+ )
34
+ (source / "CMakeLists.txt").write_text(
35
+ "cmake_minimum_required(VERSION 3.22)\n"
36
+ "project(ReleaseVersionTest LANGUAGES NONE)\n"
37
+ + metadata + "\n" + version_block
38
+ + f'configure_file("{ROOT / "cmake/bitcoin-build-config.h.in"}" '
39
+ '"${CMAKE_BINARY_DIR}/bitcoin-build-config.h" @ONLY)\n',
40
+ encoding="utf-8",
41
+ )
42
+ command = ["cmake", "-S", str(source), "-B", str(source / "build")]
43
+ if tag is not None:
44
+ command.append(f"-DCLIENT_VERSION_TAG={tag}")
45
+ result = subprocess.run(command, capture_output=True, text=True)
46
+ header = source / "build/bitcoin-build-config.h"
47
+ return result, header.read_text(encoding="utf-8") if header.exists() else ""
48
+
49
+ def test_release_tag_sets_compiled_version_without_git_metadata(self):
50
+ for tag in ("v31.1-roots.2", "v31.1-roots.4"):
51
+ with self.subTest(tag=tag):
52
+ result, header = self.configure_release_version(tag)
53
+ self.assertEqual(result.returncode, 0, result.stderr)
54
+ self.assertIn(f'#define CLIENT_VERSION_STRING "{tag[1:]}"', header)
55
+ self.assertNotIn("Manually-specified variables were not used", result.stderr)
56
+
57
+ def test_release_tag_rejects_wrong_base_and_invalid_roots_version(self):
58
+ for tag in ("v30.0-roots.4", "v31x1-roots.4", "v31.1-roots.0", "v31.1-knots.4", ""):
59
+ with self.subTest(tag=tag):
60
+ result, _ = self.configure_release_version(tag)
61
+ self.assertNotEqual(result.returncode, 0)
62
+ self.assertIn("CLIENT_VERSION_TAG must be a Roots release tag", result.stderr)
63
+
64
+ def test_untagged_build_keeps_default_roots_version(self):
65
+ result, header = self.configure_release_version()
66
+ self.assertEqual(result.returncode, 0, result.stderr)
67
+ self.assertIn('#define CLIENT_VERSION_STRING "31.1-roots.1"', header)
68
+
69
+ def test_version_and_release_tag_name_agree(self):
70
+ build_config = BUILD_CONFIG.read_text(encoding="utf-8")
71
+ self.assertIn("set(CLIENT_VERSION_MAJOR 31)", build_config)
72
+ self.assertIn("set(CLIENT_VERSION_MINOR 1)", build_config)
73
+ self.assertIn('string(APPEND CLIENT_VERSION_STRING "-roots.1")', build_config)
74
+ result = subprocess.run(
75
+ ["python3", ARCHIVE_TOOL, "root-name", "--tag", "v31.1-roots.1"],
76
+ capture_output=True, text=True, check=True,
77
+ )
78
+ self.assertEqual(result.stdout.strip(), "bitcoin-roots-31.1-roots.1")
79
+
80
+ def test_macos_deployment_verifies_the_branded_archive(self):
81
+ script = (ROOT / "ci/test/03_test_script.sh").read_text(encoding="utf-8")
82
+ start = script.index('if [[ "$CI_OS_NAME" == "macos"')
83
+ block = script[start:script.index('if [ "$RUN_UNIT_TESTS"', start)]
84
+ client_name = re.search(r'set\(CLIENT_NAME "([^"]+)"\)', BUILD_CONFIG.read_text()).group(1)
85
+ with tempfile.TemporaryDirectory(prefix="roots-macos-deployment-") as directory:
86
+ build = Path(directory)
87
+ with zipfile.ZipFile(build / "bitcoin-macos-app.zip", "w") as archive:
88
+ archive.writestr("Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt", "fixture")
89
+ tools = build / "tools"
90
+ tools.mkdir()
91
+ codesign = tools / "codesign"
92
+ codesign.write_text(
93
+ '#!/bin/sh\nprintf "%s\\n" "$*" > "$CODESIGN_LOG"\nexit "$CODESIGN_STATUS"\n',
94
+ encoding="utf-8",
95
+ )
96
+ codesign.chmod(0o755)
97
+ log = build / "codesign.log"
98
+ for status in (0, 1):
99
+ with self.subTest(codesign_status=status):
100
+ shutil.rmtree(build / "deploy", ignore_errors=True)
101
+ result = subprocess.run(
102
+ ["bash", "-ec", block],
103
+ env={**os.environ, "PATH": f"{tools}:{os.environ['PATH']}",
104
+ "BASE_BUILD_DIR": str(build), "CI_OS_NAME": "macos", "GOAL": "install deploy",
105
+ "CODESIGN_LOG": str(log), "CODESIGN_STATUS": str(status)},
106
+ capture_output=True, text=True,
107
+ )
108
+ self.assertEqual(result.returncode, status, result.stderr)
109
+ self.assertEqual(log.read_text().strip(), f"--verify {build}/deploy/Bitcoin-Qt.app")
110
+
111
+ def test_workflow_tags_build_signed_drafts_and_dispatch_only_rehearses(self):
112
+ workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
113
+ self.assertIn("workflow_dispatch:", workflow)
114
+ self.assertIn(" push:", workflow)
115
+ self.assertIn("RELEASE_TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.release_tag }}", workflow)
116
+ self.assertIn("ref: ${{ env.RELEASE_SOURCE_REF }}", workflow)
117
+ self.assertEqual(workflow.count("ci/release/prepare-release-source.sh"), 5)
118
+ publish = workflow.split(" publish-release:", 1)[1]
119
+ self.assertIn("if: github.event_name == 'push'", publish)
120
+ self.assertIn("contents: write", publish)
121
+ self.assertIn("REQUIRE_RELEASE_SIGNATURE: '1'", publish)
122
+ self.assertIn("test -s", publish)
123
+ self.assertNotIn("secrets.", workflow.split(" publish-release:", 1)[0])
124
+ self.assertNotIn("--clobber", workflow)
125
+ for metadata_workflow in (RELEASE_WORKFLOW, ROOT / ".github/workflows/ci.yml"):
126
+ metadata = metadata_workflow.read_text()
127
+ for test in ("test_release_notices.py", "test_create_release_draft.py",
128
+ "test_runner_release_notices.py", "test_create_ci_release_draft.py"):
129
+ self.assertIn("python3 ci/test/" + test, metadata)
130
+
131
+ def test_workflow_builds_complete_artifact_matrix_and_patch(self):
132
+ workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
133
+ for artifact in [
134
+ "bitcoin-roots-linux-x86_64",
135
+ "bitcoin-roots-linux-aarch64",
136
+ "bitcoin-roots-darwin-x86_64",
137
+ "bitcoin-roots-darwin-arm64",
138
+ "bitcoin-roots-windows-x86_64",
139
+ ]:
140
+ self.assertIn(artifact, workflow)
141
+ self.assertIn("ci/release/create-patch-series.sh", workflow)
142
+ self.assertIn("ci/release/create-ci-draft.py", workflow)
143
+
144
+ def test_workflow_references_only_present_local_release_files(self):
145
+ workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
146
+ local_paths = re.findall(r"(?:ci|contrib)/[A-Za-z0-9_./-]+\.(?:asc|py|sh)", workflow)
147
+ for local_path in local_paths:
148
+ with self.subTest(local_path=local_path):
149
+ self.assertTrue((ROOT / local_path).is_file())
150
+
151
+ def test_workflow_actions_are_intentionally_bounded(self):
152
+ workflow = RELEASE_WORKFLOW.read_text(encoding="utf-8")
153
+ actions = set(re.findall(r"uses: (\S+)", workflow))
154
+ self.assertEqual(
155
+ actions,
156
+ {
157
+ "actions/checkout@v6",
158
+ "actions/upload-artifact@v4",
159
+ "actions/download-artifact@v5",
160
+ "./.github/actions/configure-docker",
161
+ "./.github/actions/configure-environment",
162
+ },
163
+ )
164
+
165
+
166
+ if __name__ == "__main__":
167
+ unittest.main()