Skip to content

Commit 9a30b4e

Browse files
committed
declare the koji-build script
Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent 323421c commit 9a30b4e

File tree

5 files changed

+220
-201
lines changed

5 files changed

+220
-201
lines changed

scripts/koji/koji_build.py

Lines changed: 5 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,204 +1,9 @@
11
#!/usr/bin/env python3
2-
import argparse
3-
import logging
4-
import os
5-
import re
6-
import subprocess
7-
from contextlib import contextmanager
8-
from datetime import datetime, timedelta
9-
from pathlib import Path
102

11-
try:
12-
from specfile import Specfile
13-
except ImportError:
14-
print("error: specfile module can't be imported. Please install it with 'pip install --user specfile'.")
15-
exit(1)
3+
import sys
164

17-
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
5+
from koji_utils.koji_build import main
186

19-
@contextmanager
20-
def cd(dir):
21-
"""Change to a directory temporarily. To be used in a with statement."""
22-
prevdir = os.getcwd()
23-
os.chdir(dir)
24-
try:
25-
yield os.path.realpath(dir)
26-
finally:
27-
os.chdir(prevdir)
28-
29-
def check_dir(dirpath):
30-
if not os.path.isdir(dirpath):
31-
raise Exception("Directory %s doesn't exist" % dirpath)
32-
return dirpath
33-
34-
def check_git_repo(dirpath):
35-
"""check that the working copy is a working directory and is clean."""
36-
with cd(dirpath):
37-
return subprocess.run(['git', 'diff-index', '--quiet', 'HEAD', '--']).returncode == 0
38-
39-
def check_commit_is_available_remotely(dirpath, hash):
40-
with cd(dirpath):
41-
if not subprocess.check_output(['git', 'branch', '-r', '--contains', hash]):
42-
raise Exception("The current commit is not available in the remote repository")
43-
44-
def get_repo_and_commit_info(dirpath):
45-
with cd(dirpath):
46-
remote = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url']).decode().strip()
47-
# We want the exact hash for accurate build history
48-
hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()
49-
return remote, hash
50-
51-
def koji_url(remote, hash):
52-
if remote.startswith('git@'):
53-
remote = re.sub(r'git@(.+):', r'git+https://\1/', remote)
54-
elif remote.startswith('https://'):
55-
remote = 'git+' + remote
56-
else:
57-
raise Exception("Unrecognized remote URL")
58-
return remote + "?#" + hash
59-
60-
@contextmanager
61-
def local_branch(branch):
62-
prev_branch = subprocess.check_output(['git', 'branch', '--show-current']).strip()
63-
commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()
64-
subprocess.check_call(['git', 'checkout', '--quiet', commit])
65-
try:
66-
yield branch
67-
finally:
68-
subprocess.check_call(['git', 'checkout', prev_branch])
69-
70-
def is_old_branch(b):
71-
branch_time = datetime.strptime(b.split('/')[-1], TIME_FORMAT)
72-
return branch_time < datetime.now() - timedelta(hours=3)
73-
74-
def clean_old_branches(git_repo):
75-
with cd(git_repo):
76-
remote_branches = [
77-
line.split()[-1] for line in subprocess.check_output(['git', 'ls-remote']).decode().splitlines()
78-
]
79-
remote_branches = [b for b in remote_branches if b.startswith('refs/heads/koji/test/')]
80-
old_branches = [b for b in remote_branches if is_old_branch(b)]
81-
if old_branches:
82-
print("removing outdated remote branch(es)", flush=True)
83-
subprocess.check_call(['git', 'push', '--delete', 'origin'] + old_branches)
84-
85-
def xcpng_version(target):
86-
xcpng_version_match = re.match(r'^v(\d+\.\d+)-u-\S+$', target)
87-
if xcpng_version_match is None:
88-
raise Exception(f"Can't find XCP-ng version in {target}")
89-
return xcpng_version_match.group(1)
90-
91-
def find_next_release(package, spec, target, test_build_id, pre_build_id):
92-
assert test_build_id is not None or pre_build_id is not None
93-
builds = subprocess.check_output(['koji', 'list-builds', '--quiet', '--package', package]).decode().splitlines()
94-
if test_build_id:
95-
base_nvr = f'{package}-{spec.version}-{spec.release}.0.{test_build_id}.'
96-
else:
97-
base_nvr = f'{package}-{spec.version}-{spec.release}~{pre_build_id}.'
98-
# use a regex to match %{macro} without actually expanding the macros
99-
base_nvr_re = (
100-
re.escape(re.sub('%{.+}', "@@@", base_nvr)).replace('@@@', '.*')
101-
+ r'(\d+)'
102-
+ re.escape(f'.xcpng{xcpng_version(target)}')
103-
)
104-
build_matches = [re.match(base_nvr_re, b) for b in builds]
105-
build_nbs = [int(m.group(1)) for m in build_matches if m]
106-
build_nb = sorted(build_nbs)[-1] + 1 if build_nbs else 1
107-
if test_build_id:
108-
return f'{spec.release}.0.{test_build_id}.{build_nb}'
109-
else:
110-
return f'{spec.release}~{pre_build_id}.{build_nb}'
111-
112-
def push_bumped_release(git_repo, target, test_build_id, pre_build_id):
113-
t = datetime.now().strftime(TIME_FORMAT)
114-
branch = f'koji/test/{test_build_id or pre_build_id}/{t}'
115-
with cd(git_repo), local_branch(branch):
116-
spec_paths = subprocess.check_output(['git', 'ls-files', 'SPECS/*.spec']).decode().splitlines()
117-
assert len(spec_paths) == 1
118-
spec_path = spec_paths[0]
119-
with Specfile(spec_path) as spec:
120-
# find the next build number
121-
package = Path(spec_path).stem
122-
spec.release = find_next_release(package, spec, target, test_build_id, pre_build_id)
123-
subprocess.check_call(['git', 'commit', '--quiet', '-m', "bump release for test build", spec_path])
124-
subprocess.check_call(['git', 'push', 'origin', f'HEAD:refs/heads/{branch}'])
125-
commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()
126-
return commit
127-
128-
def main():
129-
parser = argparse.ArgumentParser(
130-
description='Build a package or chain-build several from local git repos for RPM sources'
131-
)
132-
parser.add_argument('target', help='Koji target for the build')
133-
parser.add_argument('git_repos', nargs='+',
134-
help='local path to one or more git repositories. If several are provided, '
135-
'a chained build will be started in the order of the arguments')
136-
parser.add_argument('--scratch', action="store_true", help='Perform scratch build')
137-
parser.add_argument('--nowait', action="store_true", help='Do not wait for the build to end')
138-
parser.add_argument(
139-
'--test-build',
140-
metavar="ID",
141-
help='Run a test build. The provided ID will be used to build a unique release tag.',
142-
)
143-
parser.add_argument(
144-
'--pre-build',
145-
metavar="ID",
146-
help='Run a pre build. The provided ID will be used to build a unique release tag.',
147-
)
148-
args = parser.parse_args()
149-
150-
target = args.target
151-
git_repos = [os.path.abspath(check_dir(d)) for d in args.git_repos]
152-
is_scratch = args.scratch
153-
is_nowait = args.nowait
154-
test_build = args.test_build
155-
pre_build = args.pre_build
156-
if test_build and pre_build:
157-
logging.error("--pre-build and --test-build can't be used together")
158-
exit(1)
159-
if test_build is not None and re.match('^[a-zA-Z0-9]{1,16}$', test_build) is None:
160-
logging.error("The test build id must be 16 characters long maximum and only contain letters and digits")
161-
exit(1)
162-
if pre_build is not None and re.match('^[a-zA-Z0-9]{1,16}$', pre_build) is None:
163-
logging.error("The pre build id must be 16 characters long maximum and only contain letters and digits")
164-
exit(1)
165-
166-
if len(git_repos) > 1 and is_scratch:
167-
parser.error("--scratch is not compatible with chained builds.")
168-
169-
for d in git_repos:
170-
if not check_git_repo(d):
171-
parser.error("%s is not in a clean state (or is not a git repository)." % d)
172-
173-
if len(git_repos) == 1:
174-
clean_old_branches(git_repos[0])
175-
remote, hash = get_repo_and_commit_info(git_repos[0])
176-
if test_build or pre_build:
177-
hash = push_bumped_release(git_repos[0], target, test_build, pre_build)
178-
else:
179-
check_commit_is_available_remotely(git_repos[0], hash)
180-
url = koji_url(remote, hash)
181-
command = (
182-
['koji', 'build']
183-
+ (['--scratch'] if is_scratch else [])
184-
+ [target, url]
185-
+ (['--nowait'] if is_nowait else [])
186-
)
187-
print(' '.join(command), flush=True)
188-
subprocess.check_call(command)
189-
else:
190-
urls = []
191-
for d in git_repos:
192-
clean_old_branches(d)
193-
remote, hash = get_repo_and_commit_info(d)
194-
if test_build or pre_build:
195-
hash = push_bumped_release(d, target, test_build, pre_build)
196-
else:
197-
check_commit_is_available_remotely(d, hash)
198-
urls.append(koji_url(remote, hash))
199-
command = ['koji', 'chain-build', target] + (' : '.join(urls)).split(' ') + (['--nowait'] if is_nowait else [])
200-
print(' '.join(command), flush=True)
201-
subprocess.check_call(command)
202-
203-
if __name__ == "__main__":
204-
main()
7+
print("\033[33mwarning: koji_build.py as moved to koji_utils/koji_build.py. "
8+
"Please update your configuration to use that file.\033[0m", file=sys.stderr)
9+
main()

scripts/koji/koji_utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)