Skip to content

Commit 6fde162

Browse files
authored
perfetto: add workflow to attach LUCI artifacts to draft release (#5462)
Manually-triggered workflow that reconciles LUCI-built artifacts into the matching draft GitHub release once LUCI builds are complete. Reads from the existing gs://perfetto-luci-artifacts/<version>/ layout (no infra changes needed), zips per-arch directories, uploads both those and the SDK source zips as release assets, and flips the release from draft to published. Idempotent per RFC-0022.
1 parent e624d95 commit 6fde162

2 files changed

Lines changed: 249 additions & 35 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (C) 2026 The Android Open Source Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Attaches LUCI-built binaries and SDK source zips to the matching draft
16+
# GitHub release. Leaves the release as a draft — a human publishes it
17+
# manually from the GitHub UI after reviewing the release notes.
18+
#
19+
# Manual trigger after LUCI completes. Idempotent — safe to re-run if a
20+
# previous invocation failed partway. See RFC-0022 "LUCI -> GitHub bridge".
21+
22+
name: Attach release artifacts
23+
24+
on:
25+
workflow_dispatch:
26+
inputs:
27+
version:
28+
description: 'Release version to finalize (e.g. v54.0)'
29+
required: true
30+
type: string
31+
32+
permissions:
33+
contents: write # Required to upload assets and publish the release
34+
35+
jobs:
36+
finalize:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Validate input
40+
env:
41+
VERSION: ${{ inputs.version }}
42+
run: |
43+
if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+$ ]]; then
44+
echo "::error::Version must match vX.Y (got: $VERSION)"
45+
exit 1
46+
fi
47+
48+
- name: Checkout tag
49+
uses: actions/checkout@v4
50+
with:
51+
ref: ${{ inputs.version }}
52+
fetch-depth: 0
53+
fetch-tags: true
54+
55+
- name: Verify draft release exists
56+
env:
57+
GH_TOKEN: ${{ github.token }}
58+
VERSION: ${{ inputs.version }}
59+
run: |
60+
if ! gh release view "$VERSION" --repo "${{ github.repository }}" \
61+
> /dev/null 2>&1; then
62+
echo "::error::No release found for tag $VERSION. Push the tag \
63+
first (via promote-stable.yml) to create the draft."
64+
exit 1
65+
fi
66+
67+
- name: Install gcloud / gsutil
68+
uses: google-github-actions/setup-gcloud@v2
69+
70+
- name: Download, verify, and package LUCI artifacts
71+
env:
72+
VERSION: ${{ inputs.version }}
73+
run: |
74+
tools/release/package-github-release-artifacts --yes "$VERSION"
75+
76+
- name: Upload assets to draft release
77+
env:
78+
GH_TOKEN: ${{ github.token }}
79+
VERSION: ${{ inputs.version }}
80+
run: |
81+
STAGING="/tmp/perfetto-${VERSION}-github-release"
82+
gh release upload "$VERSION" "$STAGING"/*.zip \
83+
--repo "${{ github.repository }}" --clobber
84+
echo "::notice::Artifacts attached. Review the draft release at \
85+
https://github.com/${{ github.repository }}/releases and publish it \
86+
manually when ready."

tools/release/package-github-release-artifacts

Lines changed: 163 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,161 @@ All files will be placed into /tmp/perfetto-v20.0-github-release/ .
2727
"""
2828

2929
import argparse
30-
import subprocess
3130
import os
31+
import subprocess
3232
import sys
33-
import tempfile
34-
import shutil
33+
34+
# Expected LUCI artifact manifest. Must stay in sync with
35+
# infra/luci/recipes/perfetto.py — ARTIFACTS + the platform list in
36+
# RunSteps. Verified after rsync; a mismatch (missing OR extra files) is a
37+
# hard error because it means LUCI's output changed and this script needs to
38+
# be updated.
39+
#
40+
# On Windows, each binary is accompanied by a <name>.pdb (debug symbols),
41+
# expressed as 'windows_pdb': True per artifact. Platform filters match the
42+
# 'exclude_platforms' / 'include_platforms' keys in the recipe.
43+
_ARTIFACTS = [
44+
{
45+
'name': 'trace_processor_shell'
46+
},
47+
{
48+
'name': 'traceconv'
49+
},
50+
{
51+
'name': 'tracebox',
52+
'exclude_platforms': ['windows-amd64'],
53+
},
54+
{
55+
'name': 'perfetto'
56+
},
57+
{
58+
'name': 'traced'
59+
},
60+
{
61+
'name': 'traced_probes',
62+
'exclude_platforms': ['windows-amd64'],
63+
},
64+
{
65+
'name': 'heapprofd_glibc_preload',
66+
'file': 'libheapprofd_glibc_preload.so',
67+
'include_platforms': ['linux-amd64', 'linux-arm', 'linux-arm64'],
68+
},
69+
]
70+
71+
_PLATFORMS = [
72+
'android-arm',
73+
'android-arm64',
74+
'android-x64',
75+
'android-x86',
76+
'linux-amd64',
77+
'linux-arm',
78+
'linux-arm64',
79+
'mac-amd64',
80+
'mac-arm64',
81+
'windows-amd64',
82+
]
83+
84+
_SDK_ZIPS = [
85+
'perfetto-cpp-sdk-src.zip',
86+
'perfetto-c-sdk-src.zip',
87+
]
3588

3689

3790
def exec(*args):
3891
print(' '.join(args))
3992
subprocess.check_call(args)
4093

4194

42-
def get_repo_root():
43-
"""Returns the root directory of the Perfetto repository."""
44-
script_dir = os.path.dirname(os.path.abspath(__file__))
45-
return os.path.abspath(os.path.join(script_dir, '..', '..'))
95+
def _artifact_filename(artifact, platform):
96+
"""Returns the on-disk filename LUCI uploads for `artifact` on `platform`."""
97+
base = artifact.get('file', artifact['name'])
98+
if platform == 'windows-amd64' and 'file' not in artifact:
99+
return base + '.exe'
100+
return base
101+
46102

103+
def _expected_manifest():
104+
"""Returns {platform: set(filenames)} for all LUCI-produced binaries."""
105+
manifest = {p: set() for p in _PLATFORMS}
106+
for platform in _PLATFORMS:
107+
for artifact in _ARTIFACTS:
108+
if platform in artifact.get('exclude_platforms', []):
109+
continue
110+
include = artifact.get('include_platforms')
111+
if include is not None and platform not in include:
112+
continue
113+
fname = _artifact_filename(artifact, platform)
114+
manifest[platform].add(fname)
115+
if platform == 'windows-amd64':
116+
manifest[platform].add(fname + '.pdb')
117+
return manifest
118+
119+
120+
def verify_downloads(tmpdir):
121+
"""Verifies the rsynced tree matches the expected LUCI manifest.
122+
123+
Fails loudly on anything missing or unexpected — the manifest here must
124+
match LUCI, and drift in either direction should surface immediately.
125+
"""
126+
manifest = _expected_manifest()
127+
expected_dirs = set(manifest.keys()) | {'sdk'}
128+
actual_dirs = {
129+
d for d in os.listdir(tmpdir) if os.path.isdir(os.path.join(tmpdir, d))
130+
}
131+
132+
errors = []
133+
missing_dirs = expected_dirs - actual_dirs
134+
unexpected_dirs = actual_dirs - expected_dirs
135+
if missing_dirs:
136+
errors.append('Missing platform directories: %s' %
137+
', '.join(sorted(missing_dirs)))
138+
if unexpected_dirs:
139+
errors.append('Unexpected directories under GCS path: %s' %
140+
', '.join(sorted(unexpected_dirs)))
141+
142+
for platform, expected_files in manifest.items():
143+
pdir = os.path.join(tmpdir, platform)
144+
if not os.path.isdir(pdir):
145+
continue
146+
actual_files = set(os.listdir(pdir))
147+
missing = expected_files - actual_files
148+
extra = actual_files - expected_files
149+
if missing:
150+
errors.append('%s: missing binaries: %s' %
151+
(platform, ', '.join(sorted(missing))))
152+
if extra:
153+
errors.append('%s: unexpected binaries: %s' %
154+
(platform, ', '.join(sorted(extra))))
47155

48-
def verify_git_state(expected_version):
156+
sdk_dir = os.path.join(tmpdir, 'sdk')
157+
if os.path.isdir(sdk_dir):
158+
actual_sdk = set(os.listdir(sdk_dir))
159+
expected_sdk = set(_SDK_ZIPS)
160+
missing_sdk = expected_sdk - actual_sdk
161+
extra_sdk = actual_sdk - expected_sdk
162+
if missing_sdk:
163+
errors.append('sdk: missing zips: %s' % ', '.join(sorted(missing_sdk)))
164+
if extra_sdk:
165+
errors.append('sdk: unexpected files: %s' % ', '.join(sorted(extra_sdk)))
166+
167+
if errors:
168+
print('\n'.join('ERROR: ' + e for e in errors), file=sys.stderr)
169+
print(
170+
'\nThe LUCI artifact manifest in this script is out of sync with '
171+
'infra/luci/recipes/perfetto.py. Update _ARTIFACTS / _PLATFORMS / '
172+
'_SDK_ZIPS above and re-run.',
173+
file=sys.stderr)
174+
return False
175+
176+
print('✓ All expected LUCI artifacts present across %d platforms + sdk.' %
177+
len(_PLATFORMS))
178+
return True
179+
180+
181+
def verify_git_state(expected_version, assume_yes=False):
49182
"""Verifies git is on the correct tag with no uncommitted changes."""
50183
warnings = []
51184

52-
# Check for uncommitted changes
53185
try:
54186
result = subprocess.run(['git', 'status', '--porcelain'],
55187
capture_output=True,
@@ -60,7 +192,6 @@ def verify_git_state(expected_version):
60192
except Exception as e:
61193
warnings.append(f'Could not check git status: {e}')
62194

63-
# Check current tag
64195
try:
65196
result = subprocess.run(['git', 'describe', '--exact-match', '--tags'],
66197
capture_output=True,
@@ -79,63 +210,60 @@ def verify_git_state(expected_version):
79210
print('WARNING: SDK sources may not match the release tag:')
80211
for warning in warnings:
81212
print(f' - {warning}')
213+
if assume_yes:
214+
print('\n--yes passed; proceeding despite warnings.')
215+
return True
82216
return input('\nContinue anyway? [y/N] ').lower().strip() in ['y', 'yes']
83217

84218
print(f'✓ On tag {expected_version} with clean working directory')
85219
return True
86220

87221

88-
def download_sdk_sources(tmpdir, version):
89-
"""Downloads SDK source zips from GCS."""
90-
sdk_zips = [
91-
'perfetto-cpp-sdk-src.zip',
92-
'perfetto-c-sdk-src.zip',
93-
]
94-
95-
print('\n--- Downloading SDK amalgamated sources from GCS ---')
96-
for zip_name in sdk_zips:
97-
url = f'gs://perfetto-luci-artifacts/{version}/sdk/{zip_name}'
98-
exec('gsutil', 'cp', url, os.path.join(tmpdir, zip_name))
99-
100-
return sdk_zips
101-
102-
103222
def main():
104223
parser = argparse.ArgumentParser(epilog='Example: %s v19.0' % __file__)
105224
parser.add_argument('version', help='Version tag (e.g., v20.0)')
106-
225+
parser.add_argument(
226+
'--yes',
227+
action='store_true',
228+
help='Skip all interactive confirmations (for CI use).')
107229
args = parser.parse_args()
108230

109-
# Verify we're on the correct tag with no uncommitted changes
110-
if not verify_git_state(args.version):
231+
if not verify_git_state(args.version, assume_yes=args.yes):
111232
print('Aborted.')
112233
return 1
113234

114235
tmpdir = '/tmp/perfetto-%s-github-release' % args.version
115236
src = 'gs://perfetto-luci-artifacts/%s/' % args.version
116237
os.makedirs(tmpdir, exist_ok=True)
117238

118-
# Download and package prebuilts
119239
print('--- Downloading prebuilts from GCS ---')
120240
os.chdir(tmpdir)
121241
exec('gsutil', '-m', 'rsync', '-rc', src, tmpdir + '/')
122242

243+
print('\n--- Verifying artifact manifest ---')
244+
if not verify_downloads(tmpdir):
245+
return 1
246+
123247
zips = []
124-
for arch in os.listdir(tmpdir):
248+
for arch in sorted(os.listdir(tmpdir)):
125249
if arch == 'sdk' or not os.path.isdir(os.path.join(tmpdir, arch)):
126250
continue
127251
exec('zip', '-9r', '%s.zip' % arch, arch)
128252
zips.append(arch + '.zip')
129253

130-
# Download SDK source zips
131-
sdk_zips = download_sdk_sources(tmpdir, args.version)
132-
zips.extend(sdk_zips)
254+
# SDK zips already landed under sdk/ via the rsync above; just move them
255+
# up so everything ready to upload sits in tmpdir's root.
256+
for zip_name in _SDK_ZIPS:
257+
src_path = os.path.join(tmpdir, 'sdk', zip_name)
258+
dst_path = os.path.join(tmpdir, zip_name)
259+
os.rename(src_path, dst_path)
260+
zips.append(zip_name)
133261

134262
print('')
135263
print('=' * 70)
136264
print('%d zip files saved in %s' % (len(zips), tmpdir))
137-
print('Prebuilt binaries: %d' % (len(zips) - len(sdk_zips)))
138-
print('SDK sources: %d' % len(sdk_zips))
265+
print('Prebuilt binaries: %d' % (len(zips) - len(_SDK_ZIPS)))
266+
print('SDK sources: %d' % len(_SDK_ZIPS))
139267
print('Files: %s' % ', '.join(sorted(zips)))
140268
print('=' * 70)
141269

0 commit comments

Comments
 (0)