Skip to content

Commit 7427a6a

Browse files
committed
declare the koji-build script
Signed-off-by: Gaëtan Lehmann <[email protected]>
1 parent a6b5699 commit 7427a6a

File tree

6 files changed

+256
-234
lines changed

6 files changed

+256
-234
lines changed

scripts/koji/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
__pycache__
2+
3+
# Distribution / packaging
4+
dist/
5+
*.egg-info/

scripts/koji/koji_build.py

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