id: kestra-ui-edit-backport-to-git-guard
namespace: company.team
description: Backport UI hotfixes to a Git review branch without overwriting
Git-only changes or conflicts. Requires the last deployed Git commit as the
shared baseline.
concurrency:
limit: 1
behavior: FAIL
inputs:
- id: kestra_url
type: STRING
defaults: http://localhost:8080
description: Managed Kestra instance. Run this guard from a separate operations
instance.
- id: kestra_username
type: STRING
defaults: admin@kestra.io
- id: kestra_password
type: SECRET
defaults: '{{ secret(''KESTRA_PASSWORD'') }}'
- id: repository
type: STRING
description: HTTP(S) Git repository URL with a default branch.
- id: git_username
type: STRING
- id: git_token
type: SECRET
defaults: '{{ secret(''GIT_TOKEN'') }}'
- id: managed_namespace
type: STRING
defaults: company.production
- id: baseline_commit
type: STRING
description: Full 40-character Git SHA last deployed successfully to this
namespace. Must be an audited common baseline.
- id: review_branch
type: STRING
description: Stable incident branch, for example ui-backport/incident123. Reuse
it for safe retries.
- id: git_directory
type: STRING
defaults: _flows
description: Flat directory of <flow-id>.yml files. Child namespaces and other
layouts are refused.
- id: protected_flows
type: ARRAY
itemType: STRING
defaults: []
- id: max_flows
type: INT
defaults: 500
- id: max_targets
type: INT
defaults: 10
- id: dry_run
type: BOOL
defaults: true
- id: auto_remediate
type: BOOL
defaults: false
variables:
comparison_script: |
"""Local comparison only; remote reads and writes belong to Kestra plugins."""
import hashlib, json, os, re, struct, sys
from pathlib import Path
import yaml
cfg = json.loads(os.environ['CONFIG'])
def require(ok, message):
if not ok:
raise ValueError(message)
def digest(data):
return hashlib.sha256(data).hexdigest()
def normal(source):
# Preserve YAML scalar whitespace. Only mirror PushFlows' revision stripping and LF.
return re.sub(r'^revision:\s*\d+\n?', '', source.replace('\r\n', '\n'), flags=re.M).encode('utf-8')
def validate():
require(re.fullmatch(r'[a-z0-9][a-z0-9._-]{0,149}', cfg['namespace']), 'Invalid namespace')
require(re.fullmatch(r'[0-9a-f]{40}', cfg['baseline']), 'Use the full last-deployed commit SHA')
require(re.fullmatch(r'ui-backport/[A-Za-z0-9][A-Za-z0-9_-]{0,60}', cfg['review']), 'Use an incident-specific ui-backport/ branch')
require(re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9_/-]{0,100}', cfg['directory']) and all(p not in ('', '.', '..') for p in cfg['directory'].split('/')), 'Unsafe Git directory')
require(1 <= cfg['max_flows'] <= 1000 and 1 <= cfg['max_targets'] <= 100, 'Limits outside supported range')
require(isinstance(cfg['protected'], list) and all(isinstance(x,str) for x in cfg['protected']), 'Protected IDs must be strings')
class UniqueLoader(yaml.SafeLoader):
pass
def unique_map(loader, node, deep=False):
pairs = loader.construct_pairs(node, deep=deep)
result = {}
for k,v in pairs:
require(k not in result, 'Duplicate YAML key')
result[k] = v
return result
UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, unique_map)
def flow(source):
require(len(source.encode()) <= 1048576, 'Flow exceeds 1 MiB')
data = yaml.load(source, Loader=UniqueLoader)
require(isinstance(data, dict), 'Expected flow YAML mapping')
fid, ns = data.get('id'), data.get('namespace')
require(isinstance(fid, str) and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{0,149}', fid), 'Flow ID is not a literal safe glob')
return fid, ns, digest(normal(source))
def refs(root):
git = Path(root)/'.git'
result = {}
if (git/'packed-refs').exists():
for line in (git/'packed-refs').read_text().splitlines():
if line and line[0] not in '#^':
sha, name = line.split(' ')
result[name] = sha
if (git/'refs').exists():
for path in (git/'refs').rglob('*'):
if path.is_file():
result[path.relative_to(git).as_posix()] = path.read_text().strip()
return result
def discovery():
validate()
head = (Path('main')/'.git/HEAD').read_text().strip()
require(head.startswith('ref: refs/heads/'), 'Repository has no default branch')
default = head.split('ref: refs/heads/',1)[1]
require(default != cfg['review'], 'Review branch is the default branch')
return {'review_exists': ('refs/remotes/origin/'+cfg['review']) in refs('main'), 'default_branch': default}
def files(root):
result = {}
for path in Path(root).rglob('*'):
rel = path.relative_to(root)
if rel.parts[0] == '.git':
continue
require(not path.is_symlink(), 'Symlink in repository')
if path.is_file():
require(path.stat().st_size <= 4194304, 'Repository file exceeds 4 MiB')
result[rel.as_posix()] = digest(path.read_bytes())
require(len(result) <= 5000, 'Repository exceeds 5000 files')
require(not any(x.rsplit('/',1)[-1] == '.kestraignore' for x in result), '.kestraignore can suppress a selected backport')
return result
def safe_git_index(root):
# Script artifact transfer can dereference symlinks. Git's index retains their mode.
data = (Path(root)/'.git/index').read_bytes()
require(len(data) >= 32 and data[:4] == b'DIRC', 'Missing or invalid Git index')
version, count = struct.unpack_from('>II', data, 4)
require(version in (2, 3), 'Unsupported Git index version')
require(hashlib.sha1(data[:-20]).digest() == data[-20:], 'Corrupt Git index')
pos = 12
for _ in range(count):
start = pos
require(pos + 62 <= len(data) - 20, 'Truncated Git index entry')
mode = struct.unpack_from('>I', data, pos + 24)[0]
require(mode != 0o120000, 'Symlink in repository Git index')
flags = struct.unpack_from('>H', data, pos + 60)[0]
pos += 62
if flags & 0x4000:
require(version == 3, 'Invalid extended Git index entry')
pos += 2
end = data.find(b'\0', pos, len(data) - 20)
require(end >= pos, 'Unterminated Git index path')
pos = start + ((end + 1 - start + 7) // 8) * 8
require(pos <= len(data) - 20, 'Truncated Git index padding')
def tree(root):
safe_git_index(root)
result = {}
folder = Path(root)/cfg['directory']
if not folder.exists():
return result
for path in folder.rglob('*'):
require(not path.is_symlink(), 'Symlink in flow directory')
if not path.is_file():
continue
require(path.parent == folder and path.suffix == '.yml', 'Use flat <flow-id>.yml layout in Git directory')
fid, ns, sha = flow(path.read_text())
require(ns == cfg['namespace'] and path.name == fid+'.yml', 'Git identity/path mismatch')
require(fid not in result, 'Duplicate flow identity')
result[fid] = sha
require(len(result) <= cfg['max_flows'], 'Flow count exceeds max_flows')
return result
def push_source_safe(source):
# PushFlows 4.3.0 interpolates this namespace UNESCAPED, even with no target override.
matches = list(re.finditer(r'(?m)^(\s*namespace:\s*)' + cfg['namespace'], source, re.ASCII))
if len(matches) > 1:
return False
for match in matches:
prefix = match.group(1)
indentation = prefix.split('namespace:', 1)[0].rsplit('\n', 1)[-1]
if indentation.strip('\r') or match.group(0)[len(prefix):] != cfg['namespace']:
return False
return True
def live():
sources = json.loads(Path('live.json').read_text())
require(len(sources) <= cfg['max_flows'], 'Export count exceeds max_flows')
result, unsafe = {}, []
for source in sources.values():
fid, ns, sha = flow(source)
if ns == cfg['namespace']: # Export is a prefix read; children/lookalikes never qualify.
require(fid not in result, 'Duplicate live identity')
result[fid] = sha
if not push_source_safe(source):
unsafe.append(fid)
return result, unsafe
def plan():
disc = discovery()
b, g = tree('baseline'), tree('main')
l, unsafe = live()
branch = 'review' if disc['review_exists'] else 'main'
r = tree(branch)
all_main, all_review = files('main'), files(branch)
rows, ahead = [], []
for fid in sorted(set(b)|set(g)|set(l)):
bh, gh, lh = b.get(fid), g.get(fid), l.get(fid)
if gh == lh:
status = 'IN_SYNC'
elif lh == bh:
status = 'BEHIND'
elif gh == bh and lh is not None:
status = 'AHEAD'
else:
status = 'DIVERGED'
protected = fid in cfg['protected']
rows.append({'id':fid, 'status':status, 'protected':protected, 'baseline':bh, 'git':gh, 'live':lh})
if status == 'AHEAD' and not protected:
ahead.append(fid)
require(not set(ahead).intersection(unsafe), 'Unsafe native namespace rewrite in eligible flow: ' + ', '.join(sorted(set(ahead).intersection(unsafe))))
paths = {cfg['directory']+'/'+fid+'.yml' for fid in ahead}
require({k:v for k,v in all_main.items() if k not in paths} == {k:v for k,v in all_review.items() if k not in paths}, 'Review branch has unrelated edits')
for fid in ahead:
require(r.get(fid) in (g.get(fid), l[fid]), 'Review branch contains a conflicting prior backport')
targets = [fid for fid in ahead if r.get(fid) != l[fid]]
require(len(targets) <= cfg['max_targets'], 'Target count exceeds max_targets')
return {'rows':rows, 'targets':targets, 'ahead':ahead, 'live':l, 'git':g, 'baseline':b, 'review':r, 'main_files':all_main, 'review_files':all_review, **disc}
mode = sys.argv[1]
if mode == 'validate':
validate(); output={'valid': True}
elif mode == 'discover':
output=discovery()
else:
output=plan()
if mode == 'recheck':
approved=json.loads(Path('approved.json').read_text())
require(output == approved, 'Stale approval: live, Git, review branch or target set changed')
elif mode == 'verify':
approved=json.loads(Path('approved.json').read_text())
require(output['main_files'] == approved['main_files'], 'Default branch changed')
require(output['live'] == approved['live'], 'Live definitions changed during backport')
chosen = set(approved['targets'])
paths = {cfg['directory']+'/'+fid+'.yml' for fid in chosen}
require(output['review_exists'], 'Review branch was not created')
require(all(output['review'].get(fid) == approved['live'][fid] for fid in chosen), 'Backport verification failed')
require({k:v for k,v in output['review_files'].items() if k not in paths} == {k:v for k,v in approved['review_files'].items() if k not in paths}, 'Protected repository files changed')
Path('report.json').write_text(json.dumps(output,sort_keys=True))
print('::' + json.dumps({'outputs':output}, separators=(',',':')) + '::')
tasks:
- id: validate_workspace
type: io.kestra.plugin.core.flow.WorkingDirectory
tasks:
- id: validate_inputs
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
timeout: PT2M
commands:
- python guard.py validate
outputFiles:
- report.json
- id: initial_workspace
type: io.kestra.plugin.core.flow.WorkingDirectory
tasks:
- id: initial_export
type: io.kestra.plugin.kestra.flows.Export
kestraUrl: '{{ inputs.kestra_url }}'
auth:
auto: false
username: '{{ inputs.kestra_username }}'
password: '{{ inputs.kestra_password }}'
namespace: '{{ inputs.managed_namespace }}'
- id: initial_unzip
type: io.kestra.plugin.compress.ArchiveDecompress
algorithm: ZIP
from: '{{ outputs.initial_export.flowsZip }}'
- id: initial_default
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: main
depth: 1
cloneAllBranches: true
- id: initial_baseline
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: baseline
commit: '{{ inputs.baseline_commit }}'
- id: initial_discover
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
timeout: PT2M
commands:
- python guard.py discover
outputFiles:
- report.json
- id: initial_review
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: review
branch: '{{ outputs.initial_discover.vars.review_exists ? inputs.review_branch :
outputs.initial_discover.vars.default_branch }}'
- id: inspect
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
live.json: |-
{
{% for entry in outputs.initial_unzip.files %}
{{ entry.key | toJson }}: {{ read(entry.value) | toJson }}{% if not loop.last %},{% endif %}
{% endfor %}
}
timeout: PT2M
commands:
- python guard.py plan
outputFiles:
- report.json
- id: report
type: io.kestra.plugin.core.log.Log
message: '{{ read(outputs.inspect.outputFiles[''report.json'']) }}'
- id: apply_gate
type: io.kestra.plugin.core.flow.If
condition: '{{ inputs.auto_remediate and not inputs.dry_run and
(outputs.inspect.vars.targets | length) > 0 }}'
then:
- id: approve
type: io.kestra.plugin.core.flow.Pause
pauseDuration: PT1H
onResume:
- id: approved
type: BOOL
defaults: false
- id: approved_gate
type: io.kestra.plugin.core.flow.If
condition: '{{ outputs.approve.onResume.approved ?? false }}'
then:
- id: fresh_workspace
type: io.kestra.plugin.core.flow.WorkingDirectory
tasks:
- id: fresh_export
type: io.kestra.plugin.kestra.flows.Export
kestraUrl: '{{ inputs.kestra_url }}'
auth:
auto: false
username: '{{ inputs.kestra_username }}'
password: '{{ inputs.kestra_password }}'
namespace: '{{ inputs.managed_namespace }}'
- id: fresh_unzip
type: io.kestra.plugin.compress.ArchiveDecompress
algorithm: ZIP
from: '{{ outputs.fresh_export.flowsZip }}'
- id: fresh_default
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: main
depth: 1
cloneAllBranches: true
- id: fresh_baseline
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: baseline
commit: '{{ inputs.baseline_commit }}'
- id: fresh_discover
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
timeout: PT2M
commands:
- python guard.py discover
outputFiles:
- report.json
- id: fresh_review
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: review
branch: '{{ outputs.fresh_discover.vars.review_exists ? inputs.review_branch :
outputs.fresh_discover.vars.default_branch }}'
- id: recheck
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
live.json: |-
{
{% for entry in outputs.fresh_unzip.files %}
{{ entry.key | toJson }}: {{ read(entry.value) | toJson }}{% if not loop.last %},{% endif %}
{% endfor %}
}
approved.json: '{{ read(outputs.inspect.outputFiles[''report.json'']) }}'
timeout: PT2M
commands:
- python guard.py recheck
outputFiles:
- report.json
- id: push_selected
type: io.kestra.plugin.git.PushFlows
description: One commit containing only the freshly rechecked AHEAD flow IDs.
Never deletes repository files.
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
kestraUrl: '{{ inputs.kestra_url }}'
auth:
auto: false
username: '{{ inputs.kestra_username }}'
password: '{{ inputs.kestra_password }}'
branch: '{{ inputs.review_branch }}'
sourceNamespace: '{{ inputs.managed_namespace }}'
flows: '{{ outputs.recheck.vars.targets | toJson }}'
gitDirectory: '{{ inputs.git_directory }}'
includeChildNamespaces: false
delete: false
dryRun: false
commitMessage: Backport approved UI hotfixes from {{ inputs.managed_namespace }}
- id: after_workspace
type: io.kestra.plugin.core.flow.WorkingDirectory
tasks:
- id: after_export
type: io.kestra.plugin.kestra.flows.Export
kestraUrl: '{{ inputs.kestra_url }}'
auth:
auto: false
username: '{{ inputs.kestra_username }}'
password: '{{ inputs.kestra_password }}'
namespace: '{{ inputs.managed_namespace }}'
- id: after_unzip
type: io.kestra.plugin.compress.ArchiveDecompress
algorithm: ZIP
from: '{{ outputs.after_export.flowsZip }}'
- id: after_default
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: main
depth: 1
cloneAllBranches: true
- id: after_baseline
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: baseline
commit: '{{ inputs.baseline_commit }}'
- id: after_discover
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
timeout: PT2M
commands:
- python guard.py discover
outputFiles:
- report.json
- id: after_review
type: io.kestra.plugin.git.Clone
url: '{{ inputs.repository }}'
username: '{{ inputs.git_username }}'
password: '{{ inputs.git_token }}'
directory: review
branch: '{{ outputs.after_discover.vars.review_exists ? inputs.review_branch :
outputs.after_discover.vars.default_branch }}'
- id: verify
type: io.kestra.plugin.scripts.python.Commands
description: Compare local plugin artifacts; the Git and Kestra plugins do not
provide a three-way content-hash join.
containerImage: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
memory:
memory: 256MB
dependencies:
- PyYAML==6.0.2
env:
CONFIG: '{{ {''namespace'': inputs.managed_namespace, ''baseline'':
inputs.baseline_commit, ''review'': inputs.review_branch,
''directory'': inputs.git_directory, ''protected'':
inputs.protected_flows, ''max_flows'': inputs.max_flows,
''max_targets'': inputs.max_targets} | toJson }}'
inputFiles:
guard.py: '{{ vars.comparison_script }}'
live.json: |-
{
{% for entry in outputs.after_unzip.files %}
{{ entry.key | toJson }}: {{ read(entry.value) | toJson }}{% if not loop.last %},{% endif %}
{% endfor %}
}
approved.json: '{{ read(outputs.inspect.outputFiles[''report.json'']) }}'
timeout: PT2M
commands:
- python guard.py verify
outputFiles:
- report.json
- id: verified
type: io.kestra.plugin.core.log.Log
message: Review branch {{ inputs.review_branch }} contains the approved UI
definitions. Default branch and protected repository files
verified unchanged.
errors:
- id: alert_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
messageText: 'UI backport guard failed: {{ flow.id }}, execution {{ execution.id
}}. Inspect the report and review branch before retrying.'
triggers:
- id: audit_daily
type: io.kestra.plugin.core.trigger.Schedule
cron: 0 7 * * *
disabled: true
inputs:
dry_run: true
auto_remediate: false
repository: https://git.example.com/team/flows.git
git_username: operations
baseline_commit: '0000000000000000000000000000000000000000'
review_branch: ui-backport/audit