Skip to content

Commit f07c00b

Browse files
committed
feat userver: upload deb src to PPA
commit_hash:59a53a0a0f8334d413fe5e33b3cd173807f93306
1 parent 33689c1 commit f07c00b

31 files changed

Lines changed: 546 additions & 17 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ scripts/docs/en/dynamic_configs
2727
scripts/docs/en/versions.md
2828
scripts/docs/scripts/versions.js
2929
universal/include/userver/version.hpp
30+
/debian/
31+
/.opencode/
32+
/.claude/

.mapping.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4850,6 +4850,7 @@
48504850
"samples/ydb_topic_writer_service/testsuite/test_topic_writer.py":"taxi/uservices/userver/samples/ydb_topic_writer_service/testsuite/test_topic_writer.py",
48514851
"scripts/README.md":"taxi/uservices/userver/scripts/README.md",
48524852
"scripts/add-missing-include.sh":"taxi/uservices/userver/scripts/add-missing-include.sh",
4853+
"scripts/build-and-upload-ppa/__main__.py":"taxi/uservices/userver/scripts/build-and-upload-ppa/__main__.py",
48534854
"scripts/build_and_install.sh":"taxi/uservices/userver/scripts/build_and_install.sh",
48544855
"scripts/build_and_install_all.sh":"taxi/uservices/userver/scripts/build_and_install_all.sh",
48554856
"scripts/changelog_tool/AGENTS.md":"taxi/uservices/userver/scripts/changelog_tool/AGENTS.md",
@@ -4884,8 +4885,10 @@
48844885
"scripts/chaotic/merge_yaml_configs/main.py":"taxi/uservices/userver/scripts/chaotic/merge_yaml_configs/main.py",
48854886
"scripts/chaotic/openapi_operations.py":"taxi/uservices/userver/scripts/chaotic/openapi_operations.py",
48864887
"scripts/chaotic/requirements.txt":"taxi/uservices/userver/scripts/chaotic/requirements.txt",
4888+
"scripts/check-cpack-components/__main__.py":"taxi/uservices/userver/scripts/check-cpack-components/__main__.py",
48874889
"scripts/clickhouse/ubuntu-install-clickhouse.sh":"taxi/uservices/userver/scripts/clickhouse/ubuntu-install-clickhouse.sh",
48884890
"scripts/compile_fail_test.py":"taxi/uservices/userver/scripts/compile_fail_test.py",
4891+
"scripts/debian-copyright":"taxi/uservices/userver/scripts/debian-copyright",
48894892
"scripts/debian-rules":"taxi/uservices/userver/scripts/debian-rules",
48904893
"scripts/docker/Readme.md":"taxi/uservices/userver/scripts/docker/Readme.md",
48914894
"scripts/docker/base-debian-11.dockerfile":"taxi/uservices/userver/scripts/docker/base-debian-11.dockerfile",
@@ -4927,29 +4930,43 @@
49274930
"scripts/docs/en/deps/macos.md":"taxi/uservices/userver/scripts/docs/en/deps/macos.md",
49284931
"scripts/docs/en/deps/ubuntu-22.04.md":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04.md",
49294932
"scripts/docs/en/deps/ubuntu-22.04/chaotic":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/chaotic",
4933+
"scripts/docs/en/deps/ubuntu-22.04/chaotic-openapi":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/chaotic-openapi",
49304934
"scripts/docs/en/deps/ubuntu-22.04/core":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/core",
4935+
"scripts/docs/en/deps/ubuntu-22.04/easy":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/easy",
4936+
"scripts/docs/en/deps/ubuntu-22.04/examples":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/examples",
49314937
"scripts/docs/en/deps/ubuntu-22.04/grpc":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/grpc",
4938+
"scripts/docs/en/deps/ubuntu-22.04/grpc-reflection":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/grpc-reflection",
49324939
"scripts/docs/en/deps/ubuntu-22.04/kafka":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/kafka",
49334940
"scripts/docs/en/deps/ubuntu-22.04/mongo":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/mongo",
4941+
"scripts/docs/en/deps/ubuntu-22.04/multi-index-lru":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/multi-index-lru",
49344942
"scripts/docs/en/deps/ubuntu-22.04/mysql":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/mysql",
49354943
"scripts/docs/en/deps/ubuntu-22.04/odbc":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/odbc",
4944+
"scripts/docs/en/deps/ubuntu-22.04/otlp":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/otlp",
49364945
"scripts/docs/en/deps/ubuntu-22.04/postgresql":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/postgresql",
49374946
"scripts/docs/en/deps/ubuntu-22.04/redis":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/redis",
49384947
"scripts/docs/en/deps/ubuntu-22.04/rocks":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/rocks",
4948+
"scripts/docs/en/deps/ubuntu-22.04/s3api":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/s3api",
49394949
"scripts/docs/en/deps/ubuntu-22.04/sqlite":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/sqlite",
49404950
"scripts/docs/en/deps/ubuntu-22.04/universal":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/universal",
49414951
"scripts/docs/en/deps/ubuntu-22.04/ydb":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-22.04/ydb",
49424952
"scripts/docs/en/deps/ubuntu-24.04.md":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04.md",
49434953
"scripts/docs/en/deps/ubuntu-24.04/chaotic":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/chaotic",
4954+
"scripts/docs/en/deps/ubuntu-24.04/chaotic-openapi":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/chaotic-openapi",
49444955
"scripts/docs/en/deps/ubuntu-24.04/core":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/core",
4956+
"scripts/docs/en/deps/ubuntu-24.04/easy":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/easy",
4957+
"scripts/docs/en/deps/ubuntu-24.04/examples":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/examples",
49454958
"scripts/docs/en/deps/ubuntu-24.04/grpc":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/grpc",
4959+
"scripts/docs/en/deps/ubuntu-24.04/grpc-reflection":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/grpc-reflection",
49464960
"scripts/docs/en/deps/ubuntu-24.04/kafka":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/kafka",
49474961
"scripts/docs/en/deps/ubuntu-24.04/mongo":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/mongo",
4962+
"scripts/docs/en/deps/ubuntu-24.04/multi-index-lru":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/multi-index-lru",
49484963
"scripts/docs/en/deps/ubuntu-24.04/mysql":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/mysql",
49494964
"scripts/docs/en/deps/ubuntu-24.04/odbc":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/odbc",
4965+
"scripts/docs/en/deps/ubuntu-24.04/otlp":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/otlp",
49504966
"scripts/docs/en/deps/ubuntu-24.04/postgresql":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/postgresql",
49514967
"scripts/docs/en/deps/ubuntu-24.04/redis":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/redis",
49524968
"scripts/docs/en/deps/ubuntu-24.04/rocks":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/rocks",
4969+
"scripts/docs/en/deps/ubuntu-24.04/s3api":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/s3api",
49534970
"scripts/docs/en/deps/ubuntu-24.04/sqlite":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/sqlite",
49544971
"scripts/docs/en/deps/ubuntu-24.04/universal":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/universal",
49554972
"scripts/docs/en/deps/ubuntu-24.04/ydb":"taxi/uservices/userver/scripts/docs/en/deps/ubuntu-24.04/ydb",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python3
2+
"""Build and upload userver source package to Launchpad PPA.
3+
4+
Orchestrates:
5+
1. scripts/generate-debian-directory.sh (generates debian/ + vendors pydantic wheels)
6+
2. debuild -S -sa (builds signed source package)
7+
3. dput ppa:userver-framework/userver (uploads to Launchpad)
8+
9+
Usage:
10+
scripts/build-and-upload-ppa --distro ubuntu-24.04 --version-kind nightly
11+
scripts/build-and-upload-ppa --distro ubuntu-22.04 --version-kind release
12+
"""
13+
14+
import argparse
15+
import datetime
16+
import os
17+
import pathlib
18+
import re
19+
import subprocess
20+
import sys
21+
22+
PPA = 'ppa:userver-framework/userver'
23+
24+
SUPPORTED_DISTROS = ['ubuntu-22.04', 'ubuntu-24.04']
25+
26+
27+
def find_repo_root() -> pathlib.Path:
28+
root = pathlib.Path(__file__).resolve().parent.parent.parent
29+
missing = [p for p in ['version.txt', 'scripts/generate-debian-directory.sh'] if not (root / p).exists()]
30+
if missing:
31+
sys.exit(f'ERROR: repo root {root} is missing expected files: ' + ', '.join(missing))
32+
return root
33+
34+
35+
def compute_version(root: pathlib.Path, version_kind: str) -> str:
36+
base = (root / 'version.txt').read_text().strip()
37+
if version_kind == 'release':
38+
return base
39+
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%d%H%M')
40+
return f'{base}~{timestamp}'
41+
42+
43+
def discover_signing_key() -> tuple[str, str, str]:
44+
"""Return (keyid, debfullname, debemail) from the GPG secret keyring.
45+
46+
Fails if there are 0 or >1 signing-capable secret keys.
47+
"""
48+
result = subprocess.run(
49+
['gpg', '--list-secret-keys', '--with-colons'],
50+
capture_output=True,
51+
text=True,
52+
check=True,
53+
)
54+
55+
# Collect (keyid, uid_string) pairs for signing-capable keys.
56+
# Colon format: field 1=type, field 5=keyid, field 12=capabilities.
57+
# Capabilities field contains 's' for signing.
58+
# uid lines immediately following a sec block carry field 10 = "Name <email>".
59+
keys: list[tuple[str, str]] = []
60+
current_keyid: str | None = None
61+
62+
for line in result.stdout.splitlines():
63+
fields = line.split(':')
64+
record_type = fields[0]
65+
66+
if record_type == 'sec':
67+
caps = fields[11] if len(fields) > 11 else ''
68+
if 's' in caps:
69+
current_keyid = fields[4]
70+
else:
71+
current_keyid = None
72+
73+
elif record_type == 'uid' and current_keyid is not None:
74+
uid_string = fields[9] if len(fields) > 9 else ''
75+
if uid_string:
76+
keys.append((current_keyid, uid_string))
77+
current_keyid = None # take only the first uid per key
78+
79+
if not keys:
80+
sys.exit('ERROR: no signing-capable GPG secret key found.\nGenerate one with: gpg --gen-key')
81+
82+
if len(keys) > 1:
83+
hint = '\n'.join(f' {keyid} {uid}' for keyid, uid in keys)
84+
sys.exit(
85+
'ERROR: multiple signing-capable GPG secret keys found;\n'
86+
'key selection is not parameterized — please leave exactly one:\n' + hint
87+
)
88+
89+
keyid, uid_string = keys[0]
90+
91+
# Parse "Full Name <email@example.com>"
92+
match = re.match(r'^(.*?)\s*<([^>]+)>$', uid_string)
93+
if not match:
94+
sys.exit(
95+
f'ERROR: GPG key UID {uid_string!r} is not in "Name <email>" format.\n'
96+
'Update the key UID or set DEBEMAIL/DEBFULLNAME manually.'
97+
)
98+
99+
debfullname = match.group(1).strip()
100+
debemail = match.group(2).strip()
101+
return keyid, debfullname, debemail
102+
103+
104+
def build_env(
105+
base: dict[str, str],
106+
distro: str,
107+
version: str,
108+
debfullname: str,
109+
debemail: str,
110+
) -> dict[str, str]:
111+
env = base.copy()
112+
env['DISTRO'] = distro
113+
env['VERSION'] = version
114+
env['DEBFULLNAME'] = debfullname
115+
env['DEBEMAIL'] = debemail
116+
117+
# GPG_TTY is required for gpg-agent to prompt for the passphrase.
118+
if 'GPG_TTY' not in env:
119+
try:
120+
env['GPG_TTY'] = os.ttyname(sys.stdin.fileno())
121+
except OSError:
122+
pass # not a tty; gpg may still work via agent/pinentry
123+
124+
return env
125+
126+
127+
def run(description: str, cmd: list[str], **kwargs) -> None:
128+
print(f'\n=== {description} ===', flush=True)
129+
print(' '.join(cmd), flush=True)
130+
try:
131+
subprocess.run(cmd, check=True, **kwargs)
132+
except subprocess.CalledProcessError as exc:
133+
sys.exit(f'ERROR: {description} failed with exit code {exc.returncode}')
134+
135+
136+
def main() -> None:
137+
parser = argparse.ArgumentParser(
138+
description='Build and upload userver source package to Launchpad PPA.',
139+
)
140+
parser.add_argument(
141+
'--distro',
142+
required=True,
143+
choices=SUPPORTED_DISTROS,
144+
help='Target Ubuntu distro (e.g. ubuntu-24.04).',
145+
)
146+
parser.add_argument(
147+
'--version-kind',
148+
required=True,
149+
choices=['release', 'nightly'],
150+
help=('release: use version.txt as-is; nightly: append ~<UTC timestamp> to version.txt.'),
151+
)
152+
args = parser.parse_args()
153+
154+
root = find_repo_root()
155+
os.chdir(root)
156+
157+
version = compute_version(root, args.version_kind)
158+
keyid, debfullname, debemail = discover_signing_key()
159+
160+
print(f'VERSION: {version}')
161+
print(f'DISTRO: {args.distro}')
162+
print(f'SIGNING KEY: {keyid}')
163+
print(f'DEBFULLNAME: {debfullname}')
164+
print(f'DEBEMAIL: {debemail}')
165+
print(f'PPA: {PPA}')
166+
167+
env = build_env(
168+
os.environ.copy(),
169+
distro=args.distro,
170+
version=version,
171+
debfullname=debfullname,
172+
debemail=debemail,
173+
)
174+
175+
# Step 1: generate debian/ and vendor pydantic wheels
176+
run(
177+
'Generating debian directory',
178+
['scripts/generate-debian-directory.sh'],
179+
env=env,
180+
)
181+
182+
# Step 2: build signed source package
183+
run(
184+
'Building source package',
185+
['debuild', '-S', '-sa', f'-k{keyid}'],
186+
env=env,
187+
)
188+
189+
# Step 3: upload
190+
changes_file = root.parent / f'userver_{version}_source.changes'
191+
if not changes_file.exists():
192+
sys.exit(f'ERROR: expected .changes file not found: {changes_file}\nCheck debuild output above.')
193+
194+
run(
195+
'Uploading to Launchpad PPA',
196+
['dput', PPA, str(changes_file)],
197+
env=env,
198+
)
199+
200+
print()
201+
print('Upload complete.')
202+
print(f' VERSION: {version}')
203+
print(f' changes: {changes_file}')
204+
print()
205+
print(
206+
'NOTE: "Successfully uploaded" = FTP transfer only.\n'
207+
'Authoritative result is the Launchpad acceptance email\n'
208+
'(OpenPGP-encrypted; decrypt with: GPG_TTY=$(tty) gpg --decrypt).'
209+
)
210+
211+
212+
if __name__ == '__main__':
213+
main()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
"""Guard: verify consistency between debian/*.cpack-components and cpack-metadata.json.
3+
4+
Two checks are performed:
5+
6+
Forward check: every component declared in a *.cpack-components file must
7+
actually exist in cpack-metadata.json (i.e. was built). A packaged-but-not-
8+
built component causes dh_cpack_substvars to fail with "Invalid CPack
9+
components in <pkg>".
10+
11+
Reverse check: every component in cpack-metadata.json must be claimed by at
12+
least one *.cpack-components file. An unclaimed built component leaves files
13+
in debian/tmp that no package owns, causing dh_missing to abort (compat 13).
14+
15+
Usage:
16+
check-cpack-components <cpack-metadata.json> <debian-dir>
17+
18+
Forward fix: add the component name to DISABLED_MODULES in
19+
scripts/generate-debian-directory.sh (or disable its cmake feature flag in
20+
scripts/debian-rules) and re-run generate-debian-directory.sh.
21+
22+
Reverse fix: add a deps/<DISTRO>/<component> file (may be empty) so the
23+
component is picked up by ALL_MODULES in generate-debian-directory.sh, then
24+
re-run generate-debian-directory.sh. Special-cased components with non-dev
25+
package names (like 'examples') need explicit handling in the generator.
26+
"""
27+
28+
import json
29+
import pathlib
30+
import sys
31+
32+
33+
def main() -> int:
34+
if len(sys.argv) != 3:
35+
print(f'Usage: {sys.argv[0]} <cpack-metadata.json> <debian-dir>', file=sys.stderr)
36+
return 2
37+
38+
metadata_path = pathlib.Path(sys.argv[1])
39+
debian_dir = pathlib.Path(sys.argv[2])
40+
41+
meta = json.loads(metadata_path.read_text())
42+
built_components = set(meta['components'].keys())
43+
44+
# Collect all components claimed by any package.
45+
claimed_components: set[str] = set()
46+
errors: list[str] = []
47+
48+
for comp_file in sorted(debian_dir.glob('*.cpack-components')):
49+
package = comp_file.name.replace('.cpack-components', '')
50+
for line in comp_file.read_text().splitlines():
51+
component = line.strip()
52+
if not component or component.startswith('#'):
53+
continue
54+
claimed_components.add(component)
55+
# Forward check: packaged component must have been built.
56+
if component not in built_components:
57+
errors.append(
58+
f"Package '{package}' declares CPack component '{component}'"
59+
f' but it was not built.\n'
60+
f' Built components: {sorted(built_components)}\n'
61+
f" Fix: add '{component}' to DISABLED_MODULES in"
62+
f' scripts/generate-debian-directory.sh and re-run'
63+
f' generate-debian-directory.sh.'
64+
)
65+
66+
# Reverse check: every built component must be claimed by some package.
67+
orphaned = sorted(built_components - claimed_components)
68+
for component in orphaned:
69+
errors.append(
70+
f"CPack component '{component}' was built but is not claimed by any"
71+
f' debian package (no *.cpack-components file lists it).\n'
72+
f' This will cause dh_missing to abort (compat 13).\n'
73+
f' Fix: add a deps/<DISTRO>/{component} file (may be empty) so'
74+
f' generate-debian-directory.sh packages it, then re-run'
75+
f' generate-debian-directory.sh.'
76+
)
77+
78+
if errors:
79+
for error in errors:
80+
print(f'ERROR: {error}', file=sys.stderr)
81+
return 1
82+
83+
n_files = len(list(debian_dir.glob('*.cpack-components')))
84+
print(
85+
f'check-cpack-components: all {n_files} package(s) valid;'
86+
f' all {len(built_components)} built component(s) claimed.'
87+
)
88+
return 0
89+
90+
91+
if __name__ == '__main__':
92+
sys.exit(main())

0 commit comments

Comments
 (0)