diff --git a/.github/actions/_tmp-aetherpak-publish-oci/action.yml b/.github/actions/_tmp-aetherpak-publish-oci/action.yml new file mode 100644 index 000000000..4e16bad2d --- /dev/null +++ b/.github/actions/_tmp-aetherpak-publish-oci/action.yml @@ -0,0 +1,287 @@ +# ============================================================================ +# INTENTIONALLY VENDORED — decoupled from tagged releases for now +# +# Vendored so we can iterate on the aetherpak flatpak publish + Pages +# pipeline (see PR #2301 / #2427) in follow-up PRs without cutting real +# ToolHive Studio releases (which trigger real auto-updates and binary +# signing) just to test changes here. +# +# See discussion in #2427/#2429. +# +# Source: https://github.com/aetherpak/actions/tree/main/publish-oci (MIT +# License), vendored from commit d2112669ceaa09afb7c2b96e9e9844866a6c8e78 +# (the same commit on-release.yml pins). +# +# Changes from upstream: +# - Uses the local ../_tmp-aetherpak-setup-cli instead of aetherpak/setup-cli@v1. +# - Added a `dry-run` input. When true, everything runs identically up to +# and including resolving refs/coordinates, but the actual `aetherpak +# push-oci` invocation (the only network-mutating call in this action) is +# replaced with printing the command that would have run. +# - Fixed a pre-existing issue also present upstream: `registry-token`'s +# default was `${{ github.token }}`, but action.yml input defaults are +# plain strings, not expressions (confirmed against GitHub's own docs — +# only `outputs.*.value` supports expressions). A caller that omits +# registry-token would get the literal text `${{ github.token }}` rather +# than a real token. Changed the default to empty and moved the fallback +# to the actual step's env mapping (`inputs.registry-token || github.token`), +# which does support expressions. Worth flagging upstream. +# ============================================================================ +name: '[TEMP] AetherPak Publish OCI (vendored, dry-run capable)' +description: 'Push an OSTree-built or bundle-imported Flatpak as a signed OCI image and emit a per-cell record. Parallel-safe.' + +inputs: + repo-path: + description: 'Path to the OSTree repository containing the built application.' + required: false + default: '_repo' + bundle-path: + description: 'Optional pre-built .flatpak bundle to import into repo-path.' + required: false + default: '' + app-id: + description: "Reverse-DNS application ID. Resolved from the repo's ref when empty." + required: false + default: '' + branch: + description: 'Flatpak channel. Resolution: explicit input > repo ref branch > git-ref default.' + required: false + default: '' + arch: + description: "CPU architecture (e.g. x86_64). Resolved from the repo's ref when empty." + required: false + default: '' + registry: + description: 'The registry host (default: ghcr.io).' + required: false + default: 'ghcr.io' + insecure-registry: + description: 'Skip TLS verification for the OCI registry.' + required: false + default: 'false' + oci-repository: + description: 'Target registry repository path without host (defaults to GITHUB_REPOSITORY).' + required: false + default: '' + registry-token: + description: 'Registry auth token for the OCI push. Empty falls back to github.token.' + required: false + default: '' + signing: + description: 'Image signing mode: auto (sign iff a key is set), gpg (require), or off.' + required: false + default: 'auto' + gpg-private-key: + description: "ASCII-armored GPG private key. Empty disables signing under 'auto'." + required: false + default: '' + gpg-private-key-passphrase: + description: 'Passphrase for the GPG private key, if protected.' + required: false + default: '' + records-dir: + description: 'Directory where the per-cell record subtree is written.' + required: false + default: '_records' + cli-ref: + description: 'Commit SHA of aetherpak/cli to build (passed through to the vendored setup-cli action).' + required: false + default: 'e9388a1d41021dcf0ac75feb0a2e50dba87ca81c' + dry-run: + description: '[TEMP addition] If true, skip the actual OCI push and print what would have run.' + required: false + default: 'false' + +outputs: + cell-dir: + description: "Path to this cell's record subdir (/-)." + value: ${{ steps.push.outputs.cell-dir }} + signing-path: + description: "'gpg' when signed, else 'off'." + value: ${{ steps.signing.outputs.sign == 'true' && 'gpg' || 'off' }} + app-id: + description: 'Resolved Reverse-DNS Application ID.' + value: ${{ steps.vars.outputs.app-id }} + branch: + description: 'Resolved Flatpak branch.' + value: ${{ steps.vars.outputs.branch }} + arch: + description: 'Resolved application architecture.' + value: ${{ steps.vars.outputs.arch }} + repo-path: + description: 'OSTree repo path containing the application.' + value: ${{ inputs.repo-path }} + +runs: + using: 'composite' + steps: + - name: Set up aetherpak CLI (vendored, built from pinned source commit) + uses: ./.github/actions/_tmp-aetherpak-setup-cli + with: + cli-ref: ${{ inputs.cli-ref }} + + - name: Signing gate + id: signing + shell: bash + env: + MODE: ${{ inputs.signing }} + KEY: ${{ inputs.gpg-private-key }} + run: | + set -euo pipefail + NO_SIGN=false + ALLOW_UNSIGNED=false + case "$MODE" in + off) SIGN=false; NO_SIGN=true; ALLOW_UNSIGNED=true ;; + gpg) [ -n "$KEY" ] || { echo "::error::signing=gpg but gpg-private-key is empty"; exit 1; }; SIGN=true ;; + auto) [ -n "$KEY" ] && SIGN=true || { SIGN=false; ALLOW_UNSIGNED=true; } ;; + *) echo "::error::signing must be auto|gpg|off"; exit 1 ;; + esac + echo "sign=$SIGN" >> "$GITHUB_OUTPUT" + echo "no-sign=$NO_SIGN" >> "$GITHUB_OUTPUT" + echo "allow-unsigned=$ALLOW_UNSIGNED" >> "$GITHUB_OUTPUT" + + - name: Import bundle + if: ${{ inputs.bundle-path != '' }} + shell: bash + env: + BUNDLE: ${{ inputs.bundle-path }} + REPO: ${{ inputs.repo-path }} + BRANCH: ${{ inputs.branch }} + ARCH: ${{ inputs.arch }} + APP: ${{ inputs.app-id }} + run: | + set -euo pipefail + ARGS=() + if [ -n "$BUNDLE" ]; then + while IFS= read -r line || [ -n "$line" ]; do + IFS=',' read -r -a subpaths <<< "$line" + for p in "${subpaths[@]}"; do + p=$(echo "$p" | xargs) + [ -n "$p" ] && ARGS+=(--bundle-path "$p") + done + done <<< "$BUNDLE" + fi + [ -n "$BRANCH" ] && ARGS+=(--branch "$BRANCH") + [ -n "$ARCH" ] && ARGS+=(--arch "$ARCH") + [ -n "$REPO" ] && ARGS+=(--repo-path "$REPO") + [ -n "$APP" ] && ARGS+=(--app-id "$APP") + echo "Running: aetherpak import ${ARGS[*]}" + aetherpak import "${ARGS[@]}" + + - name: Restore OSTree repo layout + if: ${{ inputs.bundle-path == '' }} + shell: bash + env: + REPO: ${{ inputs.repo-path }} + run: | + set -euo pipefail + mkdir -p "$REPO/refs/heads" "$REPO/refs/mirrors" "$REPO/refs/remotes" + + - name: Resolve coordinates + id: vars + shell: bash + env: + REPO: ${{ inputs.repo-path }} + IN_APP: ${{ inputs.app-id }} + IN_ARCH: ${{ inputs.arch }} + IN_BRANCH: ${{ inputs.branch }} + run: | + set -euo pipefail + APP="$IN_APP"; ARCH="$IN_ARCH"; BRANCH="$IN_BRANCH" + if [ -z "$APP" ] || [ -z "$ARCH" ] || [ -z "$BRANCH" ]; then + tmp="$(mktemp)" + aetherpak inspect-repo --repo-path "$REPO" --output-file "$tmp" + [ -n "$APP" ] || APP="$(sed -n 's/^app-id=//p' "$tmp")" + [ -n "$ARCH" ] || ARCH="$(sed -n 's/^arch=//p' "$tmp")" + [ -n "$BRANCH" ] || BRANCH="$(sed -n 's/^branch=//p' "$tmp")" + fi + { + echo "app-id=$APP" + echo "arch=$ARCH" + echo "branch=$BRANCH" + } >> "$GITHUB_OUTPUT" + echo "Resolved coordinates: app-id=$APP arch=$ARCH branch=$BRANCH" + + - name: Push OCI image + id: push + shell: bash + env: + REGISTRY: ${{ inputs.registry }} + OCI_REPOSITORY: ${{ inputs.oci-repository }} + REPO: ${{ inputs.repo-path }} + RECORDS_DIR: ${{ inputs.records-dir }} + INSECURE: ${{ inputs.insecure-registry }} + SIGN: ${{ steps.signing.outputs.sign }} + GPG_KEY: ${{ inputs.gpg-private-key }} + OCI_USERNAME: ${{ github.actor }} + OCI_PASSWORD: ${{ inputs.registry-token || github.token }} + AETHERPAK_GPG_PASSPHRASE: ${{ inputs.gpg-private-key-passphrase }} + DRY_RUN: ${{ inputs.dry-run }} + run: | + set -euo pipefail + + refs=$(ostree refs --repo="$REPO" | grep '^app/' || true) + if [ -z "$refs" ]; then + echo "::error::No application refs found in OSTree repository." + exit 1 + fi + + pushed_any=false + IN_APP="${{ inputs.app-id }}" + IN_ARCH="${{ inputs.arch }}" + IN_BRANCH="${{ inputs.branch }}" + + while read -r ref; do + [ -z "$ref" ] && continue + + IFS='/' read -r _ ref_app ref_arch ref_branch <<< "$ref" + + if [ -n "$IN_APP" ] && [ "$IN_APP" != "$ref_app" ]; then + continue + fi + if [ -n "$IN_ARCH" ] && [ "$IN_ARCH" != "$ref_arch" ]; then + continue + fi + if [ -n "$IN_BRANCH" ] && [ "$IN_BRANCH" != "$ref_branch" ]; then + continue + fi + + echo "Pushing cell: $ref_app ($ref_arch) on channel $ref_branch" + + REPO_NAME="${OCI_REPOSITORY:-$GITHUB_REPOSITORY}" + REPO_NAME="$(printf '%s' "$REPO_NAME" | tr '[:upper:]' '[:lower:]')" + ARGS=(--app "$ref_app" --arch "$ref_arch" --branch "$ref_branch" + --registry "$REGISTRY" --oci-repository "$REPO_NAME" + --repo-path "$REPO" --records-dir "$RECORDS_DIR" + --output-file "$GITHUB_OUTPUT") + + [ "$INSECURE" = "true" ] && ARGS+=(--insecure) + if [ "$SIGN" = "true" ]; then + keyfile="$(mktemp)" + chmod 600 "$keyfile" + printf '%s' "$GPG_KEY" > "$keyfile" + ARGS+=(--gpg-key "$keyfile") + fi + [ "${{ steps.signing.outputs.no-sign }}" = "true" ] && ARGS+=(--no-sign) + [ "${{ steps.signing.outputs.allow-unsigned }}" = "true" ] && ARGS+=(--allow-unsigned) + + if [ "$DRY_RUN" = "true" ]; then + echo "::notice::[dry-run] would run: aetherpak push-oci ${ARGS[*]}" + echo "::notice::[dry-run] would push to: ${REGISTRY}/${REPO_NAME}:${ref_app}-${ref_branch}-${ref_arch}" + # Deliberately does NOT fabricate a records-dir entry: a real + # per-cell record (image digest, size, etc.) only exists after an + # actual push, and faking one risks masking real behavior behind + # made-up data. Downstream publish-site will just see zero + # records in dry-run mode, which is itself a useful signal (does + # the template/CLI handle an empty index without crashing?). + else + aetherpak push-oci "${ARGS[@]}" + fi + [ "$SIGN" = "true" ] && rm -f "$keyfile" + pushed_any=true + done <<< "$refs" + + if [ "$pushed_any" = "false" ]; then + echo "::error::No refs matched the specified app-id/arch/branch filters." + exit 1 + fi diff --git a/.github/actions/_tmp-aetherpak-publish-site/action.yml b/.github/actions/_tmp-aetherpak-publish-site/action.yml new file mode 100644 index 000000000..a2243b66a --- /dev/null +++ b/.github/actions/_tmp-aetherpak-publish-site/action.yml @@ -0,0 +1,214 @@ +# ============================================================================ +# INTENTIONALLY VENDORED — decoupled from tagged releases for now +# +# Vendored so we can iterate on the aetherpak flatpak publish + Pages +# pipeline (see PR #2301 / #2427) in follow-up PRs without cutting real +# ToolHive Studio releases (which trigger real auto-updates and binary +# signing) just to test changes here. +# +# See discussion in #2427/#2429. +# +# Source: https://github.com/aetherpak/actions/tree/main/publish-site (MIT +# License), vendored from commit d2112669ceaa09afb7c2b96e9e9844866a6c8e78 +# (the same commit on-release.yml pins). +# +# Changes from upstream: +# - Uses the local ../_tmp-aetherpak-setup-cli instead of aetherpak/setup-cli@v1. +# - No dry-run gating needed here: `aetherpak build-site` only writes to a +# local output directory and does a read-only GET against pages-url plus +# read-only digest-existence checks during --reconcile (verified by +# reading pkg/site/site.go at v0.14.0) — it never mutates the registry or +# any remote host. Left otherwise unchanged, with a bit of extra logging. +# ============================================================================ +name: '[TEMP] AetherPak Publish Site (vendored)' +description: 'Aggregate per-cell records into the static site (index, refs, landing, signatures).' + +inputs: + records-dir: + description: 'Directory holding per-cell record subdirs (one per (app, arch)).' + required: false + default: '_records' + pages-url: + description: 'Public URL the index is served from. Required to seed the cumulative index.' + required: true + site-dir: + description: 'Output directory for the website files.' + required: false + default: '_site' + insecure-registry: + description: 'Pass --insecure to reconcile (use only for local test registries).' + required: false + default: 'false' + remote-name: + description: "Flatpak remote name and .flatpakrepo filename. Defaults to '-'." + required: false + default: '' + landing-page: + description: 'Write the static landing page (index.html).' + required: false + default: 'true' + runtime-repo: + description: 'RuntimeRepo URL written into each .flatpakref.' + required: false + default: 'https://dl.flathub.org/repo/flathub.flatpakrepo' + repo-title: + description: "Title for the .flatpakrepo. Empty defers to aetherpak.yaml's repo_title, then the CLI default." + required: false + default: '' + repo-homepage: + description: "Homepage for the .flatpakrepo. Empty defers to aetherpak.yaml's repo_homepage, then the pages URL." + required: false + default: '' + index-template: + description: "Path to a custom HTML index template. Empty defers to aetherpak.yaml's branding.index_template, then the CLI default." + required: false + default: '' + signing: + description: 'Signing mode: auto (export key iff present), gpg (require), off.' + required: false + default: 'auto' + gpg-private-key: + description: 'ASCII-armored GPG private key. Used only to export the public material.' + required: false + default: '' + gpg-private-key-passphrase: + description: 'Passphrase, if protected.' + required: false + default: '' + upload-pages-artifact: + description: 'Whether to upload the generated files as a GitHub Pages artifact.' + required: false + default: 'true' + pages-artifact-name: + description: 'The name of the uploaded GitHub Pages artifact.' + required: false + default: 'github-pages' + site-subpath: + description: 'Optional subdirectory under site-dir to structure the repository files (e.g. flatpak).' + required: false + default: '' + cli-ref: + description: 'Commit SHA of aetherpak/cli to build (passed through to the vendored setup-cli action).' + required: false + default: 'e9388a1d41021dcf0ac75feb0a2e50dba87ca81c' + +outputs: + signing-path: + description: "'gpg' when keys were exported, else 'off'." + value: ${{ steps.target.outputs.sign == 'true' && 'gpg' || 'off' }} + public-key-url: + description: 'Public URL of the exported public key, or empty.' + value: ${{ steps.target.outputs.public-key-url }} + signature-lookaside-url: + description: 'Public URL of the signature lookaside directory, or empty.' + value: ${{ steps.target.outputs.signature-lookaside-url }} + +runs: + using: 'composite' + steps: + - name: Set up aetherpak CLI (vendored, built from pinned source commit) + uses: ./.github/actions/_tmp-aetherpak-setup-cli + with: + cli-ref: ${{ inputs.cli-ref }} + install-dependencies: 'false' + + - name: Diagnostics + shell: bash + env: + GPG_KEY: ${{ inputs.gpg-private-key }} + GPG_PASS: ${{ inputs.gpg-private-key-passphrase }} + run: | + set -euo pipefail + ARGS=() + if [ -n "$GPG_KEY" ]; then + keyfile="$(mktemp)" + chmod 600 "$keyfile" + trap 'rm -f "$keyfile"' EXIT + printf '%s' "$GPG_KEY" > "$keyfile" + ARGS+=(--gpg-key "$keyfile") + [ -n "$GPG_PASS" ] && ARGS+=(--gpg-key-passphrase "$GPG_PASS") + fi + aetherpak status "${ARGS[@]}" || echo "::warning::aetherpak status reported issues; see diagnostics above" + + - name: Resolve target and signing gate + id: target + shell: bash + env: + INPUT_REMOTE_NAME: ${{ inputs.remote-name }} + MODE: ${{ inputs.signing }} + KEY: ${{ inputs.gpg-private-key }} + PAGES_URL: ${{ inputs.pages-url }} + run: | + set -euo pipefail + RNAME="${INPUT_REMOTE_NAME:-$GITHUB_REPOSITORY}" + RNAME="$(printf '%s' "$RNAME" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9_-]#-#g')" + echo "remote-name=$RNAME" >> "$GITHUB_OUTPUT" + NO_SIGN=false + ALLOW_UNSIGNED=false + case "$MODE" in + off) SIGN=false; NO_SIGN=true; ALLOW_UNSIGNED=true ;; + gpg) [ -n "$KEY" ] || { echo "::error::signing=gpg requires gpg-private-key"; exit 1; }; SIGN=true ;; + auto) [ -n "$KEY" ] && SIGN=true || { SIGN=false; ALLOW_UNSIGNED=true; } ;; + *) echo "::error::signing must be auto|gpg|off"; exit 1 ;; + esac + echo "sign=$SIGN" >> "$GITHUB_OUTPUT" + echo "no-sign=$NO_SIGN" >> "$GITHUB_OUTPUT" + echo "allow-unsigned=$ALLOW_UNSIGNED" >> "$GITHUB_OUTPUT" + if [ "$SIGN" = "true" ]; then + PAGES="${PAGES_URL%/}" + if [ -n "${{ inputs.site-subpath }}" ]; then + PAGES="${PAGES}/${{ inputs.site-subpath }}" + fi + echo "public-key-url=${PAGES}/sigs/key.asc" >> "$GITHUB_OUTPUT" + echo "signature-lookaside-url=${PAGES}/sigs" >> "$GITHUB_OUTPUT" + fi + + - name: Build site + shell: bash + env: + RECORDS_DIR: ${{ inputs.records-dir }} + PAGES_URL: ${{ inputs.pages-url }} + SITE_DIR: ${{ inputs.site-dir }} + INSECURE: ${{ inputs.insecure-registry }} + REMOTE_NAME: ${{ steps.target.outputs.remote-name }} + REPO_TITLE: ${{ inputs.repo-title }} + REPO_HOMEPAGE: ${{ inputs.repo-homepage }} + AETHERPAK_INDEX_TEMPLATE: ${{ inputs.index-template }} + RUNTIME_REPO: ${{ inputs.runtime-repo }} + LANDING: ${{ inputs.landing-page }} + SIGN: ${{ steps.target.outputs.sign }} + GPG_KEY: ${{ inputs.gpg-private-key }} + run: | + set -euo pipefail + RESOLVED_PAGES_URL="${PAGES_URL%/}" + RESOLVED_SITE_DIR="${SITE_DIR}" + if [ -n "${{ inputs.site-subpath }}" ]; then + RESOLVED_PAGES_URL="${PAGES_URL%/}/${{ inputs.site-subpath }}" + RESOLVED_SITE_DIR="${SITE_DIR%/}/${{ inputs.site-subpath }}" + fi + echo "Resolved pages URL for index seeding: $RESOLVED_PAGES_URL" + echo "Resolved site output dir: $RESOLVED_SITE_DIR" + ARGS=(--records-dir "$RECORDS_DIR" --pages-url "$RESOLVED_PAGES_URL" --site-dir "$RESOLVED_SITE_DIR" + --remote-name "$REMOTE_NAME" --runtime-repo "$RUNTIME_REPO" --reconcile) + [ -n "$REPO_TITLE" ] && ARGS+=(--repo-title "$REPO_TITLE") + [ -n "$REPO_HOMEPAGE" ] && ARGS+=(--repo-homepage "$REPO_HOMEPAGE") + [ "$INSECURE" = "true" ] && ARGS+=(--insecure) + [ "$LANDING" = "true" ] && ARGS+=(--landing-page) + if [ "$SIGN" = "true" ]; then + keyfile="$(mktemp)" + chmod 600 "$keyfile" + trap 'rm -f "$keyfile"' EXIT + printf '%s' "$GPG_KEY" > "$keyfile" + ARGS+=(--gpg-key "$keyfile") + fi + [ "${{ steps.target.outputs.no-sign }}" = "true" ] && ARGS+=(--no-sign) + [ "${{ steps.target.outputs.allow-unsigned }}" = "true" ] && ARGS+=(--allow-unsigned) + echo "Running: aetherpak build-site ${ARGS[*]}" + aetherpak build-site "${ARGS[@]}" + + - name: Upload Pages artifact + if: ${{ inputs.upload-pages-artifact == 'true' }} + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: ${{ inputs.site-dir }} + name: ${{ inputs.pages-artifact-name }} diff --git a/.github/actions/_tmp-aetherpak-setup-cli/action.yml b/.github/actions/_tmp-aetherpak-setup-cli/action.yml new file mode 100644 index 000000000..9372d329e --- /dev/null +++ b/.github/actions/_tmp-aetherpak-setup-cli/action.yml @@ -0,0 +1,199 @@ +# ============================================================================ +# INTENTIONALLY VENDORED — decoupled from tagged releases for now +# +# Vendored so we can iterate on the aetherpak flatpak publish + Pages +# pipeline (see PR #2301 / #2427) in follow-up PRs without cutting real +# ToolHive Studio releases (which trigger real auto-updates and binary +# signing) just to test changes here. Pinned to one exact source commit so +# builds stay reproducible while we iterate. +# +# See discussion in #2427/#2429. +# +# Source: https://github.com/aetherpak/setup-cli (MIT License) +# Vendored from tag: v1 +# +# Changes from upstream: +# - Upstream downloads a pre-built release binary. This version instead +# builds the CLI from source, checked out at a pinned commit SHA +# (e9388a1d41021dcf0ac75feb0a2e50dba87ca81c, the exact commit the +# v0.14.0 tag resolves to as of this writing — confirmed via +# `gh api repos/aetherpak/cli/git/refs/tags/v0.14.0`). +# +# Pinning to an exact commit keeps this test build fully reproducible +# while we iterate on the pipeline, the same way we'd pin any dependency +# during active development, regardless of anything changing upstream in +# the meantime. +# +# Confirmed this is a straightforward build: aetherpak/cli has no cgo +# (scanned all 68 .go files at this commit for `import "C"`, zero +# matches) and depends only on mainstream Go modules +# (google/go-containerregistry, spf13/cobra, ProtonMail/go-crypto, +# charmbracelet/*), each already checksum-pinned via the upstream +# go.sum. Build command mirrors aetherpak/cli's own `make release/build` +# target. +# - Hardcoded cli-ref to the pinned commit SHA above instead of supporting +# a "latest"/tag-based version input. +# ============================================================================ +name: '[TEMP] AetherPak Setup CLI (vendored, built from a pinned source commit)' +description: 'Build the AetherPak CLI from a pinned source commit and install dependencies on Linux.' +outputs: + version: + description: 'Resolved commit SHA of AetherPak CLI that was built' + value: ${{ steps.build.outputs.version }} + path: + description: 'Directory where the AetherPak CLI binary is installed' + value: ${{ steps.build.outputs.path }} +inputs: + cli-ref: + description: 'Commit SHA of aetherpak/cli to build. Must be a full 40-char SHA, not a tag or branch.' + required: false + default: 'e9388a1d41021dcf0ac75feb0a2e50dba87ca81c' + repo: + description: 'GitHub repository for AetherPak CLI source' + required: false + default: 'aetherpak/cli' + install-dependencies: + description: 'Trigger installation of missing host dependencies (flatpak, ostree, flatpak-builder)' + required: false + default: 'true' + +runs: + using: 'composite' + steps: + - name: Verify OS compatibility + shell: bash + run: | + if [ "$RUNNER_OS" != "Linux" ]; then + echo "::error::AetherPak Setup CLI action only supports Linux runners." + exit 1 + fi + + - name: Verify cli-ref looks like a full commit SHA, not a tag/branch + shell: bash + env: + CLI_REF: ${{ inputs.cli-ref }} + run: | + set -euo pipefail + if ! [[ "$CLI_REF" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::cli-ref must be a full 40-character commit SHA (got '$CLI_REF') so this test build stays pinned to one exact, reproducible source snapshot." + exit 1 + fi + + - name: Ensure tooling + if: ${{ inputs.install-dependencies == 'true' }} + shell: bash + run: | + set -euo pipefail + MISSING="" + command -v flatpak >/dev/null 2>&1 || MISSING="$MISSING flatpak" + command -v ostree >/dev/null 2>&1 || MISSING="$MISSING ostree" + command -v flatpak-builder >/dev/null 2>&1 || MISSING="$MISSING flatpak-builder" + command -v patch >/dev/null 2>&1 || MISSING="$MISSING patch" + command -v unzip >/dev/null 2>&1 || MISSING="$MISSING unzip" + command -v bzip2 >/dev/null 2>&1 || MISSING="$MISSING bzip2" + command -v zstd >/dev/null 2>&1 || MISSING="$MISSING zstd" + + NEED_DBUS="false" + command -v dbus-run-session >/dev/null 2>&1 || NEED_DBUS="true" + + if [ -n "$MISSING" ] || [ "$NEED_DBUS" = "true" ]; then + SUDO="" + command -v sudo >/dev/null 2>&1 && SUDO="sudo" + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + PKGS="$MISSING" + if [ "$NEED_DBUS" = "true" ]; then + PKGS="$PKGS dbus" + fi + $SUDO apt-get update && $SUDO apt-get install -y $PKGS + elif command -v microdnf >/dev/null 2>&1; then + PKGS="$MISSING" + if [ "$NEED_DBUS" = "true" ]; then + PKGS="$PKGS dbus-daemon" + fi + $SUDO microdnf install -y --setopt=install_weak_deps=0 --nodocs $PKGS + elif command -v dnf >/dev/null 2>&1; then + PKGS="$MISSING" + if [ "$NEED_DBUS" = "true" ]; then + PKGS="$PKGS dbus-daemon" + fi + $SUDO dnf install -y --setopt=install_weak_deps=0 --nodocs $PKGS + else + PKGS="$MISSING" + if [ "$NEED_DBUS" = "true" ]; then + PKGS="$PKGS dbus-run-session (dbus/dbus-daemon)" + fi + echo "::warning::Missing dependencies ($PKGS) could not be installed because a supported package manager (apt-get, microdnf, dnf) is not available. Please install them manually." + fi + fi + + - name: Checkout aetherpak/cli at pinned commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + repository: ${{ inputs.repo }} + ref: ${{ inputs.cli-ref }} + path: _aetherpak-cli-src + persist-credentials: false + + - name: Verify checked-out commit matches the pinned SHA exactly + shell: bash + working-directory: _aetherpak-cli-src + env: + EXPECTED_SHA: ${{ inputs.cli-ref }} + run: | + set -euo pipefail + ACTUAL_SHA="$(git rev-parse HEAD)" + if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then + echo "::error::Checked-out commit ($ACTUAL_SHA) does not match pinned cli-ref ($EXPECTED_SHA)." + exit 1 + fi + echo "Verified checkout is exactly $ACTUAL_SHA" + + - name: Set up Go (version pinned by the checked-out go.mod itself) + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version-file: _aetherpak-cli-src/go.mod + cache-dependency-path: _aetherpak-cli-src/go.sum + + - name: Build AetherPak CLI from source + id: build + shell: bash + working-directory: _aetherpak-cli-src + env: + CLI_REF: ${{ inputs.cli-ref }} + run: | + set -euo pipefail + + case "$(echo "$RUNNER_ARCH" | tr '[:upper:]' '[:lower:]')" in + x64|amd64) ARCH="amd64" ;; + arm64) ARCH="arm64" ;; + *) + echo "::error::Unsupported CPU architecture: $RUNNER_ARCH. Only x86_64/amd64 and arm64 are supported." + exit 1 + ;; + esac + + INSTALL_DIR="$RUNNER_TEMP/aetherpak-bin" + mkdir -p "$INSTALL_DIR" + + echo "Building aetherpak from pinned commit $CLI_REF for linux/$ARCH..." + # Mirrors aetherpak/cli's own `make release/build` target (see Makefile). + GOOS=linux GOARCH="$ARCH" CGO_ENABLED=0 go build \ + -ldflags="-s -w -X github.com/aetherpak/aetherpak/cmd.Version=$CLI_REF" \ + -o "$INSTALL_DIR/aetherpak" \ + main.go + + chmod +x "$INSTALL_DIR/aetherpak" + + echo "$INSTALL_DIR" >> "$GITHUB_PATH" + echo "Successfully built aetherpak from commit $CLI_REF to $INSTALL_DIR and added to GITHUB_PATH" + + echo "version=$CLI_REF" >> "$GITHUB_OUTPUT" + echo "path=$INSTALL_DIR" >> "$GITHUB_OUTPUT" + + - name: Smoke-test the built binary runs + shell: bash + run: | + set -euo pipefail + "$RUNNER_TEMP/aetherpak-bin/aetherpak" --help >/dev/null + echo "Built binary executes successfully." diff --git a/.github/workflows/_tmp-test-aetherpak-publish.yml b/.github/workflows/_tmp-test-aetherpak-publish.yml new file mode 100644 index 000000000..4be81e9d9 --- /dev/null +++ b/.github/workflows/_tmp-test-aetherpak-publish.yml @@ -0,0 +1,102 @@ +# ============================================================================ +# INTENTIONALLY MERGED FOR NOW — decoupled from tagged releases +# +# Workflow to validate the aetherpak flatpak publish + Pages pipeline +# introduced in #2301, without cutting a real release (which would trigger +# the real on-release.yml, real auto-updates, and binary signing) and +# without needing the production GPG signing secrets. +# +# - Manual trigger only (workflow_dispatch). Never runs on push/PR/release. +# - Reuses a flatpak bundle artifact that a normal PR build already produced +# (pr-build-test.yml uploads flatpak-bundle-x86_64 on every PR) instead of +# rebuilding anything. +# - dry_run=true (default): runs everything up to and including +# `aetherpak build-site` for real (all local/read-only per source review of +# pkg/site/site.go), but skips the actual `aetherpak push-oci` call and the +# gh-pages deploy entirely. Inspect the generated site as a workflow +# artifact. +# - dry_run=false: does one real, isolated push — branch "test" (a distinct +# OCI tag suffix, cannot collide with the real "stable"/"beta" tags — see +# pkg/oci/oci.go tag format) and Pages subpath "flatpak-test" (isolated +# from the real "flatpak" subpath and "latest/" release manifest via +# destination_dir, verified against peaceiris/actions-gh-pages source). +# +# This keeps the pipeline decoupled from tagged releases while we iterate on +# it in follow-up PRs. See discussion in #2427/#2429. +# ============================================================================ +name: '[TEMP] Test AetherPak Flatpak Publish' + +on: + workflow_dispatch: + inputs: + run_id: + description: "Workflow run ID to pull the flatpak-bundle-x86_64 artifact from (e.g. a recent PR build's run)" + required: true + type: string + dry_run: + description: 'If true (default), skip the actual OCI push and gh-pages deploy' + required: false + type: boolean + default: true + +permissions: + contents: write # peaceiris/actions-gh-pages pushes a commit to gh-pages + packages: write + actions: read + +jobs: + test-publish-flatpak: + name: Test Publish Flatpak (dry_run=${{ inputs.dry_run }}) + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Download existing flatpak bundle artifact + uses: actions/download-artifact@fa0a91b85d4f404e444e00e0baa8db40025c1464 # v8 + with: + name: flatpak-bundle-x86_64 + path: _bundles + run-id: ${{ inputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: List downloaded bundle(s) + run: find _bundles -type f + + - name: Push Flatpak OCI to GHCR (test channel, unsigned, dry-run capable) + uses: ./.github/actions/_tmp-aetherpak-publish-oci + with: + bundle-path: _bundles/**/*.flatpak + branch: test + registry-token: ${{ secrets.GITHUB_TOKEN }} + records-dir: _records + signing: 'off' + dry-run: ${{ inputs.dry_run }} + + - name: Build site index (isolated subpath, unsigned) + uses: ./.github/actions/_tmp-aetherpak-publish-site + with: + records-dir: _records + pages-url: https://stacklok.github.io/toolhive-studio + site-subpath: flatpak-test + remote-name: toolhive-test + index-template: .github/workflows/templates/stacklok.html + upload-pages-artifact: 'false' + site-dir: _site + signing: 'off' + + - name: Upload generated site as a build artifact (always, for inspection) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: aetherpak-test-site + path: _site + retention-days: 7 + + - name: Deploy test site to gh-pages (isolated subfolder, real run only) + if: ${{ !inputs.dry_run }} + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: _site/flatpak-test + destination_dir: flatpak-test + keep_files: true