Skip to content

Commit 5669489

Browse files
committed
feat(cicd): Implement fallback logic for missing component artifacts and enhance version reporting
1 parent b3c7d7f commit 5669489

File tree

3 files changed

+277
-24
lines changed

3 files changed

+277
-24
lines changed

.github/workflows/build_components.yml

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ jobs:
198198
needs: [build-light, build-heavy]
199199
runs-on: ubuntu-latest
200200
if: always() && (github.event_name != 'schedule' || github.ref == 'refs/heads/update/components')
201-
continue-on-error: true
201+
continue-on-error: ${{ github.ref != 'refs/heads/main' }}
202202
steps:
203203

204204
# Clone Repository (pull_request_target)
@@ -231,7 +231,60 @@ jobs:
231231
fi
232232
done
233233
234+
- name: Generate a token for Rekku
235+
id: generate-rekku-token
236+
uses: actions/create-github-app-token@v1
237+
with:
238+
app-id: ${{ vars.REKKU_APP_ID }}
239+
private-key: ${{ secrets.REKKU_PRIVATE_KEY }}
240+
repositories: "components"
241+
owner: "RetroDECK"
242+
243+
# This will generate desired_versions.sh based on the current state of the components
244+
# Useful to "lock" the versions in future builds or to bring it to main (manually)
245+
- name: Generate desired_versions.sh (snapshot)
246+
env:
247+
GITHUB_TOKEN: ${{ steps.generate-rekku-token.outputs.token }}
248+
run: |
249+
set -e
250+
automation-tools/alchemist/generate_desired_versions.sh -f automation-tools/alchemist/desired_versions.sh
251+
latest_generated="$(ls -1t automation-tools/alchemist/desired_versions_*.sh | head -n 1)"
252+
if [ -z "$latest_generated" ] || [ ! -f "$latest_generated" ]; then
253+
echo "[ERROR] desired_versions generator did not produce an output file" >&2
254+
exit 1
255+
fi
256+
cp -f "$latest_generated" desired_versions.sh
257+
258+
- name: Fallback missing component artifacts from releases (non-main)
259+
if: github.ref != 'refs/heads/main'
260+
env:
261+
GITHUB_TOKEN: ${{ steps.generate-rekku-token.outputs.token }}
262+
GITHUB_REPOSITORY: ${{ github.repository }}
263+
MATCH_LABEL: cooker
264+
FALLBACK_COMPONENTS_FILE: fallback_components.txt
265+
MISSING_COMPONENTS_FILE: missing_components.txt
266+
run: |
267+
bash automation-tools/fallback_release_artifacts.sh
268+
269+
- name: Fail on missing artifacts (main only)
270+
if: github.ref == 'refs/heads/main'
271+
run: |
272+
set -e
273+
MISSING_ARTIFACTS=()
274+
for folder in $(find . -maxdepth 2 -mindepth 2 -type f \( -name 'component_recipe.json' -o -name 'component_manifest.json' \) -printf '%h\n' | sed 's|^\./||' | sort -u); do
275+
if [ ! -d "$folder/artifacts" ] || ! find "$folder/artifacts" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.gz" -o -name "*.tar" -o -name "*.7z" -o -name "*.appimage" \) | grep -q .; then
276+
MISSING_ARTIFACTS+=("$folder")
277+
fi
278+
done
279+
if [ ${#MISSING_ARTIFACTS[@]} -ne 0 ]; then
280+
echo "[ERROR] Missing artifacts on main; failing build. Missing:" >&2
281+
printf '%s\n' "${MISSING_ARTIFACTS[@]}" >&2
282+
exit 1
283+
fi
284+
234285
- name: Generate components_version_list.md
286+
env:
287+
FALLBACK_COMPONENTS_FILE: fallback_components.txt
235288
run: |
236289
# Loop through each <component_name> folder
237290
for component_dir in */; do
@@ -240,6 +293,11 @@ jobs:
240293
continue
241294
fi
242295
296+
# Only process real components
297+
if [[ ! -f "${component_dir}component_recipe.json" && ! -f "${component_dir}component_manifest.json" ]]; then
298+
continue
299+
fi
300+
243301
# Path to artifacts directory
244302
artifacts_dir="${component_dir}artifacts"
245303
@@ -280,15 +338,6 @@ jobs:
280338
281339
# Show the resulting components_version_list.md file
282340
cat components_version_list.md
283-
- name: Generate a token for Rekku
284-
id: generate-rekku-token
285-
uses: actions/create-github-app-token@v1
286-
with:
287-
app-id: ${{ vars.REKKU_APP_ID }}
288-
private-key: ${{ secrets.REKKU_PRIVATE_KEY }}
289-
repositories: "components"
290-
owner: "RetroDECK"
291-
292341
- name: Get Branch Name
293342
run: |
294343
if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "pull_request_target" ]]; then
@@ -348,6 +397,16 @@ jobs:
348397
echo "This is a RetroDECK Components Artifacts release from [this commit](https://github.com/${{ github.repository }}/commit/${{ github.sha }}), from branch [${{ env.BRANCH_NAME }}](https://github.com/RetroDECK/RetroDECK/tree/feat/${{ env.BRANCH_NAME }})." >> "$RELEASE_BODY_FILE"
349398
echo "" >> "$RELEASE_BODY_FILE"
350399
400+
# Fallback warnings (if any)
401+
if [ -f "fallback_components.txt" ] && [ -s "fallback_components.txt" ]; then
402+
echo "[WARNING] Fallback artifacts were used for the following components:" >> "$RELEASE_BODY_FILE"
403+
while IFS='|' read -r comp tag; do
404+
[ -z "$comp" ] && continue
405+
echo "- $comp (from release $tag)" >> "$RELEASE_BODY_FILE"
406+
done < fallback_components.txt
407+
echo "" >> "$RELEASE_BODY_FILE"
408+
fi
409+
351410
# Append the contents of components_version_list.md to the release body
352411
cat components_version_list.md >> "$RELEASE_BODY_FILE"
353412
echo "" >> "$RELEASE_BODY_FILE"
@@ -356,7 +415,7 @@ jobs:
356415
MISSING_ARTIFACTS=()
357416
358417
# Iterate through folders in the repo root
359-
for folder in $(find . -maxdepth 1 -mindepth 1 -type d -not -name '.*' -not -name 'automation-tools' -not -name 'downloaded-artifacts' -exec basename {} \;); do
418+
for folder in $(find . -maxdepth 2 -mindepth 2 -type f \( -name 'component_recipe.json' -o -name 'component_manifest.json' \) -printf '%h\n' | sed 's|^\./||' | sort -u); do
360419
if [ ! -d "$folder/artifacts" ] || ! find "$folder/artifacts" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.gz" -o -name "*.tar" -o -name "*.7z" -o -name "*.appimage" \) | grep -q .; then
361420
MISSING_ARTIFACTS+=("$folder")
362421
fi
@@ -420,7 +479,7 @@ jobs:
420479
tag: "${{ env.TAG }}"
421480
name: "RetroDECK Components ${{ env.TAG }}"
422481
body: ${{ steps.generate-body.outputs.RELEASE_BODY }}
423-
artifacts: "*/artifacts/*,!*/artifacts/component_version,components_version_list.md"
482+
artifacts: "*/artifacts/*,!*/artifacts/component_version,components_version_list.md,desired_versions.sh"
424483
allowUpdates: true
425484
omitBodyDuringUpdate: true
426485
makeLatest: ${{ env.MAKE_LATEST }}
@@ -456,6 +515,16 @@ jobs:
456515
echo "## RetroDECK Components Artifacts" > pr_body.md
457516
echo "" >> pr_body.md
458517
echo "This pull request updates the RetroDECK components artifacts to version ${{ env.TAG }}." >> pr_body.md
518+
echo "" >> pr_body.md
519+
520+
if [ -f "fallback_components.txt" ] && [ -s "fallback_components.txt" ]; then
521+
echo "### WARNING: Fallback artifacts used" >> pr_body.md
522+
while IFS='|' read -r comp tag; do
523+
[ -z "$comp" ] && continue
524+
echo "- $comp (from release $tag)" >> pr_body.md
525+
done < fallback_components.txt
526+
echo "" >> pr_body.md
527+
fi
459528
460529
echo "## Changes:" >> pr_body.md
461530
echo "$(cat commits_list.txt)" >> pr_body.md
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Fallback logic:
6+
# - For each component missing artifacts, try to download its latest available artifact from GitHub Releases.
7+
# - Search releases from newest to oldest and stop at the first match.
8+
# - Intended to be used ONLY for non-main branches (workflow should gate this).
9+
10+
log() {
11+
local level="$1"; shift
12+
echo "[$level] $*" >&2
13+
}
14+
15+
require_cmd() {
16+
command -v "$1" >/dev/null 2>&1 || {
17+
log ERROR "Missing required command: $1"
18+
exit 1
19+
}
20+
}
21+
22+
require_cmd curl
23+
require_cmd jq
24+
25+
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
26+
GITHUB_REPOSITORY="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required (e.g. RetroDECK/components)}"
27+
28+
# Prefer an explicit token if provided; else unauthenticated requests.
29+
AUTH_HEADER=()
30+
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
31+
AUTH_HEADER=(-H "Authorization: token ${GITHUB_TOKEN}")
32+
fi
33+
34+
# Label used to select releases (default: cooker). Keep aligned with workflow tagging.
35+
MATCH_LABEL="${MATCH_LABEL:-cooker}"
36+
37+
FALLBACK_OUT_FILE="${FALLBACK_COMPONENTS_FILE:-fallback_components.txt}"
38+
MISSING_OUT_FILE="${MISSING_COMPONENTS_FILE:-missing_components.txt}"
39+
40+
cd "$REPO_ROOT"
41+
42+
detect_missing_components() {
43+
local missing=()
44+
while IFS= read -r folder; do
45+
[[ -z "$folder" ]] && continue
46+
if [[ ! -d "$folder" ]]; then
47+
continue
48+
fi
49+
50+
# Only treat directories as components if they have a component recipe/manifest.
51+
if [[ ! -f "$folder/component_recipe.json" && ! -f "$folder/component_manifest.json" ]]; then
52+
continue
53+
fi
54+
55+
# Must have at least one artifact file to be considered present
56+
if [[ ! -d "$folder/artifacts" ]] || ! find "$folder/artifacts" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.gz" -o -name "*.tar" -o -name "*.7z" -o -name "*.appimage" \) | grep -q .; then
57+
missing+=("$folder")
58+
fi
59+
done < <(find . -maxdepth 1 -mindepth 1 -type d -not -name '.*' -exec basename {} \;)
60+
61+
printf '%s\n' "${missing[@]:-}"
62+
}
63+
64+
fetch_releases_page() {
65+
local page="$1"
66+
local url="https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100&page=${page}"
67+
curl -fsSL "${AUTH_HEADER[@]}" "$url"
68+
}
69+
70+
find_asset_in_releases() {
71+
local releases_json="$1"
72+
local component="$2"
73+
local asset_name="$3"
74+
75+
# Return a TSV line: <tag_name>\t<download_url> for the FIRST match
76+
jq -r --arg label "$MATCH_LABEL" --arg name "$asset_name" '
77+
.[]
78+
| select(.tag_name | test($label))
79+
| . as $rel
80+
| ($rel.assets // [])[]?
81+
| select(.name == $name)
82+
| [$rel.tag_name, .browser_download_url]
83+
| @tsv
84+
' <<<"$releases_json" | head -n 1
85+
}
86+
87+
download_asset() {
88+
local url="$1"
89+
local out="$2"
90+
mkdir -p "$(dirname "$out")"
91+
curl -fL "${AUTH_HEADER[@]}" -o "$out" "$url"
92+
}
93+
94+
main() {
95+
: >"$FALLBACK_OUT_FILE"
96+
: >"$MISSING_OUT_FILE"
97+
98+
mapfile -t missing_components < <(detect_missing_components)
99+
if [[ ${#missing_components[@]} -eq 0 ]]; then
100+
log INFO "No missing components detected."
101+
return 0
102+
fi
103+
104+
log WARN "Missing components detected: ${missing_components[*]}"
105+
printf '%s\n' "${missing_components[@]}" >"$MISSING_OUT_FILE"
106+
107+
for component in "${missing_components[@]}"; do
108+
local_tar_name="${component}.tar.gz"
109+
local_sha_name="${component}.tar.gz.sha"
110+
111+
found_tag=""
112+
url_tar=""
113+
url_sha=""
114+
115+
for page in 1 2 3 4 5; do
116+
releases_json=""
117+
if ! releases_json=$(fetch_releases_page "$page" 2>/dev/null); then
118+
log WARN "Failed to fetch releases page ${page}; continuing..."
119+
continue
120+
fi
121+
122+
# Stop if API returned an empty array
123+
if [[ "$(jq -r 'length' <<<"$releases_json")" == "0" ]]; then
124+
break
125+
fi
126+
127+
tar_match=$(find_asset_in_releases "$releases_json" "$component" "$local_tar_name" || true)
128+
sha_match=$(find_asset_in_releases "$releases_json" "$component" "$local_sha_name" || true)
129+
130+
if [[ -n "$tar_match" ]]; then
131+
found_tag=$(cut -f1 <<<"$tar_match")
132+
url_tar=$(cut -f2 <<<"$tar_match")
133+
fi
134+
if [[ -n "$sha_match" ]]; then
135+
# Prefer the tag from tar_match if present; else from sha_match
136+
[[ -z "$found_tag" ]] && found_tag=$(cut -f1 <<<"$sha_match")
137+
url_sha=$(cut -f2 <<<"$sha_match")
138+
fi
139+
140+
if [[ -n "$url_tar" ]]; then
141+
break
142+
fi
143+
done
144+
145+
if [[ -z "$url_tar" ]]; then
146+
log ERROR "Fallback not found for component '$component' (searched releases matching '${MATCH_LABEL}')."
147+
continue
148+
fi
149+
150+
log WARN "Using fallback for '$component' from release '$found_tag'"
151+
download_asset "$url_tar" "$REPO_ROOT/$component/artifacts/$local_tar_name"
152+
if [[ -n "$url_sha" ]]; then
153+
download_asset "$url_sha" "$REPO_ROOT/$component/artifacts/$local_sha_name"
154+
fi
155+
156+
echo "${component}|${found_tag}" >>"$FALLBACK_OUT_FILE"
157+
done
158+
159+
if [[ -s "$FALLBACK_OUT_FILE" ]]; then
160+
log INFO "Fallback used for: $(cut -d'|' -f1 "$FALLBACK_OUT_FILE" | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
161+
else
162+
log INFO "No fallback artifacts were recovered."
163+
fi
164+
}
165+
166+
main "$@"

0 commit comments

Comments
 (0)