Skip to content

Alchemic Circle: Build RetroDECK Components #319

Alchemic Circle: Build RetroDECK Components

Alchemic Circle: Build RetroDECK Components #319

name: "Alchemic Circle: Build RetroDECK Components"
on:
workflow_dispatch:
inputs:
branch_override:
description: 'Override branch (leave empty to use selected branch from dropdown)'
required: false
type: string
trigger_flatpak_build:
description: 'Trigger flatpak build after release (equivalent to [release] trigger)'
required: false
type: boolean
default: false
force_countertop:
description: 'Force countertop variant in flatpak build (implies [release] trigger)'
required: false
type: boolean
default: false
dry_run:
description: 'Dry run - build and validate but do not create release or trigger flatpak'
required: false
type: boolean
default: true
push:
branches:
- main
- cooker
# schedule:
# Weekly epicure build - Monday at 14:00 UTC
# - cron: '0 14 * * 1'
permissions:
contents: write
env:
FLATPAK_REPO: ${{ github.repository_owner }}/RetroDECK
jobs:
# ===========================================================================
# Check whether this event should trigger a build
# ===========================================================================
check-trigger:
name: Check Build Trigger
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
trigger_flatpak: ${{ steps.check.outputs.trigger_flatpak }}
is_dry_run: ${{ steps.check.outputs.is_dry_run }}
force_countertop: ${{ steps.check.outputs.force_countertop }}
steps:
- name: Checkout code
if: github.event_name == 'push'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Evaluate trigger
id: check
run: |
# Trigger words for commit message detection
BUILD_TRIGGER="build"
RELEASE_TRIGGER="release"
DRY_RUN_TRIGGER="dry-run"
COUNTERTOP_TRIGGER="countertop"
# --- Schedule trigger (epicure) ---
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "Scheduled trigger - epicure build"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=true" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
echo "force_countertop=false" >> $GITHUB_OUTPUT
exit 0
fi
# --- Manual dispatch ---
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "Manual trigger"
echo "should_build=true" >> $GITHUB_OUTPUT
if [[ "${{ inputs.force_countertop }}" == "true" ]]; then
echo "force_countertop=true" >> $GITHUB_OUTPUT
# Countertop implies release behavior
echo "trigger_flatpak=true" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
elif [[ "${{ inputs.dry_run }}" == "true" ]]; then
echo "force_countertop=false" >> $GITHUB_OUTPUT
echo "is_dry_run=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
else
echo "force_countertop=false" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
if [[ "${{ inputs.trigger_flatpak_build }}" == "true" ]]; then
echo "trigger_flatpak=true" >> $GITHUB_OUTPUT
else
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
fi
fi
exit 0
fi
# --- Push event ---
COMMITS=$(git log --format=%B ${{ github.event.before }}..${{ github.event.after }})
# Detect [countertop] flag in commits (can combine with other triggers)
FORCE_COUNTERTOP="false"
if echo "$COMMITS" | grep -qiE "\[${COUNTERTOP_TRIGGER}\]"; then
echo "Found [countertop] trigger in commit messages"
FORCE_COUNTERTOP="true"
fi
echo "force_countertop=$FORCE_COUNTERTOP" >> $GITHUB_OUTPUT
# PR merge commits always trigger a build (no trigger word needed)
if echo "$COMMITS" | grep -qiE "^Merge pull request #"; then
echo "PR merge detected - auto-building"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
exit 0
fi
# [countertop] implies release behavior (build + release + trigger flatpak)
if [[ "$FORCE_COUNTERTOP" == "true" ]]; then
echo "[countertop] implies release behavior"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=true" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
exit 0
fi
# [dry-run] -> build only, no release
if echo "$COMMITS" | grep -qiE "\[${DRY_RUN_TRIGGER}\]"; then
echo "Found [dry-run] trigger in commit messages"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
echo "is_dry_run=true" >> $GITHUB_OUTPUT
exit 0
fi
# [release] -> build + release + trigger flatpak
if echo "$COMMITS" | grep -qiE "\[${RELEASE_TRIGGER}\]"; then
echo "Found [release] trigger in commit messages"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=true" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
exit 0
fi
# [build] -> build + release only
if echo "$COMMITS" | grep -qiE "\[${BUILD_TRIGGER}\]"; then
echo "Found [build] trigger in commit messages"
echo "should_build=true" >> $GITHUB_OUTPUT
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
exit 0
fi
# No trigger found
echo "No build trigger found in commit messages - skipping"
echo "should_build=false" >> $GITHUB_OUTPUT
echo "trigger_flatpak=false" >> $GITHUB_OUTPUT
echo "is_dry_run=false" >> $GITHUB_OUTPUT
# ===========================================================================
# Determine build parameters and validate sources
# ===========================================================================
configure:
name: Configure Build and Validate Files
needs: check-trigger
if: needs.check-trigger.outputs.should_build == 'true'
runs-on: ubuntu-latest
outputs:
branch: ${{ steps.config.outputs.branch }}
is_dry_run: ${{ needs.check-trigger.outputs.is_dry_run }}
trigger_flatpak: ${{ needs.check-trigger.outputs.trigger_flatpak }}
force_countertop: ${{ needs.check-trigger.outputs.force_countertop }}
steps:
- name: Determine build parameters
id: config
run: |
# Determine branch
if [[ "${{ github.event_name }}" == "schedule" ]]; then
BRANCH="epicure"
elif [[ -n "${{ inputs.branch_override }}" ]]; then
BRANCH="${{ inputs.branch_override }}"
else
BRANCH="${{ github.ref_name }}"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo ""
echo "=========================================="
echo "Build Configuration"
echo "=========================================="
echo " Trigger: ${{ github.event_name }}"
echo " Branch: $BRANCH"
echo " Dry run: ${{ needs.check-trigger.outputs.is_dry_run }}"
echo " Trigger flatpak: ${{ needs.check-trigger.outputs.trigger_flatpak }}"
echo " Force countertop: ${{ needs.check-trigger.outputs.force_countertop }}"
echo "=========================================="
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ steps.config.outputs.branch }}
- name: Validate Bash scripts
run: |
echo "Validating Bash scripts..."
ERRORS=0
FILES=0
while IFS= read -r script; do
if ! bash -n "$script" 2>/dev/null; then
echo "FAILED: $script"
ERRORS=$((ERRORS + 1))
else
FILES=$((FILES + 1))
fi
done < <(find . -type f -name "*.sh")
if [[ "$ERRORS" -gt 0 ]]; then
echo "ERROR: $ERRORS script(s) failed syntax validation"
exit 1
fi
echo "$FILES Bash scripts passed syntax validation"
- name: Validate JSON files
run: |
echo "Validating JSON files..."
ERRORS=0
FILES=0
while IFS= read -r json; do
if ! jq empty "$json" 2>/dev/null; then
echo "FAILED: $json"
ERRORS=$((ERRORS + 1))
else
FILES=$((FILES + 1))
fi
done < <(find . -type f -name "*.json")
if [[ "$ERRORS" -gt 0 ]]; then
echo "ERROR: $ERRORS JSON file(s) failed validation"
exit 1
fi
echo "$FILES JSON files passed validation"
# ===========================================================================
# Auto-pin versions on main branch and sync epicure
# ===========================================================================
# When code is merged from cooker to main, this job:
# 1. Fetches the proposed version pins from the latest cooker release
# 2. Commits them as version_pins.sh to the main branch
# 3. Syncs the epicure branch from main with pins removed
#
# This ensures the main build uses reproducible pinned versions, and
# epicure stays in sync with main's code while resolving dynamically.
#
# On non-main branches, this job runs but all steps are skipped. This
# avoids the GitHub Actions issue where a job-level skip propagates
# through the dependency chain and causes downstream jobs to skip.
# ===========================================================================
auto-pin-versions:
name: Auto-Pin Versions and Sync Epicure (main only)
needs: configure
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check if pinning is needed
id: check
run: |
if [[ "${{ needs.configure.outputs.branch }}" == "main" ]]; then
echo "Main branch detected, version pinning and epicure sync will run"
echo "is_main=true" >> $GITHUB_OUTPUT
else
echo "Not on main branch, skipping version pinning and epicure sync"
echo "is_main=false" >> $GITHUB_OUTPUT
fi
- name: Checkout main
if: steps.check.outputs.is_main == 'true'
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Configure git identity
if: steps.check.outputs.is_main == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Fetch proposed pins from latest cooker release
id: fetch-pins
if: steps.check.outputs.is_main == 'true'
run: |
echo "Looking for latest cooker release with proposed version pins..."
LATEST_COOKER=$(gh release list --limit 50 --json tagName,createdAt \
-q '[.[] | select(.tagName | startswith("cooker-"))] | sort_by(.createdAt) | reverse | .[0].tagName')
if [[ -z "$LATEST_COOKER" || "$LATEST_COOKER" == "null" ]]; then
echo "No cooker release found"
echo "found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Latest cooker release: $LATEST_COOKER"
if gh release download "$LATEST_COOKER" -p "proposed_version_pins.sh" -O proposed_version_pins.sh 2>/dev/null; then
echo "Proposed version pins downloaded from $LATEST_COOKER"
echo "found=true" >> $GITHUB_OUTPUT
echo ""
echo "Proposed pins content:"
cat proposed_version_pins.sh
else
echo "No proposed_version_pins.sh found in release $LATEST_COOKER"
echo "found=false" >> $GITHUB_OUTPUT
fi
- name: Commit version pins to main
if: steps.check.outputs.is_main == 'true' && steps.fetch-pins.outputs.found == 'true'
run: |
# Compare with existing pins file if present
if [[ -f version_pins.sh ]] && diff -q version_pins.sh proposed_version_pins.sh &>/dev/null; then
echo "version_pins.sh is already up to date, no commit needed"
exit 0
fi
echo "Updating version_pins.sh..."
cp proposed_version_pins.sh version_pins.sh
git add version_pins.sh
git commit -m "Auto-pin component versions from latest cooker build"
git push
echo "version_pins.sh committed to main"
- name: Sync epicure branch from main
if: steps.check.outputs.is_main == 'true'
run: |
echo "Syncing epicure branch from main..."
# Reset epicure to match current main (including any pins commit above)
git checkout -B epicure origin/main
# Remove version pins so all components resolve dynamically
if [[ -f version_pins.sh ]]; then
git rm version_pins.sh
git commit -m "Remove version pins for epicure (dynamic resolution)"
else
echo "No version_pins.sh present, epicure is already unpinned"
fi
git push --force origin epicure
echo "Epicure branch synced from main (unpinned)"
# ===========================================================================
# Resolve component versions and detect reusable artifacts
# ===========================================================================
resolve-versions:
name: Resolve Component Versions
needs: [configure, auto-pin-versions]
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
outputs:
versions-json: ${{ steps.resolve.outputs.versions }}
prev-versions-json: ${{ steps.get-prev.outputs.prev_versions }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ needs.configure.outputs.branch }}
fetch-depth: 0
- name: Get previous release metadata
id: get-prev
run: |
BRANCH_NAME="${{ needs.configure.outputs.branch }}"
echo "Looking for previous release on branch: ${BRANCH_NAME}"
PREV_RELEASE=$(gh release list --limit 100 --json tagName \
-q "[.[] | select(.tagName | startswith(\"${BRANCH_NAME}-\"))] | .[0].tagName")
if [[ -z "$PREV_RELEASE" || "$PREV_RELEASE" == "null" ]]; then
echo "No previous release found for branch ${BRANCH_NAME}"
echo "prev_versions={}" >> $GITHUB_OUTPUT
exit 0
fi
echo "Previous release found: ${PREV_RELEASE}"
if gh release download "$PREV_RELEASE" -p "components_metadata.json" 2>/dev/null; then
PREV_METADATA=$(jq -c . < components_metadata.json)
echo "Previous metadata downloaded"
echo "prev_versions=$PREV_METADATA" >> $GITHUB_OUTPUT
echo "Previous component versions:"
echo "$PREV_METADATA" | jq -r 'to_entries[] | " \(.key): \(.value.version)"'
else
echo "No metadata found in previous release"
echo "prev_versions={}" >> $GITHUB_OUTPUT
fi
- name: Install build dependencies
if: matrix.can_reuse == false
run: |
echo "Installing build dependencies..."
unset pkg_mgr
for candidate in apt pacman dnf; do
command -v "$candidate" &> /dev/null && pkg_mgr="$candidate" && break
done
case "${pkg_mgr:-}" in
apt)
sudo add-apt-repository -y ppa:flatpak/stable
sudo apt update
sudo apt install -y flatpak flatpak-builder p7zip-full xmlstarlet bzip2 curl jq unzip
;;
pacman)
sudo pacman -Sy --noconfirm flatpak flatpak-builder p7zip xmlstarlet bzip2 curl jq unzip
;;
dnf)
sudo dnf install -y flatpak flatpak-builder p7zip p7zip-plugins xmlstarlet bzip2 curl jq unzip
;;
*)
echo "ERROR: No supported package manager found (apt, pacman, dnf)"
exit 1
;;
esac
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo
echo "Build dependencies installed"
- name: Resolve component versions
id: resolve
env:
PREV_VERSIONS: ${{ steps.get-prev.outputs.prev_versions }}
run: |
echo "Resolving component versions..."
metadata='{}'
for recipe in */component_recipe.json; do
[[ -f "$recipe" ]] || continue
component=$(basename "$(dirname "$recipe")")
component_dir=$(dirname "$recipe")
echo ""
echo "Processing $component..."
version=$(automation-tools/alchemist/alchemist.sh -f "$recipe" --resolve-versions)
if [[ -z "$version" ]]; then
echo "ERROR: Failed to resolve version for $component"
exit 1
fi
tree_hash=$(git rev-parse "HEAD:$component_dir")
# Carry forward previous built_on for reused components
prev_built_on=$(echo "$PREV_VERSIONS" | jq -r --arg c "$component" '.[$c].built_on // empty')
metadata=$(echo "$metadata" | jq \
--arg component "$component" \
--arg version "$version" \
--arg tree_hash "$tree_hash" \
--arg built_on "$prev_built_on" \
'.[$component] = {
version: $version,
git_tree_hash: $tree_hash,
built_on: $built_on
}')
echo " Version: $version"
echo " Tree hash: ${tree_hash:0:12}..."
done
echo ""
echo "Version resolution complete"
echo "versions=$(echo "$metadata" | jq -c .)" >> $GITHUB_OUTPUT
echo "$metadata" | jq . > components_metadata.json
echo ""
echo "Component versions:"
echo "$metadata" | jq -r 'to_entries[] | " \(.key): \(.value.version)"'
- name: Upload metadata
uses: actions/upload-artifact@v7
with:
name: components-metadata
path: components_metadata.json
retention-days: 1
- name: Generate proposed version pins
run: |
echo "Generating proposed version pins from resolved metadata..."
automation-tools/alchemist/generate_version_pins.sh -m components_metadata.json -o proposed_version_pins.sh
echo ""
echo "Proposed version pins:"
cat proposed_version_pins.sh
- name: Upload proposed version pins
uses: actions/upload-artifact@v7
with:
name: proposed-version-pins
path: proposed_version_pins.sh
retention-days: 90
# ===========================================================================
# Build the component matrix with reuse detection
# ===========================================================================
setup-matrix:
name: Setup Build Matrix
needs: [configure, resolve-versions]
runs-on: ubuntu-latest
outputs:
build-matrix: ${{ steps.set-matrix.outputs.build }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ needs.configure.outputs.branch }}
- name: Build component matrix
id: set-matrix
run: |
CURRENT_VERSIONS='${{ needs.resolve-versions.outputs.versions-json }}'
PREV_VERSIONS='${{ needs.resolve-versions.outputs.prev-versions-json }}'
echo "Current versions: $(echo "$CURRENT_VERSIONS" | jq 'keys | length') components"
echo "Previous versions: $(echo "$PREV_VERSIONS" | jq 'keys | length') components"
echo ""
recipes=$(
for recipe in */component_recipe.json; do
[[ -f "$recipe" ]] || continue
component=$(basename "$(dirname "$recipe")")
current_version=$(echo "$CURRENT_VERSIONS" | jq -r --arg c "$component" '.[$c].version // "unknown"')
current_tree=$(echo "$CURRENT_VERSIONS" | jq -r --arg c "$component" '.[$c].git_tree_hash // empty')
prev_version=$(echo "$PREV_VERSIONS" | jq -r --arg c "$component" '.[$c].version // empty')
prev_tree=$(echo "$PREV_VERSIONS" | jq -r --arg c "$component" '.[$c].git_tree_hash // empty')
can_reuse="false"
if [[ -n "$prev_version" && -n "$prev_tree" ]]; then
if [[ "$current_version" == "$prev_version" && "$current_tree" == "$prev_tree" ]]; then
can_reuse="true"
fi
fi
jq -n \
--arg recipe "$recipe" \
--arg component "$component" \
--arg version "$current_version" \
--arg can_reuse "$can_reuse" \
'{
recipe: $recipe,
component: $component,
version: $version,
can_reuse: ($can_reuse == "true")
}'
done | jq -s -c .
)
recipe_count=$(echo "$recipes" | jq 'length')
if [[ "$recipe_count" -eq 0 ]]; then
echo "ERROR: No component recipes found"
exit 1
fi
reuse_count=$(echo "$recipes" | jq '[.[] | select(.can_reuse == true)] | length')
build_count=$(echo "$recipes" | jq '[.[] | select(.can_reuse == false)] | length')
echo "Build matrix:"
echo "$recipes" | jq -r '.[] |
if .can_reuse then
" \(.component) \(.version) - REUSE"
else
" \(.component) \(.version) - BUILD"
end'
echo ""
echo "=========================================="
echo "Matrix Summary"
echo " Reuse: $reuse_count"
echo " Build: $build_count"
echo " Total: $recipe_count"
echo "=========================================="
echo "build=$recipes" >> $GITHUB_OUTPUT
# ===========================================================================
# Build each component (or reuse from previous release)
# ===========================================================================
build-components:
name: Build ${{ matrix.component }}
needs: [configure, setup-matrix]
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
strategy:
matrix:
include: ${{ fromJSON(needs.setup-matrix.outputs.build-matrix) }}
fail-fast: true
max-parallel: 20
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ needs.configure.outputs.branch }}
fetch-depth: 0
- name: Install build dependencies
if: matrix.can_reuse == false
run: |
echo "Installing build dependencies..."
unset pkg_mgr
for candidate in apt pacman dnf; do
command -v "$candidate" &> /dev/null && pkg_mgr="$candidate" && break
done
case "${pkg_mgr:-}" in
apt)
sudo add-apt-repository -y ppa:flatpak/stable
sudo apt update
sudo apt install -y flatpak flatpak-builder p7zip-full xmlstarlet bzip2 curl jq unzip
;;
pacman)
sudo pacman -Sy --noconfirm flatpak flatpak-builder p7zip xmlstarlet bzip2 curl jq unzip
;;
dnf)
sudo dnf install -y flatpak flatpak-builder p7zip p7zip-plugins xmlstarlet bzip2 curl jq unzip
;;
*)
echo "ERROR: No supported package manager found (apt, pacman, dnf)"
exit 1
;;
esac
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo
echo "Build dependencies installed"
- name: Download previous artifact
if: matrix.can_reuse == true
run: |
COMPONENT="${{ matrix.component }}"
BRANCH_NAME="${{ needs.configure.outputs.branch }}"
echo "Reusing $COMPONENT from previous release"
PREV_RELEASE=$(gh release list --limit 100 --json tagName \
-q "[.[] | select(.tagName | startswith(\"${BRANCH_NAME}-\"))] | .[0].tagName")
if [[ -z "$PREV_RELEASE" || "$PREV_RELEASE" == "null" ]]; then
echo "ERROR: No previous release found but can_reuse is true"
exit 1
fi
echo "Downloading $COMPONENT assets from $PREV_RELEASE..."
mkdir -p "${COMPONENT}/artifacts"
gh release download "$PREV_RELEASE" \
-p "${COMPONENT}.tar.gz" \
-p "${COMPONENT}.tar.gz.sha" \
-D "${COMPONENT}/artifacts/"
echo "Reused assets downloaded"
- name: Build component
if: matrix.can_reuse == false
run: |
COMPONENT="${{ matrix.component }}"
VERSION="${{ matrix.version }}"
echo "Building $COMPONENT version $VERSION..."
automation-tools/alchemist/alchemist.sh -f "${{ matrix.recipe }}"
echo "Build complete"
- name: Verify artifacts
run: |
COMPONENT="${{ matrix.component }}"
if [[ ! -f "${COMPONENT}/artifacts/${COMPONENT}.tar.gz" ]]; then
echo "ERROR: Archive not found: ${COMPONENT}/artifacts/${COMPONENT}.tar.gz"
exit 1
fi
if [[ ! -f "${COMPONENT}/artifacts/${COMPONENT}.tar.gz.sha" ]]; then
echo "ERROR: SHA file not found: ${COMPONENT}/artifacts/${COMPONENT}.tar.gz.sha"
exit 1
fi
if ! grep -q "${COMPONENT}.tar.gz" "${COMPONENT}/artifacts/${COMPONENT}.tar.gz.sha"; then
echo "ERROR: SHA file does not reference expected archive filename"
exit 1
fi
ARCHIVE_SIZE=$(du -h "${COMPONENT}/artifacts/${COMPONENT}.tar.gz" | cut -f1)
echo "Artifacts verified ($ARCHIVE_SIZE)"
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.component }}-artifacts
path: ${{ matrix.component }}/artifacts/
retention-days: 1
if-no-files-found: error
# ===========================================================================
# Create release in components repo
# ===========================================================================
create-release:
name: Create Release
needs: [configure, resolve-versions, build-components]
if: needs.configure.outputs.is_dry_run == 'false'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
outputs:
release_tag: ${{ steps.tag.outputs.tag }}
branch: ${{ needs.configure.outputs.branch }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ needs.configure.outputs.branch }}
- name: Generate release tag
id: tag
run: |
BRANCH_NAME="${{ needs.configure.outputs.branch }}"
BRANCH_SAFE=$(echo "$BRANCH_NAME" | sed 's/\//-/g')
TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
RELEASE_TAG="${BRANCH_SAFE}-${TIMESTAMP}"
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "timestamp=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "branch_safe=$BRANCH_SAFE" >> $GITHUB_OUTPUT
echo "Release tag: $RELEASE_TAG"
- name: Determine release type
id: release-type
run: |
BRANCH="${{ steps.tag.outputs.branch }}"
case "$BRANCH" in
main)
echo "is_latest=true" >> $GITHUB_OUTPUT
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "is_draft=false" >> $GITHUB_OUTPUT
echo "Release type: Latest"
;;
cooker|epicure)
echo "is_latest=false" >> $GITHUB_OUTPUT
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "is_draft=false" >> $GITHUB_OUTPUT
echo "Release type: Pre-release"
;;
*)
echo "is_latest=false" >> $GITHUB_OUTPUT
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "is_draft=false" >> $GITHUB_OUTPUT
echo "Release type: Pre-release ($BRANCH)"
;;
esac
- name: Download all component artifacts
uses: actions/download-artifact@v8
with:
path: downloaded-artifacts/
pattern: '*-artifacts'
- name: Download components metadata
uses: actions/download-artifact@v8
with:
name: components-metadata
path: downloaded-artifacts/
- name: Download proposed version pins
uses: actions/download-artifact@v8
with:
name: proposed-version-pins
path: downloaded-artifacts/
- name: Collect and verify artifacts
id: collect
run: |
mkdir -p release-assets
find downloaded-artifacts/ -name "*.tar.gz" -o -name "*.tar.gz.sha" | \
while read -r file; do
cp "$file" release-assets/
done
# Include proposed version pins as a release asset
if [[ -f downloaded-artifacts/proposed_version_pins.sh ]]; then
cp downloaded-artifacts/proposed_version_pins.sh release-assets/
echo "Proposed version pins included in release assets"
else
echo "WARNING: proposed_version_pins.sh not found in artifacts"
fi
# Verify matching pairs
MISSING=0
for archive in release-assets/*.tar.gz; do
if [[ ! -f "${archive}.sha" ]]; then
echo "ERROR: Missing SHA file for $(basename "$archive")"
MISSING=$((MISSING + 1))
fi
done
if [[ "$MISSING" -gt 0 ]]; then
echo "ERROR: $MISSING artifact(s) missing SHA files"
exit 1
fi
ARTIFACT_COUNT=$(ls release-assets/*.tar.gz | wc -l)
echo "Artifacts collected and verified: $ARTIFACT_COUNT components"
echo "artifact_count=$ARTIFACT_COUNT" >> $GITHUB_OUTPUT
- name: Generate release metadata and sources JSON
run: |
RELEASE_TAG="${{ steps.tag.outputs.tag }}"
BUILD_TIMESTAMP="${{ steps.tag.outputs.timestamp }}"
CURRENT_METADATA=$(cat downloaded-artifacts/components_metadata.json)
PREV_VERSIONS='${{ needs.resolve-versions.outputs.prev-versions-json }}'
REPO="${{ github.repository }}"
BASE_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}"
echo "Generating release metadata and sources JSON..."
release_metadata='{}'
sources_json='[]'
for archive in release-assets/*.tar.gz; do
COMPONENT=$(basename "$archive" .tar.gz)
SHA=$(awk '{print $1}' "release-assets/${COMPONENT}.tar.gz.sha")
URL="${BASE_URL}/${COMPONENT}.tar.gz"
VERSION=$(echo "$CURRENT_METADATA" | \
jq -r --arg c "$COMPONENT" '.[$c].version // "unknown"')
TREE_HASH=$(echo "$CURRENT_METADATA" | \
jq -r --arg c "$COMPONENT" '.[$c].git_tree_hash // empty')
# Determine if reused or newly built
PREV_TREE=$(echo "$PREV_VERSIONS" | \
jq -r --arg c "$COMPONENT" '.[$c].git_tree_hash // empty')
PREV_VERSION=$(echo "$PREV_VERSIONS" | \
jq -r --arg c "$COMPONENT" '.[$c].version // empty')
if [[ -n "$PREV_TREE" && "$TREE_HASH" == "$PREV_TREE" && "$VERSION" == "$PREV_VERSION" ]]; then
BUILT_ON=$(echo "$PREV_VERSIONS" | \
jq -r --arg c "$COMPONENT" '.[$c].built_on // empty')
STATUS="reused"
else
BUILT_ON="$BUILD_TIMESTAMP"
STATUS="new"
fi
release_metadata=$(echo "$release_metadata" | jq \
--arg component "$COMPONENT" \
--arg version "$VERSION" \
--arg tree_hash "$TREE_HASH" \
--arg built_on "$BUILT_ON" \
--arg sha "$SHA" \
--arg url "$URL" \
--arg status "$STATUS" \
'.[$component] = {
version: $version,
git_tree_hash: $tree_hash,
built_on: $built_on,
sha256: $sha,
url: $url,
status: $status
}')
sources_json=$(echo "$sources_json" | jq \
--arg url "$URL" \
--arg sha "$SHA" \
'. += [{
type: "file",
url: $url,
sha256: $sha,
dest: "components"
}]')
done
echo "$release_metadata" | jq . > release-assets/release_metadata.json
echo "$sources_json" | jq . > release-assets/components-sources.json
# Also overwrite the metadata artifact with final version (includes built_on/status)
echo "$release_metadata" | jq . > release-assets/components_metadata.json
echo "Release metadata and sources JSON generated"
- name: Generate release notes
run: |
RELEASE_TAG="${{ steps.tag.outputs.tag }}"
BUILD_TIMESTAMP="${{ steps.tag.outputs.timestamp }}"
BRANCH="${{ steps.tag.outputs.branch }}"
COMMIT="${{ github.sha }}"
METADATA=$(cat release-assets/release_metadata.json)
NEW_COUNT=$(echo "$METADATA" | \
jq '[to_entries[] | select(.value.status == "new")] | length')
REUSED_COUNT=$(echo "$METADATA" | \
jq '[to_entries[] | select(.value.status == "reused")] | length')
TOTAL_COUNT=$(echo "$METADATA" | jq 'keys | length')
cat > release_notes.md <<EOF
## ${{ github.repository_owner }} ${{ github.event.repository.name }} ${RELEASE_TAG}
**Branch:** \`${BRANCH}\`
**Commit:** \`${COMMIT}\`
**Built On:** ${BUILD_TIMESTAMP}
**Components:** ${TOTAL_COUNT} total (${NEW_COUNT} new, ${REUSED_COUNT} reused)
---
## Components
| Component | Version | Status | Built On | Commit |
|-----------|---------|--------|----------|--------|
EOF
echo "$METADATA" | jq -r '
to_entries |
sort_by(.key) |
.[] |
"| \(.key) | \(.value.version) | \(if .value.status == "new" then "New Build" else "Reused" end) | \(.value.built_on) | \(if .value.status == "new" then "'"$COMMIT"'" else "---" end) |"
' >> release_notes.md
echo "Release notes generated"
- name: Create release
run: |
RELEASE_TAG="${{ steps.tag.outputs.tag }}"
TITLE="${{ github.repository_owner }} ${{ github.event.repository.name }} ${RELEASE_TAG}"
FLAGS=""
[[ "${{ steps.release-type.outputs.is_draft }}" == "true" ]] && FLAGS="$FLAGS --draft"
[[ "${{ steps.release-type.outputs.is_prerelease }}" == "true" ]] && FLAGS="$FLAGS --prerelease"
[[ "${{ steps.release-type.outputs.is_latest }}" == "true" ]] && FLAGS="$FLAGS --latest"
echo "Creating release..."
echo " Tag: $RELEASE_TAG"
echo " Title: $TITLE"
gh release create "$RELEASE_TAG" \
--title "$TITLE" \
--notes-file release_notes.md \
$FLAGS \
release-assets/*
echo ""
echo "=========================================="
echo "Release created"
echo " URL: https://github.com/${{ github.repository }}/releases/tag/${RELEASE_TAG}"
echo "=========================================="
# ===========================================================================
# Dry-run summary (when release is skipped)
# ===========================================================================
dry-run-summary:
name: Dry-Run Summary
needs: [configure, resolve-versions, build-components]
if: needs.configure.outputs.is_dry_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Download components metadata
uses: actions/download-artifact@v8
with:
name: components-metadata
path: .
- name: Show dry-run results
run: |
BRANCH="${{ needs.configure.outputs.branch }}"
METADATA=$(cat components_metadata.json)
echo "=========================================="
echo "DRY RUN - Build Complete (no release created)"
echo "=========================================="
echo ""
echo "Branch: $BRANCH"
echo ""
echo "Components that were built/validated:"
echo "$METADATA" | jq -r 'to_entries[] | " \(.key): \(.value.version)"'
echo ""
echo "Dry-run complete - no release was created"
echo "=========================================="
# ===========================================================================
# Trigger flatpak build in the flatpak repo
# ===========================================================================
trigger-flatpak-build:
name: Trigger Flatpak Build
needs: [configure, create-release]
if: needs.configure.outputs.trigger_flatpak == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger flatpak repo build
run: |
BRANCH="${{ needs.create-release.outputs.branch }}"
RELEASE_TAG="${{ needs.create-release.outputs.release_tag }}"
echo "Triggering flatpak build..."
echo " Flatpak repo: $FLATPAK_REPO"
echo " Components branch: $BRANCH"
echo " Components release: $RELEASE_TAG"
echo " Force countertop: ${{ needs.configure.outputs.force_countertop }}"
# Epicure components -> trigger on main branch (epicure uses main code)
# All others -> trigger on matching branch
if [[ "$BRANCH" == "epicure" ]]; then
TARGET_BRANCH="main"
else
TARGET_BRANCH="$BRANCH"
fi
gh workflow run "Build Flatpak" \
--repo "$FLATPAK_REPO" \
--ref "$TARGET_BRANCH" \
-f triggered_by_components=true \
-f branch_override="$BRANCH" \
-f components_release_tag="$RELEASE_TAG" \
-f force_countertop="${{ needs.configure.outputs.force_countertop }}"
echo ""
echo "=========================================="
echo "Flatpak build triggered"
echo " Target branch: $TARGET_BRANCH"
echo "=========================================="
env:
GH_TOKEN: ${{ secrets.API_REQUEST_TOKEN }}
# ===========================================================================
# Summary
# ===========================================================================
summary:
name: Build Summary
needs: [check-trigger, configure, setup-matrix, build-components, create-release, trigger-flatpak-build]
if: always() && needs.check-trigger.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Download components metadata
if: needs.build-components.result == 'success'
uses: actions/download-artifact@v8
with:
name: components-metadata
path: .
- name: Generate summary
run: |
BRANCH="${{ needs.configure.outputs.branch }}"
IS_DRY_RUN="${{ needs.configure.outputs.is_dry_run }}"
TRIGGER_FLATPAK="${{ needs.configure.outputs.trigger_flatpak }}"
BUILD_MATRIX='${{ needs.setup-matrix.outputs.build-matrix }}'
BUILD_RESULT="${{ needs.build-components.result }}"
RELEASE_RESULT="${{ needs.create-release.result }}"
FLATPAK_RESULT="${{ needs.trigger-flatpak-build.result }}"
# Determine release tag (actual or would-be for dry run)
RELEASE_TAG="${{ needs.create-release.outputs.release_tag }}"
if [[ -z "$RELEASE_TAG" ]]; then
# Dry run or release was skipped - generate what the tag would be
BRANCH_SAFE=$(echo "$BRANCH" | sed 's/\//-/g')
RELEASE_TAG="${BRANCH_SAFE}-$(date -u +"%Y%m%d-%H%M%S") (not created)"
fi
# Header
echo "## Components Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "$IS_DRY_RUN" == "true" ]]; then
echo "> **Dry run** - no release was created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Build parameters table
echo "### Parameters" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${BRANCH}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release tag | \`${RELEASE_TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Trigger flatpak | ${TRIGGER_FLATPAK} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Components table
if [[ -n "$BUILD_MATRIX" && "$BUILD_MATRIX" != "null" ]]; then
TOTAL=$(echo "$BUILD_MATRIX" | jq 'length')
REUSE_COUNT=$(echo "$BUILD_MATRIX" | jq '[.[] | select(.can_reuse == true)] | length')
BUILD_COUNT=$(echo "$BUILD_MATRIX" | jq '[.[] | select(.can_reuse == false)] | length')
echo "### Components ($TOTAL total: $BUILD_COUNT built, $REUSE_COUNT reused)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Component | Version | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|---------|--------|" >> $GITHUB_STEP_SUMMARY
echo "$BUILD_MATRIX" | jq -r '
sort_by(.component) | .[] |
"| \(.component) | \(.version) | \(if .can_reuse then "Reused" else "Built" end) |"
' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Results table
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Stage | Result |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build | ${BUILD_RESULT} |" >> $GITHUB_STEP_SUMMARY
if [[ "$IS_DRY_RUN" != "true" ]]; then
echo "| Release | ${RELEASE_RESULT} |" >> $GITHUB_STEP_SUMMARY
fi
if [[ "$TRIGGER_FLATPAK" == "true" ]]; then
echo "| Flatpak trigger | ${FLATPAK_RESULT} |" >> $GITHUB_STEP_SUMMARY
fi