|
1 | 1 | #!/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 |
10 | 2 |
|
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 |
16 | 4 |
|
17 | | -TIME_FORMAT = '%Y-%m-%d-%H-%M-%S' |
| 5 | +from koji_utils.koji_build import main |
18 | 6 |
|
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() |
0 commit comments