From 17928f9716ec2d6a5ef99b387e1d93233c0cbf5e Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sat, 18 Apr 2026 22:24:15 +0200 Subject: [PATCH 1/2] feat: add script to push installers to GHCR as OCI artifacts - Introduced a new script `push-installers.sh` to handle the uploading of installer artifacts to GitHub Container Registry (GHCR) after the build process. - Updated the GitHub Actions workflow to include a step for publishing installers to GHCR, ensuring they are available alongside tool images on the org's linked artifacts page. - Enhanced releasing documentation to clarify the dual publication of installers as both GitHub Release assets and OCI artifacts. --- .../scripts/app-release/push-installers.sh | 56 +++++++++++++++++++ .github/workflows/app-release.yml | 24 ++++++++ doc/guides/releasing.md | 16 +++++- 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/app-release/push-installers.sh diff --git a/.github/scripts/app-release/push-installers.sh b/.github/scripts/app-release/push-installers.sh new file mode 100755 index 0000000..16fa98e --- /dev/null +++ b/.github/scripts/app-release/push-installers.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Push the installers produced by tauri-action to GHCR as an OCI artifact so +# they show up under the org's linked artifacts page. +# Env: ARTIFACT_PATHS (JSON array from tauri-action), TAG, SLUG (macos|linux| +# windows), OWNER, REPO, REVISION, GHCR_TOKEN. +set -euo pipefail + +: "${ARTIFACT_PATHS:?}" +: "${TAG:?}" +: "${SLUG:?}" +: "${OWNER:?}" +: "${REPO:?}" +: "${REVISION:?}" +: "${GHCR_TOKEN:?}" + +owner_lower=$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]') +version="${TAG#v}" +image="ghcr.io/${owner_lower}/pengine-installer-${SLUG}:${version}" + +# Stage artifacts into a scratch dir so oras records flat basenames rather +# than the long tauri-action build paths. +staging=$(mktemp -d) +trap 'rm -rf "$staging"' EXIT + +files=() +while IFS= read -r src; do + [ -z "$src" ] && continue + cp "$src" "$staging/" + files+=("$(basename "$src"):application/octet-stream") +done < <(printf '%s' "$ARTIFACT_PATHS" | jq -r '.[]') + +if [ ${#files[@]} -eq 0 ]; then + echo "No installer artifacts produced by tauri-action; skipping GHCR push." >&2 + exit 0 +fi + +printf '%s' "$GHCR_TOKEN" | oras login ghcr.io -u "$OWNER" --password-stdin + +echo "Pushing ${#files[@]} file(s) to ${image}" +( cd "$staging" && oras push "$image" \ + --artifact-type "application/vnd.pengine.installer.v1" \ + --annotation "org.opencontainers.image.source=https://github.com/${REPO}" \ + --annotation "org.opencontainers.image.revision=${REVISION}" \ + --annotation "org.opencontainers.image.version=${version}" \ + --annotation "org.opencontainers.image.title=pengine installers (${SLUG})" \ + "${files[@]}" ) + +{ + echo "## pengine-installer-${SLUG}" + echo + echo "- Image: \`${image}\`" + echo "- Files:" + for f in "${files[@]}"; do + echo " - \`${f%%:*}\`" + done +} >> "${GITHUB_STEP_SUMMARY:-/dev/null}" diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 6348c25..6ff86c1 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -9,6 +9,10 @@ name: App Release # # Builds are currently unsigned. See doc/guides/releasing.md for how to obtain # Apple and Windows code-signing credentials and wire them back in here. +# +# After the installers are built, each platform job also pushes them to GHCR +# as an OCI artifact (ghcr.io//pengine-installer-:) so +# they appear on the org's linked artifacts page alongside tool images. on: push: @@ -22,6 +26,7 @@ on: permissions: contents: write + packages: write jobs: build: @@ -30,12 +35,15 @@ jobs: matrix: include: - platform: macos-latest + slug: macos args: "--target universal-apple-darwin" rust-targets: "aarch64-apple-darwin,x86_64-apple-darwin" - platform: ubuntu-22.04 + slug: linux args: "" rust-targets: "" - platform: windows-latest + slug: windows args: "" rust-targets: "" @@ -74,6 +82,7 @@ jobs: run: bun install --frozen-lockfile - name: Build and release + id: tauri uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -91,3 +100,18 @@ jobs: releaseDraft: true prerelease: false args: ${{ matrix.args }} + + - name: Set up oras + uses: oras-project/setup-oras@v1 + + - name: Publish installers to GHCR + shell: bash + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }} + TAG: ${{ github.event.inputs.tag || github.ref_name }} + SLUG: ${{ matrix.slug }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.repository }} + REVISION: ${{ github.sha }} + run: bash .github/scripts/app-release/push-installers.sh diff --git a/doc/guides/releasing.md b/doc/guides/releasing.md index b6328b1..bde4c9d 100644 --- a/doc/guides/releasing.md +++ b/doc/guides/releasing.md @@ -3,7 +3,21 @@ Pengine ships installers for macOS, Windows, and Linux via the [`App Release`](../../.github/workflows/app-release.yml) GitHub Actions workflow. Pushing a tag matching `v*` (e.g. `v1.0.1`) triggers a build on each -platform and uploads the installers as assets on a **draft** GitHub Release. +platform and publishes the installers in two places: + +- as assets on a **draft** GitHub Release (for humans to download), and +- as OCI artifacts on GHCR under + `ghcr.io/pengine-ai/pengine-installer-:`, which + surface on the org's + [linked artifacts page](https://github.com/orgs/pengine-ai/artifacts) next to + the tool images. + +Pull an installer from the registry with +[`oras`](https://oras.land/docs/installation): + +```bash +oras pull ghcr.io/pengine-ai/pengine-installer-macos:1.0.1 +``` ```bash git tag v1.0.1 From 491ab31f4c078279a031673781ebec6cff56f392 Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sat, 18 Apr 2026 22:49:26 +0200 Subject: [PATCH 2/2] fix: improve installer file handling in push-installers.sh - Added logic to strip stray carriage return characters from file paths to ensure compatibility with Windows environments. - Implemented a check to skip non-file artifacts, enhancing the robustness of the script by preventing errors during the copying process. --- .../scripts/app-release/push-installers.sh | 56 ------------------- .github/scripts/app-release/stage-binaries.sh | 35 ++++++++++++ .github/workflows/app-release.yml | 36 +++++++----- doc/guides/releasing.md | 15 +++-- 4 files changed, 63 insertions(+), 79 deletions(-) delete mode 100755 .github/scripts/app-release/push-installers.sh create mode 100755 .github/scripts/app-release/stage-binaries.sh diff --git a/.github/scripts/app-release/push-installers.sh b/.github/scripts/app-release/push-installers.sh deleted file mode 100755 index 16fa98e..0000000 --- a/.github/scripts/app-release/push-installers.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Push the installers produced by tauri-action to GHCR as an OCI artifact so -# they show up under the org's linked artifacts page. -# Env: ARTIFACT_PATHS (JSON array from tauri-action), TAG, SLUG (macos|linux| -# windows), OWNER, REPO, REVISION, GHCR_TOKEN. -set -euo pipefail - -: "${ARTIFACT_PATHS:?}" -: "${TAG:?}" -: "${SLUG:?}" -: "${OWNER:?}" -: "${REPO:?}" -: "${REVISION:?}" -: "${GHCR_TOKEN:?}" - -owner_lower=$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]') -version="${TAG#v}" -image="ghcr.io/${owner_lower}/pengine-installer-${SLUG}:${version}" - -# Stage artifacts into a scratch dir so oras records flat basenames rather -# than the long tauri-action build paths. -staging=$(mktemp -d) -trap 'rm -rf "$staging"' EXIT - -files=() -while IFS= read -r src; do - [ -z "$src" ] && continue - cp "$src" "$staging/" - files+=("$(basename "$src"):application/octet-stream") -done < <(printf '%s' "$ARTIFACT_PATHS" | jq -r '.[]') - -if [ ${#files[@]} -eq 0 ]; then - echo "No installer artifacts produced by tauri-action; skipping GHCR push." >&2 - exit 0 -fi - -printf '%s' "$GHCR_TOKEN" | oras login ghcr.io -u "$OWNER" --password-stdin - -echo "Pushing ${#files[@]} file(s) to ${image}" -( cd "$staging" && oras push "$image" \ - --artifact-type "application/vnd.pengine.installer.v1" \ - --annotation "org.opencontainers.image.source=https://github.com/${REPO}" \ - --annotation "org.opencontainers.image.revision=${REVISION}" \ - --annotation "org.opencontainers.image.version=${version}" \ - --annotation "org.opencontainers.image.title=pengine installers (${SLUG})" \ - "${files[@]}" ) - -{ - echo "## pengine-installer-${SLUG}" - echo - echo "- Image: \`${image}\`" - echo "- Files:" - for f in "${files[@]}"; do - echo " - \`${f%%:*}\`" - done -} >> "${GITHUB_STEP_SUMMARY:-/dev/null}" diff --git a/.github/scripts/app-release/stage-binaries.sh b/.github/scripts/app-release/stage-binaries.sh new file mode 100755 index 0000000..321ead7 --- /dev/null +++ b/.github/scripts/app-release/stage-binaries.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Stage the binaries produced by tauri-action into a flat directory so they +# can be uploaded as GitHub Actions build artifacts and attested. +# +# Env: ARTIFACT_PATHS (JSON array from tauri-action's artifactPaths output). +# Output: dir= appended to $GITHUB_OUTPUT. +set -euo pipefail + +: "${ARTIFACT_PATHS:?}" + +staging="${GITHUB_WORKSPACE:-$PWD}/dist/release" +rm -rf "$staging" +mkdir -p "$staging" + +while IFS= read -r src; do + # Strip stray CR — on windows-latest the env var can arrive CRLF-terminated. + src="${src%$'\r'}" + [ -z "$src" ] && continue + # tauri-action lists the .app bundle (a directory) alongside the .dmg on + # macOS; skip anything that isn't a regular file. + if [ ! -f "$src" ]; then + echo "Skipping non-file artifact: $src" >&2 + continue + fi + cp "$src" "$staging/" +done < <(printf '%s' "$ARTIFACT_PATHS" | jq -r '.[]') + +count=$(find "$staging" -maxdepth 1 -type f | wc -l | tr -d ' ') +if [ "$count" -eq 0 ]; then + echo "No binary artifacts produced by tauri-action." >&2 + exit 1 +fi + +echo "dir=$staging" >> "$GITHUB_OUTPUT" +echo "Staged $count file(s) in $staging" diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 6ff86c1..f8312b9 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -10,9 +10,10 @@ name: App Release # Builds are currently unsigned. See doc/guides/releasing.md for how to obtain # Apple and Windows code-signing credentials and wire them back in here. # -# After the installers are built, each platform job also pushes them to GHCR -# as an OCI artifact (ghcr.io//pengine-installer-:) so -# they appear on the org's linked artifacts page alongside tool images. +# After the build, each platform job also uploads its bundles as GitHub +# Actions build artifacts and generates a signed SLSA provenance attestation +# (actions/attest-build-provenance). The attestation is what surfaces the +# binaries on the org's linked-artifacts view via the artifact metadata API. on: push: @@ -26,7 +27,8 @@ on: permissions: contents: write - packages: write + id-token: write + attestations: write jobs: build: @@ -101,17 +103,21 @@ jobs: prerelease: false args: ${{ matrix.args }} - - name: Set up oras - uses: oras-project/setup-oras@v1 - - - name: Publish installers to GHCR + - name: Stage binaries + id: stage shell: bash env: - GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }} - TAG: ${{ github.event.inputs.tag || github.ref_name }} - SLUG: ${{ matrix.slug }} - OWNER: ${{ github.repository_owner }} - REPO: ${{ github.repository }} - REVISION: ${{ github.sha }} - run: bash .github/scripts/app-release/push-installers.sh + run: bash .github/scripts/app-release/stage-binaries.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: pengine-${{ matrix.slug }} + path: ${{ steps.stage.outputs.dir }} + if-no-files-found: error + + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: ${{ steps.stage.outputs.dir }}/* diff --git a/doc/guides/releasing.md b/doc/guides/releasing.md index bde4c9d..886a714 100644 --- a/doc/guides/releasing.md +++ b/doc/guides/releasing.md @@ -6,17 +6,16 @@ workflow. Pushing a tag matching `v*` (e.g. `v1.0.1`) triggers a build on each platform and publishes the installers in two places: - as assets on a **draft** GitHub Release (for humans to download), and -- as OCI artifacts on GHCR under - `ghcr.io/pengine-ai/pengine-installer-:`, which - surface on the org's - [linked artifacts page](https://github.com/orgs/pengine-ai/artifacts) next to - the tool images. +- as GitHub Actions build artifacts (`pengine-macos`, `pengine-linux`, + `pengine-windows`) with a signed SLSA build-provenance attestation. The + attestations surface on the org's + [linked artifacts page](https://github.com/orgs/pengine-ai/artifacts) via + the artifact metadata API — GHCR is reserved for Docker images (tools). -Pull an installer from the registry with -[`oras`](https://oras.land/docs/installation): +Verify a downloaded binary's provenance with the GitHub CLI: ```bash -oras pull ghcr.io/pengine-ai/pengine-installer-macos:1.0.1 +gh attestation verify ./pengine_1.0.1_amd64.AppImage --owner pengine-ai ``` ```bash