draft builds

This commit is contained in:
Yuming Zhu 2023-09-22 21:57:30 +08:00 committed by Yu Ming Zhu
parent 124450cec0
commit 87409499a3
30 changed files with 1763 additions and 186 deletions

View file

@ -580,6 +580,8 @@ def handle_build(options, session, args):
parser.add_option("--custom-user-metadata", type="str",
help="Provide a JSON string of custom metadata to be deserialized and "
"stored under the build's extra.custom_user_metadata field")
parser.add_option("--draft", action="store_true",
help="Build draft build instead")
(build_opts, args) = parser.parse_args(args)
if len(args) != 2:
parser.error("Exactly two arguments (a build target and a SCM URL or srpm file) are "
@ -588,6 +590,8 @@ def handle_build(options, session, args):
parser.error("--no-/rebuild-srpm is only allowed for --scratch builds")
if build_opts.arch_override and not build_opts.scratch:
parser.error("--arch_override is only allowed for --scratch builds")
if build_opts.scratch and build_opts.draft:
parser.error("--scratch and --draft cannot be both specfied")
custom_user_metadata = {}
if build_opts.custom_user_metadata:
try:
@ -618,7 +622,7 @@ def handle_build(options, session, args):
if build_opts.arch_override:
opts['arch_override'] = koji.parse_arches(build_opts.arch_override)
for key in ('skip_tag', 'scratch', 'repo_id', 'fail_fast', 'wait_repo', 'wait_builds',
'rebuild_srpm'):
'rebuild_srpm', 'draft'):
val = getattr(build_opts, key)
if val is not None:
opts[key] = val
@ -830,6 +834,8 @@ def handle_wrapper_rpm(options, session, args):
parser.add_option("--nowait", action="store_false", dest="wait", help="Don't wait on build")
parser.add_option("--background", action="store_true",
help="Run the build at a lower priority")
parser.add_option("--draft", action="store_true",
help="Build draft build instead")
(build_opts, args) = parser.parse_args(args)
if build_opts.inis:
@ -839,6 +845,8 @@ def handle_wrapper_rpm(options, session, args):
if len(args) < 3:
parser.error("You must provide a build target, a build ID or NVR, "
"and a SCM URL to a specfile fragment")
if build_opts.scratch and build_opts.draft:
parser.error("--scratch and --draft cannot be both specfied")
activate_session(session, options)
target = args[0]
@ -874,6 +882,8 @@ def handle_wrapper_rpm(options, session, args):
opts['skip_tag'] = True
if build_opts.scratch:
opts['scratch'] = True
if build_opts.draft:
opts['draft'] = True
task_id = session.wrapperRPM(build_id, url, target, priority, opts=opts)
print("Created task: %d" % task_id)
print("Task info: %s/taskinfo?taskID=%s" % (options.weburl, task_id))
@ -1314,6 +1324,10 @@ def handle_import(goptions, session, args):
parser.add_option("--test", action="store_true", help="Don't actually import")
parser.add_option("--create-build", action="store_true", help="Auto-create builds as needed")
parser.add_option("--src-epoch", help="When auto-creating builds, use this epoch")
parser.add_option("--create-draft", action="store_true",
help="Auto-create draft builds instead as needed")
parser.add_option("--draft-build", metavar='NVR|ID',
help="The target draft build to import to")
(options, args) = parser.parse_args(args)
if len(args) < 1:
parser.error("At least one package must be specified")
@ -1324,6 +1338,35 @@ def handle_import(goptions, session, args):
options.src_epoch = int(options.src_epoch)
except (ValueError, TypeError):
parser.error("Invalid value for epoch: %s" % options.src_epoch)
if options.create_draft:
print("Will create draft build instead if desired nvr doesn't exist")
options.create_build = True
draft_build = None
draft_target_nvr = None
if options.draft_build:
if options.create_build:
parser.error(
"To ensure no misuse, don't specify --draft-build while auto-creating."
)
try:
draft_build = int(options.draft_build)
except ValueError:
draft_build = options.draft_build
draft_build = session.getBuild(draft_build)
if not draft_build:
error("No such build: %s" % options.draft_build)
if not draft_build.get('draft'):
error("%s is not a draft build" % draft_build['nvr'])
b_state = koji.BUILD_STATES[draft_build['state']]
if b_state != 'COMPLETE':
error("draft build %s is expected as COMPLETE, got %s" % (draft_build['nvr'], b_state))
target_release = draft_build.get('extra', {}).get('draft', {}).get('target_release')
if not target_release:
error("Invalid draft build: %s,"
" no draft.target_release found in extra" % draft_build['nvr'])
draft_target_nvr = "%s-%s-%s" % (
draft_build['name'], draft_build['version'], target_release
)
activate_session(session, goptions)
to_import = {}
for path in args:
@ -1335,6 +1378,7 @@ def handle_import(goptions, session, args):
else:
nvr = "%(name)s-%(version)s-%(release)s" % koji.parse_NVRA(data['sourcerpm'])
to_import.setdefault(nvr, []).append((path, data))
builds_missing = False
nvrs = sorted(to_list(to_import.keys()))
for nvr in nvrs:
@ -1343,29 +1387,41 @@ def handle_import(goptions, session, args):
if data['sourcepackage']:
break
else:
if nvr == draft_target_nvr:
continue
# when no srpm and create_draft
elif options.create_draft:
print("Missing srpm for draft build creating with target nvr: %s" % nvr)
builds_missing = True
continue
# no srpm included, check for build
binfo = session.getBuild(nvr)
if not binfo:
print("Missing build or srpm: %s" % nvr)
builds_missing = True
if builds_missing and not options.create_build:
if builds_missing and (not options.create_build or options.create_draft):
print("Aborting import")
return
# local function to help us out below
def do_import(path, data):
def do_import(path, data, draft_build=None):
draft = bool(draft_build) or options.create_draft
rinfo = dict([(k, data[k]) for k in ('name', 'version', 'release', 'arch')])
prev = session.getRPM(rinfo)
if prev and not prev.get('external_repo_id', 0):
if prev['payloadhash'] == koji.hex_string(data['sigmd5']):
print("RPM already imported: %s" % path)
else:
warn("md5sum mismatch for %s" % path)
warn(" A different rpm with the same name has already been imported")
warn(" Existing sigmd5 is %r, your import has %r" % (
prev['payloadhash'], koji.hex_string(data['sigmd5'])))
print("Skipping import")
return
if draft_build or not options.create_draft:
opts = {}
if draft_build:
opts['build'] = draft_build
prev = session.getRPM(rinfo, **opts)
if prev and not prev.get('external_repo_id', 0):
if prev['payloadhash'] == koji.hex_string(data['sigmd5']):
print("RPM already imported: %s" % path)
else:
warn("md5sum mismatch for %s" % path)
warn(" A different rpm with the same name has already been imported")
warn(" Existing sigmd5 is %r, your import has %r" % (
prev['payloadhash'], koji.hex_string(data['sigmd5'])))
print("Skipping import")
return
if options.test:
print("Test mode -- skipping import for %s" % path)
return
@ -1381,41 +1437,69 @@ def handle_import(goptions, session, args):
sys.stdout.write("importing %s... " % path)
sys.stdout.flush()
try:
session.importRPM(serverdir, os.path.basename(path))
opts = {}
if draft_build:
opts['build'] = draft_build
if draft:
opts['draft'] = draft
rpminfo = session.importRPM(serverdir, os.path.basename(path), **opts)
except koji.GenericError as e:
rpminfo = None
print("\nError importing: %s" % str(e).splitlines()[-1])
sys.stdout.flush()
else:
print("done")
sys.stdout.flush()
return rpminfo
for nvr in nvrs:
# check for existing build
need_build = True
binfo = session.getBuild(nvr)
if binfo:
b_state = koji.BUILD_STATES[binfo['state']]
if b_state == 'COMPLETE':
need_build = False
elif b_state in ['FAILED', 'CANCELED']:
if not options.create_build:
print("Build %s state is %s. Skipping import" % (nvr, b_state))
is_draft = True
if nvr == draft_target_nvr:
binfo = draft_build
need_build = False
elif options.create_draft:
binfo = None
need_build = True
else:
is_draft = False
binfo = session.getBuild(nvr)
if binfo:
b_state = koji.BUILD_STATES[binfo['state']]
if b_state == 'COMPLETE':
need_build = False
elif b_state in ['FAILED', 'CANCELED']:
if not options.create_build:
print("Build %s state is %s. Skipping import" % (nvr, b_state))
continue
else:
print("Build %s exists with state=%s. Skipping import" % (nvr, b_state))
continue
else:
print("Build %s exists with state=%s. Skipping import" % (nvr, b_state))
continue
# import srpms first, if any
for path, data in to_import[nvr]:
if data['sourcepackage']:
if binfo and b_state != 'COMPLETE':
# we can not fix state for draft build by createEmptyBuild
if not is_draft and binfo and b_state != 'COMPLETE':
# need to fix the state
print("Creating empty build: %s" % nvr)
b_data = koji.util.dslice(binfo, ['name', 'version', 'release'])
b_data['epoch'] = data['epoch']
session.createEmptyBuild(**b_data)
binfo = session.getBuild(nvr)
do_import(path, data)
dbld = binfo if is_draft else None
will_create = False
if options.create_draft and not dbld:
will_create = True
print("Will create draft build with target nvr: %s while importing" % nvr)
rpminfo = do_import(path, data, draft_build=dbld)
# only needed for draft build, but
# TODO: should be able to apply to regular import
if rpminfo and rpminfo.get('build', {}).get('draft'):
binfo = rpminfo['build']
if will_create:
print("Draft build: %s created" % binfo['nvr'])
need_build = False
if need_build:
@ -1424,11 +1508,11 @@ def handle_import(goptions, session, args):
if binfo:
# should have caught this earlier, but just in case...
b_state = koji.BUILD_STATES[binfo['state']]
print("Build %s state is %s. Skipping import" % (nvr, b_state))
print("Build %s state is %s. Skipping import" % (binfo['nvr'], b_state))
continue
else:
print("No such build: %s (include matching srpm or use "
"--create-build option to add it)" % nvr)
"--create-build/--create-draft option to add it)" % nvr)
continue
else:
# let's make a new build
@ -1439,17 +1523,22 @@ def handle_import(goptions, session, args):
# pull epoch from first rpm
data = to_import[nvr][0][1]
b_data['epoch'] = data['epoch']
if options.test:
print("Test mode -- would have created empty build: %s" % nvr)
if options.create_draft:
b_data['draft'] = True
b_display = "empty draft build with target nvr: %s" % nvr
else:
print("Creating empty build: %s" % nvr)
session.createEmptyBuild(**b_data)
binfo = session.getBuild(nvr)
b_display = "empty build: %s" % nvr
if options.test:
print("Test mode -- would have created %s" % b_display)
else:
print("Creating %s" % b_display)
buildid = session.createEmptyBuild(**b_data)
binfo = session.getBuild(buildid)
for path, data in to_import[nvr]:
if data['sourcepackage']:
continue
do_import(path, data)
do_import(path, data, draft_build=binfo if is_draft else None)
def handle_import_cg(goptions, session, args):
@ -2727,11 +2816,15 @@ def anon_handle_list_tagged(goptions, session, args):
parser.add_option("--ts", type='int', metavar="TIMESTAMP",
help="query at last event before timestamp")
parser.add_option("--repo", type='int', metavar="REPO#", help="query at event for a repo")
parser.add_option("--draft-only", action="store_true", help="Only list draft builds/rpms")
parser.add_option("--no-draft", action="store_true", help="Only list regular builds/rpms")
(options, args) = parser.parse_args(args)
if len(args) == 0:
parser.error("A tag name must be specified")
elif len(args) > 2:
parser.error("Only one package name may be specified")
if options.no_draft and options.draft_only:
parser.error("--draft-only conflicts with --no-draft")
ensure_connection(session, goptions)
pathinfo = koji.PathInfo()
package = None
@ -2753,6 +2846,10 @@ def anon_handle_list_tagged(goptions, session, args):
options.rpms = True
if options.type:
opts['type'] = options.type
elif options.no_draft:
opts['draft'] = koji.FLAG_REGULAR_BUILD
elif options.draft_only:
opts['draft'] = koji.FLAG_DRAFT_BUILD
event = koji.util.eventFromOpts(session, options)
event_id = None
if event:
@ -2798,7 +2895,9 @@ def anon_handle_list_tagged(goptions, session, args):
fmt = "%(path)s"
data = [x for x in data if 'path' in x]
else:
fmt = "%(name)s-%(version)s-%(release)s.%(arch)s"
fmt = "%(name)s-%(version)s-%(release)s.%(arch)s%(draft_suffix)s"
for x in data:
x['draft_suffix'] = (' (#draft_%s)' % x['build_id']) if x.get('draft') else ''
if options.sigs:
fmt = "%(sigkey)s " + fmt
else:
@ -2861,10 +2960,13 @@ def anon_handle_list_buildroot(goptions, session, args):
fmt = "%(nvr)s.%(arch)s"
order = sorted([(fmt % x, x) for x in list_rpms])
for nvra, rinfo in order:
if options.verbose and rinfo.get('is_update'):
print("%s [update]" % nvra)
else:
print(nvra)
line = nvra
if options.verbose:
if rinfo.get('draft'):
line += " (#draft_%s)" % rinfo['build_id']
if rinfo.get('is_update'):
line += " [update]"
print(line)
list_archives = session.listArchives(**opts)
if list_archives:
@ -3403,6 +3505,8 @@ def anon_handle_list_builds(goptions, session, args):
parser.add_option("--source", help="Only builds where the source field matches (glob pattern)")
parser.add_option("--owner", help="List builds built by this owner")
parser.add_option("--volume", help="List builds by volume ID")
parser.add_option("--draft-only", action="store_true", help="Only list draft builds")
parser.add_option("--no-draft", action="store_true", help="Only list regular builds")
parser.add_option("-k", "--sort-key", action="append", metavar='FIELD',
default=[], help="Sort the list by the named field. Allowed sort keys: "
"build_id, owner_name, state")
@ -3419,6 +3523,12 @@ def anon_handle_list_builds(goptions, session, args):
value = getattr(options, key)
if value is not None:
opts[key] = value
if options.no_draft and options.draft_only:
parser.error("--draft-only conflits with --no-draft")
elif options.no_draft:
opts['draft'] = koji.FLAG_REGULAR_BUILD
elif options.draft_only:
opts['draft'] = koji.FLAG_DRAFT_BUILD
if options.cg:
opts['cgID'] = options.cg
if options.package:
@ -3520,13 +3630,24 @@ def anon_handle_rpminfo(goptions, session, args):
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--buildroots", action="store_true",
help="show buildroots the rpm was used in")
parser.add_option("--build", metavar="NVR|ID",
help="show the rpm(s) in the build")
(options, args) = parser.parse_args(args)
if len(args) < 1:
parser.error("Please specify an RPM")
opts = {}
build = options.build
if options.build:
try:
build = int(build)
except ValueError:
pass
opts['build'] = build
ensure_connection(session, goptions)
error_hit = False
for rpm in args:
info = session.getRPM(rpm)
info = session.getRPM(rpm, **opts)
if info is None:
warn("No such rpm: %s\n" % rpm)
error_hit = True
@ -3536,24 +3657,29 @@ def anon_handle_rpminfo(goptions, session, args):
else:
info['epoch'] = str(info['epoch']) + ":"
if not info.get('external_repo_id', 0):
buildinfo = session.getBuild(info['build_id'])
buildinfo['name'] = buildinfo['package_name']
buildinfo['arch'] = 'src'
if buildinfo['epoch'] is None:
buildinfo['epoch'] = ""
if info['arch'] == 'src':
srpminfo = info.copy()
else:
buildinfo['epoch'] = str(buildinfo['epoch']) + ":"
srpminfo = session.listRPMs(buildID=info['build_id'], arches='src')[0]
if srpminfo['epoch'] is None:
srpminfo['epoch'] = ""
else:
srpminfo['epoch'] = str(srpminfo['epoch']) + ":"
buildinfo = session.getBuild(info['build_id'])
print("RPM: %(epoch)s%(name)s-%(version)s-%(release)s.%(arch)s [%(id)d]" % info)
if info.get('draft'):
print("Draft: YES")
if info.get('external_repo_id'):
repo = session.getExternalRepo(info['external_repo_id'])
print("External Repository: %(name)s [%(id)i]" % repo)
print("External Repository url: %(url)s" % repo)
else:
print("Build: %(nvr)s [%(id)d]" % buildinfo)
print("RPM Path: %s" %
os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(info)))
print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % buildinfo)
print("SRPM: %(epoch)s%(name)s-%(version)s-%(release)s [%(id)d]" % srpminfo)
print("SRPM Path: %s" %
os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(buildinfo)))
os.path.join(koji.pathinfo.build(buildinfo), koji.pathinfo.rpm(srpminfo)))
print("Built: %s" % time.strftime('%a, %d %b %Y %H:%M:%S %Z',
time.localtime(info['buildtime'])))
print("SIGMD5: %(payloadhash)s" % info)
@ -3563,6 +3689,7 @@ def anon_handle_rpminfo(goptions, session, args):
headers=["license"])
if 'license' in headers:
print("License: %(license)s" % headers)
# kept for backward compatibility
print("Build ID: %(build_id)s" % info)
if info['buildroot_id'] is None:
print("No buildroot data available")
@ -3619,6 +3746,8 @@ def anon_handle_buildinfo(goptions, session, args):
info['arch'] = 'src'
info['state'] = koji.BUILD_STATES[info['state']]
print("BUILD: %(name)s-%(version)s-%(release)s [%(id)d]" % info)
if info.get('draft'):
print("Draft: YES")
print("State: %(state)s" % info)
if info['state'] == 'BUILDING':
print("Reserved by: %(cg_name)s" % info)

View file

@ -925,3 +925,10 @@ class DatetimeJSONEncoder(json.JSONEncoder):
if isinstance(o, xmlrpc_client.DateTime):
return str(o)
return json.JSONEncoder.default(self, o)
def yesno(x):
if x:
return 'Y'
else:
return 'N'