Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85803130
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
28 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lilybuild/lilybuild/artifacts/resource.py b/lilybuild/lilybuild/artifacts/resource.py
index a8f3fc4..ea4abdf 100644
--- a/lilybuild/lilybuild/artifacts/resource.py
+++ b/lilybuild/lilybuild/artifacts/resource.py
@@ -1,85 +1,151 @@
import json
from klein import Klein
from twisted.internet import defer
from twisted.web.static import File
from buildbot.data.resultspec import ResultSpec, Property
import os
+import re
POSSIBLE_ARTIFACT_TYPES = {
'archive': 'artifacts.tar',
'reports': 'reports.tar',
}
BAD_REQUEST = 'BAD_REQUEST'
NOT_FOUND = 'NOT_FOUND'
def validate_artifact_type(at):
return at in POSSIBLE_ARTIFACT_TYPES
+REF_NAME_REGEX = re.compile(r'^[A-Za-z0-9_\-]+$')
+def validate_ref_name(ref_name):
+ return re.match(REF_NAME_REGEX, ref_name)
+
class Api:
app = Klein()
def __init__(self, ep):
self.ep = ep
def lbc(self):
return self.ep.config['lbc']
def www(self):
return self.ep.master.www
@app.route('/builds/<int:build_id>/jobs/<int:job_index>/artifacts/<artifact_type>', methods=['GET'])
@defer.inlineCallbacks
def getArtifact(self, request, build_id, job_index, artifact_type):
if not validate_artifact_type(artifact_type):
request.setResponseCode(400)
request.setHeader('Content-Type', 'application/json')
return json.dumps({'error': BAD_REQUEST})
storage_dir = self.lbc().storage_dir
try:
artifact_file = yield self.get_artifact_location(request, build_id, job_index, artifact_type)
if os.path.exists(artifact_file):
return File(artifact_file)
else:
raise FileNotFoundError('Not Found')
except:
request.setResponseCode(404)
request.setHeader('Content-Type', 'application/json')
return json.dumps({'error': NOT_FOUND})
@defer.inlineCallbacks
- def get_artifact_location(self, request, build_id, job_index, artifact_type):
+ def get_artifact_location(self, request, build_id, job_index, artifact_type, job_name=None):
+ props_to_get = [
+ 'lilybuild_repo_id',
+ 'lilybuild_source',
+ ]
+ if job_index is None and job_name is None:
+ raise FileNotFoundError('Not Found')
+ if job_index is None and job_name is not None:
+ props_to_get.append('lilybuild_job_map')
result_spec = ResultSpec(
# see popProperties in ResultSpec
- properties=[Property(b'property', 'eq', [
- 'lilybuild_repo_id',
- 'lilybuild_source',
- ])]
+ properties=[Property(b'property', 'eq', props_to_get)]
)
build = yield self.ep.master.data.get_with_resultspec(
('builds', build_id), result_spec
)
if not build:
raise FileNotFoundError('Not Found')
if 'lilybuild_repo_id' not in build['properties']:
raise FileNotFoundError('Not Found')
if 'lilybuild_source' not in build['properties']:
source = 'none'
else:
source = build['properties']['lilybuild_source'][0]
+ if job_index is None:
+ if 'lilybuild_job_map' not in build['properties']:
+ raise FileNotFoundError('Not Found')
+ job_map = build['properties']['lilybuild_job_map'][0]
+ if job_name not in job_map:
+ raise FileNotFoundError('Not Found')
+ job_index = job_map[job_name]
# Things not pushed to staging area is considered private
# and can only be seen by project members
if source == 'arc-patch':
# see ForgejoAuthz
yield self.www().assertUserAllowed(request, ('lilybuild_artifacts', 'builds', build_id, 'jobs', job_index, 'artifacts', artifact_type), 'get', {})
repo_id = build['properties']['lilybuild_repo_id'][0]
fn = POSSIBLE_ARTIFACT_TYPES[artifact_type]
res = os.path.join(
self.lbc().storage_dir,
'repos', str(repo_id),
'builds', str(build_id),
'jobs', str(job_index),
'artifacts', fn
)
return res
+
+ @app.route('/repos/<int:repo_id>/latest/<ref_name>/artifacts/<artifact_type>', methods=['GET'])
+ @defer.inlineCallbacks
+ def getLatestArtifact(self, request, repo_id, ref_name, artifact_type):
+ if not validate_ref_name(ref_name):
+ request.setResponseCode(400)
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({'error': f'Bad ref_name format: {ref_name}'})
+
+ if not validate_artifact_type(artifact_type):
+ request.setResponseCode(400)
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({'error': BAD_REQUEST})
+
+ is_good = True
+ if b'good' in request.args:
+ good = request.args[b'good'][0]
+ if good == b'1':
+ is_good = True
+ elif good == b'0':
+ is_good = False
+ else:
+ request.setResponseCode(400)
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({'error': 'query `good` must be 0 or 1'})
+
+ if b'job' not in request.args:
+ request.setResponseCode(400)
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({'error': 'query `job` missing'})
+
+ job_name = request.args[b'job'][0].decode()
+ link_name = os.path.join(
+ self.lbc().storage_dir,
+ 'repos', str(repo_id),
+ 'latest-good' if is_good else 'latest', ref_name
+ )
+ try:
+ target = os.readlink(link_name)
+ build_id = int(os.path.basename(target))
+ artifact_file = yield self.get_artifact_location(request, build_id, None, artifact_type, job_name)
+ if os.path.exists(artifact_file):
+ return File(artifact_file)
+ else:
+ raise FileNotFoundError('Not Found')
+ except:
+ request.setResponseCode(404)
+ request.setHeader('Content-Type', 'application/json')
+ return json.dumps({'error': NOT_FOUND})
diff --git a/lilybuild/lilybuild/ci_steps.py b/lilybuild/lilybuild/ci_steps.py
index 10e6124..1a30087 100644
--- a/lilybuild/lilybuild/ci_steps.py
+++ b/lilybuild/lilybuild/ci_steps.py
@@ -1,574 +1,576 @@
from buildbot.plugins import *
from buildbot.process import buildstep, logobserver
from buildbot.interfaces import IRenderable
from twisted.internet import defer
from .ci_syntax import ci_file
from .ci_syntax import rules as ci_rules
from .helpers import rsync_rules_from_artifacts, get_job_script, normalize_image
from .phorge import SendCoverageToPhorge
import re
import sys
import json
SAFETAR_EXEC = '/lilybuild/lilybuild/safetar.py'
COVERAGE_EXEC = '/lilybuild/lilybuild/coverage.py'
def on_success(step):
return step.build.results == util.SUCCESS
def on_always(_step):
return True
def fill_list(*args):
return list(args)
class RunCIJobStep(steps.BuildStep):
# 200 MiB
artifact_max_size = 200 * 1024 * 1024
default_image = 'alpine'
master_job_artifact_dir_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/builds/%(prop:lilybuild_root_build_id)s/jobs/%(kw:job)s/artifacts'
artifact_file_name = 'artifacts.tar'
master_job_artifact_file_name_pattern = master_job_artifact_dir_pattern + '/' + artifact_file_name
reports_file_name = 'reports.tar'
master_reports_file_name_pattern = master_job_artifact_dir_pattern + '/' + reports_file_name
master_pages_dir_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/pages'
phorge_coverage_file_name = 'coverage-phorge.json'
def __init__(
self,
lbc,
src_relative=None,
src_dir=None,
storage_dir=None,
repo_id=None,
result_relative=None,
result_dir=None,
artifact_stage_relative=None,
artifact_stage_dir=None,
job_prop=None,
artifact_link_base=None,
**kwargs):
self.lbc = lbc
self.src_relative = src_relative
self.src_dir = src_dir
self.work_root_dir = kwargs['workdir']
self.script_dir = 'script'
self.storage_dir = storage_dir
self.repo_id = repo_id
self.artifact_stage_relative = artifact_stage_relative
self.artifact_stage_dir = artifact_stage_dir
self.result_relative = result_relative
self.result_dir = result_dir
self.artifact_link_base = artifact_link_base
super().__init__(name='Run step', **kwargs)
def get_cur_repo_config(self):
return self.lbc.repos[self.getProperty('lilybuild_repo_id')]
@defer.inlineCallbacks
def run(self):
job_prop = self.getProperty('lilybuild_job_prop')
job = ci_file.CIJob.from_prop(job_prop)
job_index = self.getProperty('lilybuild_job_index')
variables = yield self.get_ci_variables(job)
should_run = True
if len(job.rules):
should_run = False
for r in job.rules:
try:
when = r.get('when', 'on_success')
if when == 'never':
should_run_to_set = False
else:
should_run_to_set = True
rule_str = r.get('if')
if not rule_str:
# No condition == always true
# TODO: `changes` rule
should_run = should_run_to_set
break
res = ci_rules.evaluate_rule(ci_rules.parse_rule(rule_str), variables)
if not res:
continue
should_run = should_run_to_set
break
except SyntaxError:
self.addCompleteLog('error', f'Rule "{rule_str}" has syntax errors')
except:
pass
if should_run:
next_steps = self.job_to_steps(job, job_index, variables)
self.build.addStepsAfterCurrentStep(next_steps)
return util.SUCCESS
else:
self.addCompleteLog('info', 'Job skipped by a rule')
return util.SKIPPED
@defer.inlineCallbacks
def get_ci_variables(self, job):
res = {}
res.update(job.get_predefined_ci_variables())
res.update(self.getProperty('lilybuild_pipeline_vars'))
res['CI_JOB_IMAGE'] = job.image or self.default_image
res['CI_JOB_URL'] = yield self.build.getUrl()
res['CI_JOB_ID'] = self.build.buildid
res['CI_PROJECT_DIR'] = '/build'
try:
repo = self.lbc.repos[self.getProperty('lilybuild_repo_id')]
variables = yield repo['variables_getter'](self.build)
res_vars = {}
for var in variables:
value = variables[var]
if IRenderable.providedBy(value):
value = yield self.build.render(value)
res_vars[var] = value
res.update(res_vars)
except Exception as e:
self.addCompleteLog('exception', f'{e}')
return res
def get_upload_artifacts_jobs(self, short_name, artifact_type, artifact_name, base_dir, paths, exclude, master_pattern, job_index, doStepIf=on_success, has_pages=False):
archive_artifact_step = steps.ShellCommand(
name=f'Archive artifacts: {short_name}',
command=[
SAFETAR_EXEC,
],
initialStdin=json.dumps({
'op': 'create',
'archive_file': artifact_name,
'base_dir': base_dir,
'content': paths,
'items_to_exclude': exclude,
'compression': 'gz',
'limit_bytes': self.get_cur_repo_config()['artifact_uncompressed_limit'],
}),
workdir=self.work_root_dir,
doStepIf=doStepIf,
)
masterdest = util.Interpolate(
master_pattern,
st=self.storage_dir,
job=job_index,
doStepIf=doStepIf,
)
parent_build_id = self.getProperty('lilybuild_pipeline_vars')['CI_PIPELINE_ID']
artifact_url = f'{self.artifact_link_base}/plugins/lilybuild_artifacts/builds/{parent_build_id}/jobs/{job_index}/artifacts/{artifact_type}' if self.artifact_link_base else None
upload_artifact_step = steps.FileUpload(
workersrc=artifact_name,
maxsize=self.get_cur_repo_config()['artifact_compressed_limit'],
name=f'Upload artifacts: {short_name}',
masterdest=masterdest,
workdir=self.work_root_dir,
url=artifact_url,
doStepIf=doStepIf,
)
r = [archive_artifact_step, upload_artifact_step]
if has_pages:
r.append(steps.MasterShellCommand(
command=util.Transform(fill_list,
sys.executable,
'-m', 'lilybuild.pages',
util.Interpolate(
self.master_pages_dir_pattern,
st=self.storage_dir,
),
masterdest,
),
name='Deploy pages',
logEnviron=False,
doStepIf=doStepIf,
))
return r
def job_to_steps(self, job, job_index, variables):
script_name = self.script_dir + '/run.sh'
source_step = self.lbc.create_source_step()
script_step = steps.StringDownload(
get_job_script(variables, job),
name='Set up script',
workerdest=script_name,
workdir=self.work_root_dir,
doStepIf=on_success,
)
chmod_step = steps.ShellCommand(
name='Make script executable',
command=['chmod', '+x', script_name],
workdir=self.work_root_dir,
doStepIf=on_success,
)
artifact_steps = []
dep_job_indices = self.getProperty('lilybuild_dependency_job_indices')
if dep_job_indices:
for i in dep_job_indices:
# The steps may not run or may not have an artifact even if it runs
download_job = steps.FileDownload(
mastersrc=util.Interpolate(
self.master_job_artifact_file_name_pattern,
st=self.storage_dir,
job=i,
),
maxsize=self.get_cur_repo_config()['artifact_compressed_limit'],
name=f'Download artifacts from job #{i}',
workerdest=self.artifact_file_name,
workdir=self.work_root_dir,
doStepIf=on_success,
haltOnFailure=False,
flunkOnFailure=False,
# https://github.com/buildbot/buildbot/issues/3709
blocksize=256 * 1024,
)
unarchive_job = steps.ShellCommand(
name=f'Unarchive artifacts from job #{i}',
command=[
SAFETAR_EXEC,
],
initialStdin=json.dumps({
'op': 'extract',
'archive_file': self.artifact_file_name,
'target_dir': self.src_relative,
}),
workdir=self.work_root_dir,
doStepIf=on_success,
haltOnFailure=False,
flunkOnFailure=False,
)
artifact_steps += [download_job, unarchive_job]
run_step = steps.ShellCommand(
name='Run script in container',
command=[
'/lilybuild/podman-helper',
normalize_image(job.image or self.default_image),
self.src_relative,
self.script_dir,
self.result_relative,
],
workdir=self.work_root_dir,
doStepIf=on_success,
# 2h timeout by default
# TODO support timeout by each job
timeout=60 * 60 * 2,
)
clean_script_step = steps.ShellCommand(
name='Clean script dir',
command=[
'rm',
'-rf',
self.script_dir,
],
workdir=self.work_root_dir,
alwaysRun=True,
)
steps_to_run = [source_step, script_step, chmod_step] + artifact_steps + [run_step, clean_script_step]
if 'paths' in job.artifacts:
steps_to_run += self.get_upload_artifacts_jobs(
'files',
'archive',
self.artifact_file_name,
self.result_relative,
job.artifacts.get('paths', []),
job.artifacts.get('exclude', []),
self.master_job_artifact_file_name_pattern,
job_index,
has_pages=job.is_pages()
)
if job.has_supported_coverage_report():
steps_to_run += [steps.ShellCommand(
name='Process reports',
command=[
COVERAGE_EXEC,
],
initialStdin=json.dumps({
'source_dir': self.src_relative,
'result_dir': self.result_relative,
'untrusted_coverage_file': job.artifacts['reports']['coverage_report']['path'],
'output_dir': self.artifact_stage_relative,
}),
workdir=self.work_root_dir,
doStepIf=on_always,
)] + self.get_upload_artifacts_jobs(
'reports',
'reports',
self.reports_file_name,
self.artifact_stage_relative,
['*'],
[],
self.master_reports_file_name_pattern,
job_index,
doStepIf=on_always
) + [SendCoverageToPhorge(
self.lbc,
self.artifact_stage_relative + '/' + self.phorge_coverage_file_name,
workdir=self.work_root_dir,
)]
clean_stage_dir_again_step = steps.ShellCommand(
name='Clean stage, result and artifact',
command=[
'rm',
'-rf',
self.result_relative,
self.artifact_file_name,
self.artifact_stage_relative,
self.reports_file_name,
],
workdir=self.work_root_dir,
alwaysRun=True,
)
steps_to_run.append(clean_stage_dir_again_step)
return steps_to_run
class TriggerMultipleJobsStep(steps.Trigger):
properties_to_keep = [
'branch',
'revision',
'repository',
'harbormaster_build_target_phid',
'harbormaster_variable_buildable.diff',
'harbormaster_variable_repository.staging.ref',
'harbormaster_variable_repository.staging.uri',
'harbormaster_variable_repository.uri',
'lilybuild_repo',
'lilybuild_repo_id',
'lilybuild_pipeline_vars',
]
def __init__(self, lbc, jobs_with_data, **kwargs):
self.lbc = lbc
self.jobs = jobs_with_data
super().__init__(schedulerNames=[self.lbc.triggerable_scheduler_name], **kwargs)
def getSchedulersAndProperties(self):
ret = []
common_properties = {
'lilybuild_root_build_id': self.build.buildid,
}
for prop in self.properties_to_keep:
if self.hasProperty(prop):
common_properties[prop] = self.getProperty(prop)
for (job, i, dep_job_indices) in self.jobs:
properties = common_properties.copy()
properties['lilybuild_job_prop'] = job.to_prop()
properties['lilybuild_job_index'] = i
properties['virtual_builder_name'] = 'lilybuild-job - ' + common_properties['lilybuild_repo'] + ' - ' + job.name
properties['lilybuild_dependency_job_indices'] = dep_job_indices
ret.append({
'sched_name': self.lbc.triggerable_scheduler_name,
'props_to_set': properties,
'unimportant': False,
})
return ret
class LatestMixin:
latest_build_dir_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/latest'
latest_good_build_dir_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/latest-good'
latest_name_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/latest/%(kw:name)s'
latest_good_name_pattern = '%(kw:st)s/repos/%(prop:lilybuild_repo_id)s/latest-good/%(kw:name)s'
class EnsureLatestDirs(steps.MasterShellCommand, LatestMixin):
def __init__(self, lbc, storage_dir, **kwargs):
self.lbc = lbc
self.storage_dir = storage_dir
command = util.Transform(
fill_list,
'mkdir',
'-pv',
util.Interpolate(self.latest_build_dir_pattern, st=self.storage_dir),
util.Interpolate(self.latest_good_build_dir_pattern, st=self.storage_dir))
super().__init__(
name='Ensure latest dirs',
command=command,
logEnviron=False,
doStepIf=on_always,
**kwargs
)
class MarkLatest(steps.MasterShellCommand, LatestMixin):
def __init__(self, lbc, storage_dir, buildid, ref_name, is_good, **kwargs):
self.lbc = lbc
self.storage_dir = storage_dir
name_pattern = self.latest_good_name_pattern if is_good else self.latest_name_pattern
command = util.Transform(
fill_list,
'ln',
'-sfvn',
util.Interpolate('../builds/%(kw:buildid)s', buildid=buildid),
util.Interpolate(name_pattern, st=self.storage_dir, name=ref_name),
)
super().__init__(
name='Mark build as latest-good' if is_good else 'Mark build as latest',
command=command,
logEnviron=False,
doStepIf=on_success if is_good else on_always,
**kwargs
)
class AnalyzeCIFileCommand(buildstep.ShellMixin, steps.BuildStep):
ci_def_file = '.gitlab-ci.yml'
build_target_prop_name = 'harbormaster_build_target_phid'
def __init__(
self,
lbc,
src_relative=None,
src_dir=None,
storage_dir=None,
repo_id=None,
result_relative=None,
result_dir=None,
artifact_stage_relative=None,
artifact_stage_dir=None,
**kwargs):
kwargs['name'] = 'Analyze CI file'
kwargs['command'] = ['cat', self.ci_def_file]
self.lbc = lbc
self.src_relative = src_relative
self.src_dir = src_dir
self.work_root_dir = kwargs['workdir']
self.script_dir = 'script'
self.storage_dir = storage_dir
self.repo_id = repo_id
self.artifact_stage_relative = artifact_stage_relative
self.artifact_stage_dir = artifact_stage_dir
self.result_relative = result_relative
self.result_dir = result_dir
kwargs['workdir'] = self.src_dir
kwargs = self.setupShellMixin(kwargs)
super().__init__(**kwargs)
self.observer = logobserver.BufferLogObserver()
self.addLogObserver('stdio', self.observer)
def stage_to_step(self, stage_name, stage_jobs, job_name_to_index_map, ci_file):
jobs_with_data = []
for job in stage_jobs:
dep_job_names = [
jn
for jn in ci_file.get_jobs_to_pull_artifacts_from(job.name)
if ci_file.jobs[jn].has_artifacts_archive()
]
dep_job_indices = [job_name_to_index_map[jn] for jn in dep_job_names]
jobs_with_data.append((job, job_name_to_index_map[job.name], dep_job_indices))
trigger = TriggerMultipleJobsStep(
name=stage_name,
lbc=self.lbc,
jobs_with_data=jobs_with_data,
waitForFinish=True,
doStepIf=on_success,
)
return trigger
- def get_steps(self, stdout):
+ def get_steps_and_job_map(self, stdout):
f = ci_file.CIFile(stdout)
stages = f.get_grouped_jobs()
jobs = [job for (stage, js) in f.get_grouped_jobs() for job in js]
job_names = [job.name for job in jobs]
job_name_to_index_map = {}
for (i, j) in enumerate(jobs):
job_name_to_index_map[j.name] = i
steps = [self.stage_to_step(stage_name, stage_jobs, job_name_to_index_map, f) for (stage_name, stage_jobs) in stages]
print('steps:', steps)
- return steps
+ return (steps, job_name_to_index_map)
def get_is_phorge(self):
return not not self.getProperty(self.build_target_prop_name)
def get_ref_and_type(self):
ref_type = 'branch'
ref = self.getProperty('branch')
if self.getProperty('category') == 'tag':
ref_type = 'tag'
if ref is not None:
m = re.match(r'^refs/(heads|tags)/(.+)$', ref)
if m:
ref = m.group(2)
return (ref, ref_type)
def get_cur_repo_config(self):
return self.lbc.repos[self.getProperty('lilybuild_repo_id')]
@defer.inlineCallbacks
def get_pipeline_ci_vars(self):
url = yield self.build.getUrl()
res = {
'CI_PIPELINE_ID': self.build.buildid,
'CI_PIPELINE_IID': self.build.buildid,
'CI_PIPELINE_URL': url,
'CI_PROJECT_ID': self.getProperty('lilybuild_repo_id'),
'CI_CONFIG_PATH': self.ci_def_file,
}
if not self.get_is_phorge():
res['CI_COMMIT_SHA'] = self.getProperty('got_revision')
res['CI_COMMIT_SHORT_SHA'] = res['CI_COMMIT_SHA'][:8]
(ref, ref_type) = self.get_ref_and_type()
res['CI_COMMIT_REF_NAME'] = ref
res['CI_COMMIT_REF_SLUG'] = ci_file.ci_slugify(ref)
res['CI_COMMIT_REF_PROTECTED'] = 'false'
if ref_type == 'tag':
res['CI_COMMIT_TAG'] = ref
elif ref_type == 'branch':
res['CI_COMMIT_BRANCH'] = ref
return res
@defer.inlineCallbacks
def run(self):
# run './build.sh --list-stages' to generate the list of stages
cmd = yield self.makeRemoteShellCommand()
yield self.runCommand(cmd)
# if the command passes extract the list of stages
result = cmd.results()
if result == util.SUCCESS:
pipeline_vars = yield self.get_pipeline_ci_vars()
self.setProperty('lilybuild_pipeline_vars', pipeline_vars, self.__class__.__name__)
# create a ShellCommand for each stage and add them to the build
- self.build.addStepsAfterCurrentStep(self.get_steps(self.observer.getStdout()))
+ (steps, job_map) = self.get_steps_and_job_map(self.observer.getStdout())
+ self.setProperty('lilybuild_job_map', job_map, self.__class__.__name__)
+ self.build.addStepsAfterCurrentStep(steps)
latest_branch_map = self.get_cur_repo_config()['artifact_latest_branch_map']
ref, _ref_type = self.get_ref_and_type()
if ref in latest_branch_map:
latest_name = latest_branch_map[ref]
self.build.addStepsAfterLastStep([
EnsureLatestDirs(lbc=self.lbc, storage_dir=self.storage_dir),
MarkLatest(
lbc=self.lbc,
storage_dir=self.storage_dir,
buildid=self.build.buildid,
ref_name=latest_name,
is_good=True,
),
MarkLatest(
lbc=self.lbc,
storage_dir=self.storage_dir,
buildid=self.build.buildid,
ref_name=latest_name,
is_good=False,
),
])
return result
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 9, 1:22 AM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784382
Default Alt Text
(28 KB)
Attached To
Mode
rB lilybuild
Attached
Detach File
Event Timeline
Log In to Comment