Alchemic Circle: Build RetroDECK Components #331
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |