1+ #! /usr/bin/env bash
2+ #
3+ # fetch-roachtest-artifacts - Fetch artifacts for a roachtest issue
4+ #
5+ set -euo pipefail
6+
7+ usage () {
8+ echo " Usage: $0 <issue-number-or-url-or-teamcity-url>"
9+ echo " "
10+ echo " Examples:"
11+ echo " $0 12345"
12+ echo " $0 https://github.com/cockroachdb/cockroach/issues/12345"
13+ echo " $0 https://github.com/cockroachdb/cockroach/issues/12345#issuecomment-67890"
14+ echo " $0 'https://teamcity.cockroachdb.com/buildConfiguration/Cockroach_Nightlies_RoachtestWeeklyBazel/20321836?buildTab=artifacts#/c2c/import/7tb/kv0'"
15+ exit 1
16+ }
17+
18+ # URL encode special characters for TeamCity API (but not slashes)
19+ url_encode () {
20+ local string=" $1 "
21+ echo " $string " | sed ' s/=/%3D/g; s/ /%20/g'
22+ }
23+
24+ # Recursively download artifacts from TeamCity
25+ download_artifacts_recursive () {
26+ local build_id=" $1 "
27+ local path=" $2 "
28+ local dest_dir=" $3 "
29+ local depth=" ${4:- 0} "
30+
31+ # Prevent infinite recursion
32+ [[ $depth -gt 10 ]] && return
33+
34+ # Get children of current path
35+ local encoded_path
36+ encoded_path=$( url_encode " $path " )
37+ local children_xml
38+ children_xml=$( curl -s " https://teamcity.cockroachdb.com/guestAuth/app/rest/builds/id:$build_id /artifacts/children/$encoded_path /" 2> /dev/null)
39+
40+ # Debug output
41+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
42+ echo " DEBUG: Checking path: $path (encoded: $encoded_path )"
43+ echo " DEBUG: XML response: ${children_xml: 0: 200} ..."
44+ fi
45+
46+ # Check if we got valid XML
47+ if [[ -z " $children_xml " || " $children_xml " == * " <error" * || " $children_xml " == * " HTTP 400" * || " $children_xml " == * " status code: 400" * || " $children_xml " == * " 404" * || " $children_xml " == * " Build not found" * ]]; then
48+ # If this is the root path (depth 0), it's a fatal error
49+ if [[ $depth -eq 0 ]]; then
50+ echo " Error: Build $build_id not found or path $path does not exist"
51+ exit 1
52+ fi
53+ return
54+ fi
55+
56+ # Extract all items (files and directories)
57+ local items
58+ items=$( echo " $children_xml " | grep -o ' name="[^"]*"' | cut -d' "' -f2)
59+
60+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
61+ echo " DEBUG: Found items: $items "
62+ fi
63+
64+ [[ -z " $items " ]] && return
65+
66+ local pids=()
67+ while read -r item; do
68+ [[ -z " $item " ]] && continue
69+
70+ local item_path=" $path /$item "
71+ local encoded_item_path
72+ encoded_item_path=$( url_encode " $item_path " )
73+
74+ # Check if this item has children (is a directory) first
75+ local item_children
76+ item_children=$( curl -s " https://teamcity.cockroachdb.com/guestAuth/app/rest/builds/id:$build_id /artifacts/children/$encoded_item_path /" 2> /dev/null)
77+
78+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
79+ echo " DEBUG: Checking if $item is directory. Response: ${item_children: 0: 100} ..."
80+ fi
81+
82+ if [[ -n " $item_children " && " $item_children " != * " <error" * && " $item_children " != * " HTTP 400" * && " $item_children " != * " status code: 400" * && " $item_children " != * " status code: 404" * ]] && [[ " $item " != * .zip ]]; then
83+ # It's a directory (not a zip file), recurse into it
84+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
85+ echo " DEBUG: Recursing into directory: $item "
86+ fi
87+ download_artifacts_recursive " $build_id " " $item_path " " $dest_dir " $(( depth + 1 ))
88+ elif [[ " $item " == * .* ]]; then
89+ # It's a file, download it
90+ (
91+ # Create subdirectory structure relative to the original path
92+ local relative_path=" ${item_path# $2 / } " # Remove the root path prefix
93+ local file_dir=" $dest_dir /$( dirname " $relative_path " ) "
94+ mkdir -p " $file_dir "
95+ local file_path=" $dest_dir /$relative_path "
96+
97+ local file_url=" https://teamcity.cockroachdb.com/guestAuth/app/rest/builds/id:$build_id /artifacts/content/$encoded_item_path "
98+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
99+ echo " DEBUG: Downloading $file_url to $file_path "
100+ fi
101+ if curl -f -s " $file_url " -o " $file_path " 2> /dev/null && [[ -s " $file_path " ]]; then
102+ echo " Downloaded: $relative_path "
103+
104+ # Only unzip actual zip files
105+ if [[ " $item " == * .zip ]] && file " $file_path " | grep -q " Zip archive" ; then
106+ local extract_dir=" ${file_path% .zip} "
107+ mkdir -p " $extract_dir "
108+ (cd " $extract_dir " && unzip -o -q " ../$( basename " $file_path " ) " )
109+
110+ # Flatten if archive contains single directory
111+ local contents=($( ls " $extract_dir " ) )
112+ if [[ ${# contents[@]} -eq 1 && -d " $extract_dir /${contents[0]} " ]]; then
113+ mv " $extract_dir /${contents[0]} " /* " $extract_dir /" 2> /dev/null || true
114+ rmdir " $extract_dir /${contents[0]} " 2> /dev/null || true
115+ fi
116+
117+ rm " $file_path "
118+ fi
119+ fi
120+ ) &
121+ pids+=($! )
122+ else
123+ # It's a file without an extension, download it
124+ (
125+ # Create subdirectory structure relative to the original path
126+ local relative_path=" ${item_path# $2 / } " # Remove the root path prefix
127+ local file_dir=" $dest_dir /$( dirname " $relative_path " ) "
128+ mkdir -p " $file_dir "
129+ local file_path=" $dest_dir /$relative_path "
130+
131+ local file_url=" https://teamcity.cockroachdb.com/guestAuth/app/rest/builds/id:$build_id /artifacts/content/$encoded_item_path "
132+ if [[ -n " ${ROACHTEST_DEBUG:- } " ]]; then
133+ echo " DEBUG: Downloading $file_url to $file_path "
134+ fi
135+ if curl -f -s " $file_url " -o " $file_path " 2> /dev/null && [[ -s " $file_path " ]]; then
136+ echo " Downloaded: $relative_path "
137+ fi
138+ ) &
139+ pids+=($! )
140+ fi
141+ done <<< " $items"
142+
143+ # Wait for all downloads to complete
144+ if [[ ${# pids[@]} -gt 0 ]]; then
145+ for pid in " ${pids[@]} " ; do
146+ wait " $pid "
147+ done
148+ fi
149+ }
150+
151+ # Handle direct TeamCity artifact URLs
152+ handle_teamcity_url () {
153+ local tc_url=" $1 "
154+
155+ # Extract build ID and path from TeamCity URL
156+ local build_id path
157+ build_id=$( echo " $tc_url " | grep -o ' [0-9]\+' | head -1)
158+
159+ if [[ " $tc_url " == * " #/" * ]]; then
160+ path=$( echo " $tc_url " | sed ' s/.*#\///' )
161+ else
162+ echo " Error: TeamCity URL must include a path after #/"
163+ echo " Expected format: https://teamcity.cockroachdb.com/.../BUILD_ID?buildTab=artifacts#/path"
164+ echo " Got: $tc_url "
165+ exit 1
166+ fi
167+
168+ if [[ -z " $build_id " || -z " $path " ]]; then
169+ echo " Error: Could not extract build ID or path from TeamCity URL: $tc_url "
170+ echo " Expected format: https://teamcity.cockroachdb.com/.../BUILD_ID?buildTab=artifacts#/path"
171+ exit 1
172+ fi
173+
174+ # Extract test name from path for directory naming
175+ local test_name
176+ test_name=$( echo " $path " | tr ' /' ' -' | tr -cd ' a-zA-Z0-9_-' )
177+ test_name=" ${test_name:- unknown} "
178+
179+ # Create artifacts directory
180+ local artifacts_dir=" artifacts/teamcity-${test_name} -${build_id} "
181+ mkdir -p " $artifacts_dir "
182+ echo " Fetching TeamCity artifacts from build $build_id path $path "
183+ echo " Downloading to $artifacts_dir "
184+
185+ # Download artifacts
186+ download_artifacts_recursive " $build_id " " $path " " $artifacts_dir "
187+
188+ # Verify at least artifacts.zip was downloaded (could be nested in subdirectories)
189+ if ! find " $artifacts_dir " -name " artifacts.zip" -o -name " artifacts" -type d | grep -q . ; then
190+ echo " Error: Failed to download artifacts.zip or extract artifacts directory"
191+ echo " Build $build_id may not have artifacts at path $path , or artifacts may have expired"
192+ exit 1
193+ fi
194+
195+ echo " TeamCity artifacts successfully downloaded to $artifacts_dir "
196+ exit 0
197+ }
198+
199+ main () {
200+ [[ $# -eq 0 || " $1 " == " --help" ]] && usage
201+
202+ # Check if this is a direct TeamCity URL
203+ if [[ " $1 " == * " teamcity.cockroachdb.com" * ]]; then
204+ if [[ " $1 " == * " buildTab=artifacts" * ]]; then
205+ handle_teamcity_url " $1 "
206+ return
207+ else
208+ echo " Error: TeamCity URL must include 'buildTab=artifacts'"
209+ echo " Expected format: https://teamcity.cockroachdb.com/.../BUILD_ID?buildTab=artifacts#/path"
210+ exit 1
211+ fi
212+ fi
213+
214+ # Extract issue number and comment ID (if present)
215+ local issue_number comment_id=" "
216+
217+ if [[ " $1 " == * " /issues/" * ]]; then
218+ # GitHub URL: extract path after github.com/cockroachdb/cockroach/
219+ local path=" ${1#* github.com/ cockroachdb/ cockroach/ } "
220+ if [[ " $path " == issues/* ]]; then
221+ # Remove issues/ prefix and split on #
222+ local remainder=" ${path# issues/ } "
223+ issue_number=" ${remainder%%#* } "
224+
225+ # Check for comment ID
226+ if [[ " $remainder " == * " #issuecomment-" * ]]; then
227+ comment_id=" ${remainder#*# issuecomment-} "
228+ fi
229+ fi
230+ else
231+ # Just a number: 12345
232+ issue_number=" ${1##*/ } "
233+ issue_number=" ${issue_number% )* } "
234+ issue_number=" ${issue_number%#* } "
235+ fi
236+
237+ [[ ! " $issue_number " =~ ^[0-9]+$ ]] && { echo " Error: Invalid issue number from '$1 '" ; exit 1; }
238+
239+ # Get issue data from GitHub API
240+ local issue_data
241+ issue_data=$( curl -s " https://api.github.com/repos/cockroachdb/cockroach/issues/$issue_number " ) || {
242+ echo " Error: Failed to fetch issue #$issue_number "
243+ exit 1
244+ }
245+
246+ # Check if issue exists
247+ if echo " $issue_data " | jq -e ' .message // empty' > /dev/null 2>&1 ; then
248+ echo " Error: $( echo " $issue_data " | jq -r ' .message' ) "
249+ exit 1
250+ fi
251+
252+ # Extract info using jq and basic string ops
253+ local title body url test_name
254+ title=$( echo " $issue_data " | jq -r ' .title' )
255+ url=$( echo " $issue_data " | jq -r ' .html_url' )
256+
257+ # Get body from specific comment or issue
258+ if [[ -n " $comment_id " ]]; then
259+ echo " Fetching comment #$comment_id from issue #$issue_number "
260+ local comment_data
261+ comment_data=$( curl -s " https://api.github.com/repos/cockroachdb/cockroach/issues/comments/$comment_id " ) || {
262+ echo " Error: Failed to fetch comment #$comment_id "
263+ exit 1
264+ }
265+
266+ if echo " $comment_data " | jq -e ' .message // empty' > /dev/null 2>&1 ; then
267+ echo " Error: $( echo " $comment_data " | jq -r ' .message' ) "
268+ exit 1
269+ fi
270+
271+ body=$( echo " $comment_data " | jq -r ' .body' )
272+ url=" ${url} #issuecomment-${comment_id} "
273+ else
274+ echo " Fetching issue #$issue_number "
275+ body=$( echo " $issue_data " | jq -r ' .body' )
276+ fi
277+ test_name=$( echo " $title " | sed -n ' s/.*roachtest[: ]*\([^ ]*\) .*/\1/p' | head -1)
278+ test_name=" ${test_name:- unknown} "
279+ test_name=$( echo " $test_name " | tr ' /' ' -' | tr -cd ' a-zA-Z0-9_-=' )
280+
281+ # Create artifacts directory
282+ local artifacts_dir=" artifacts/roachtest-${test_name} -${issue_number} "
283+ if [[ -n " $comment_id " ]]; then
284+ artifacts_dir=" artifacts/roachtest-${test_name} -${issue_number} -comment-${comment_id} "
285+ fi
286+ mkdir -p " $artifacts_dir "
287+ echo " fetching to $artifacts_dir "
288+
289+ # Find and download TeamCity artifacts
290+ local downloaded_files=()
291+ while read -r tc_url; do
292+ [[ -z " $tc_url " || ! " $tc_url " =~ buildTab= artifacts ]] && continue
293+
294+ # Transform: buildConfiguration/.../ID?buildTab=artifacts#/path → API download
295+ local build_id path
296+ build_id=$( echo " $tc_url " | grep -o ' [0-9]\+' | head -1)
297+ path=$( echo " $tc_url " | sed ' s/.*#\///' )
298+
299+ if [[ -n " $build_id " && -n " $path " ]]; then
300+ download_artifacts_recursive " $build_id " " $path " " $artifacts_dir "
301+ downloaded_files+=(" $artifacts_dir " )
302+ fi
303+ done <<< " $(echo " $body " | grep -o 'https://teamcity[^[:space:]]*' | sed 's/)$//' || true)"
304+
305+ # Output summary and verification
306+ # Always verify artifacts were actually downloaded, regardless of downloaded_files array
307+ if ! find " $artifacts_dir " -name " artifacts.zip" -o -name " artifacts" -type d | grep -q . ; then
308+ if [ ${# downloaded_files[@]} -gt 0 ]; then
309+ echo " Error: #$issue_number - $title : failed to download artifacts.zip"
310+ echo " TeamCity artifacts may have expired or the build may not contain test artifacts"
311+ else
312+ echo " Error: #$issue_number - $title : no artifacts found"
313+ echo " This could happen if:"
314+ echo " - TeamCity artifacts have expired"
315+ echo " - Issue has no TeamCity links"
316+ echo " - URLs don't match expected format"
317+ fi
318+ exit 1
319+ fi
320+ echo " #$issue_number - $title : all artifacts downloaded to $artifacts_dir "
321+ }
322+
323+ main " $@ "
0 commit comments