v30.3-roots.1 · Commit 63: [PATCH 63/68] fix(release): restore signed five-platform CI releases
ci/test/test_create_ci_release_draft.py
Before: /dev/null · After: ci/test/test_create_ci_release_draft.py
File metadata
new file mode 100644 index 0000000000000000000000000000000000000000..53199a4d30a87d4a671a350fa9d0567c64a1e5b0
Download original patch · Release notes
Diff rows 1–105 of 105. Line numbers refer to the original before/after files.
+ Added− RemovedUnmarked lines are context.
Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.
ci/test/test_create_ci_release_draft.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import shutil
|
| 10 |
+
import subprocess
|
| 11 |
+
import tempfile
|
| 12 |
+
import unittest
|
| 13 |
+
|
| 14 |
+
import test_create_release_draft as fixtures
|
| 15 |
+
|
| 16 |
+
ROOT = fixtures.ROOT
|
| 17 |
+
RELEASE_TAG = fixtures.RELEASE_TAG
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@unittest.skipUnless(shutil.which('gpg'), 'gpg is required')
|
| 21 |
+
class CreateCIDraftTest(unittest.TestCase):
|
| 22 |
+
@classmethod
|
| 23 |
+
def setUpClass(cls):
|
| 24 |
+
cls.temporary = tempfile.TemporaryDirectory()
|
| 25 |
+
cls.key_home = Path(cls.temporary.name)
|
| 26 |
+
cls.key_home.chmod(0o700)
|
| 27 |
+
cls.gpg('--passphrase', '', '--quick-generate-key', 'CI Fixture <fixture@example.invalid>', 'ed25519', 'sign', '0')
|
| 28 |
+
cls.fingerprint = cls.gpg('--with-colons', '--list-keys').stdout.decode().split('fpr:::::::::', 1)[1].split(':', 1)[0]
|
| 29 |
+
cls.public = cls.gpg('--armor', '--export', cls.fingerprint).stdout
|
| 30 |
+
cls.secret = cls.gpg('--armor', '--export-secret-keys', cls.fingerprint).stdout.decode()
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def tearDownClass(cls):
|
| 34 |
+
cls.temporary.cleanup()
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def gpg(cls, *args):
|
| 38 |
+
return subprocess.run(['gpg', '--batch', '--homedir', cls.key_home, *args], check=True, capture_output=True)
|
| 39 |
+
|
| 40 |
+
def setUp(self):
|
| 41 |
+
self.fixture = fixtures.CreateReleaseDraftTest()
|
| 42 |
+
self.fixture.setUp()
|
| 43 |
+
self.addCleanup(self.fixture.doCleanups)
|
| 44 |
+
f = self.fixture
|
| 45 |
+
self.public_key = f.work / 'public.asc'
|
| 46 |
+
self.public_key.write_bytes(self.public)
|
| 47 |
+
subprocess.run([ROOT / 'ci/release/sign-manifest.sh', f.manifest, self.public_key, self.fingerprint],
|
| 48 |
+
env={**os.environ, 'BITCOIN_ROOTS_GPG_SK': self.secret, 'REQUIRE_RELEASE_SIGNATURE': '1'},
|
| 49 |
+
check=True, capture_output=True)
|
| 50 |
+
|
| 51 |
+
def invoke(self, **environment):
|
| 52 |
+
f = self.fixture
|
| 53 |
+
return subprocess.run(['python3', f.repository / 'ci/release/create-ci-draft.py', '--tag', RELEASE_TAG,
|
| 54 |
+
'--repository', 'owner/repository', '--assets', f.assets, '--notes', f.notes,
|
| 55 |
+
'--public-key', self.public_key, '--fingerprint', self.fingerprint, '--run-id', '123'],
|
| 56 |
+
cwd=f.repository, capture_output=True, text=True,
|
| 57 |
+
env={**os.environ, 'PATH': f'{f.tools}:{os.environ["PATH"]}',
|
| 58 |
+
'DRAFT_TEST_REMOTE': str(f.repository), 'DRAFT_TEST_RECORD': str(f.record), **environment})
|
| 59 |
+
|
| 60 |
+
def test_actual_signed_ci_draft_creates_exact_eight_assets_with_provenance(self):
|
| 61 |
+
result = self.invoke()
|
| 62 |
+
self.assertEqual(result.returncode, 0, result.stderr)
|
| 63 |
+
record = json.loads(self.fixture.record.read_text())
|
| 64 |
+
self.assertEqual(len(record['assets']), 8)
|
| 65 |
+
self.assertIn('SHA512SUMS.asc', record['assets'])
|
| 66 |
+
self.assertIn(self.fingerprint, record['notes'])
|
| 67 |
+
self.assertIn(self.fixture.commit, record['notes'])
|
| 68 |
+
self.assertIn('/actions/runs/123', record['notes'])
|
| 69 |
+
self.assertIn('--draft', record['args'])
|
| 70 |
+
self.assertNotIn('--clobber', record['args'])
|
| 71 |
+
|
| 72 |
+
def test_rejects_existing_draft_or_public_release_without_write(self):
|
| 73 |
+
for is_draft in (True, False):
|
| 74 |
+
with self.subTest(draft=is_draft):
|
| 75 |
+
result = self.invoke(DRAFT_TEST_RELEASES=json.dumps([[{'tag_name': RELEASE_TAG, 'draft': is_draft}]]))
|
| 76 |
+
self.assertNotEqual(result.returncode, 0)
|
| 77 |
+
self.assertIn('already exists', result.stderr)
|
| 78 |
+
self.assertFalse(self.fixture.record.exists())
|
| 79 |
+
|
| 80 |
+
def test_rejects_missing_or_invalid_signature_and_extra_assets(self):
|
| 81 |
+
signature = self.fixture.assets / 'SHA512SUMS.asc'
|
| 82 |
+
original = signature.read_bytes()
|
| 83 |
+
signature.unlink()
|
| 84 |
+
self.assertNotEqual(self.invoke().returncode, 0)
|
| 85 |
+
signature.write_bytes(b'Not a signature\n')
|
| 86 |
+
self.assertNotEqual(self.invoke().returncode, 0)
|
| 87 |
+
signature.write_bytes(original)
|
| 88 |
+
(self.fixture.assets / 'unexpected').write_text('extra')
|
| 89 |
+
self.assertNotEqual(self.invoke().returncode, 0)
|
| 90 |
+
self.assertFalse(self.fixture.record.exists())
|
| 91 |
+
|
| 92 |
+
def test_required_signing_rejects_missing_secret_and_wrong_public_fingerprint(self):
|
| 93 |
+
environment = {k: v for k, v in os.environ.items() if k != 'BITCOIN_ROOTS_GPG_SK'}
|
| 94 |
+
environment['REQUIRE_RELEASE_SIGNATURE'] = '1'
|
| 95 |
+
command = [ROOT / 'ci/release/sign-manifest.sh', self.fixture.manifest, self.public_key, self.fingerprint]
|
| 96 |
+
result = subprocess.run(command, env=environment, capture_output=True, text=True)
|
| 97 |
+
self.assertNotEqual(result.returncode, 0)
|
| 98 |
+
self.assertIn('Required release signing key', result.stderr)
|
| 99 |
+
command[-1] = '0' * 40
|
| 100 |
+
result = subprocess.run(command, env={**environment, 'BITCOIN_ROOTS_GPG_SK': self.secret}, capture_output=True)
|
| 101 |
+
self.assertNotEqual(result.returncode, 0)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == '__main__':
|
| 105 |
+
unittest.main()
|