Files
Pixhelf/.forgejo/workflows/release.yaml
T

411 lines
15 KiB
YAML

name: Build and release
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: docker
container:
image: docker.io/library/node:22-bookworm
steps:
- name: Check out source
uses: https://data.forgejo.org/actions/checkout@v6
- name: Install Rust and cross-compilation toolchains
run: |
apt-get update
apt-get install --yes --no-install-recommends \
binutils \
build-essential \
ca-certificates \
curl \
file \
pkg-config \
python3-venv
curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs \
| sh -s -- --default-toolchain stable --profile minimal --no-modify-path -y
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
"$HOME/.cargo/bin/rustup" target add \
x86_64-unknown-linux-musl \
aarch64-unknown-linux-musl
python3 -m venv /opt/cargo-zigbuild
/opt/cargo-zigbuild/bin/pip install \
--disable-pip-version-check \
--no-cache-dir \
cargo-zigbuild==0.23.0 \
ziglang==0.16.0
echo "/opt/cargo-zigbuild/bin" >> "$GITHUB_PATH"
"$HOME/.cargo/bin/rustc" --version
/opt/cargo-zigbuild/bin/cargo-zigbuild --version
/opt/cargo-zigbuild/bin/python -m ziglang version
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Build frontend
working-directory: frontend
run: npm run build
- name: Test Rust application
run: |
export PATH="$HOME/.cargo/bin:$PATH"
export PIXHELF_SKIP_FRONTEND_BUILD=1
cargo test --locked
- name: Build static musl release binaries
run: |
set -eu
export PATH="/opt/cargo-zigbuild/bin:$HOME/.cargo/bin:$PATH"
export CARGO_ZIGBUILD_PYTHON_PATH=/opt/cargo-zigbuild/bin/python
export PIXHELF_SKIP_FRONTEND_BUILD=1
mkdir -p dist
build_release() {
target="$1"
asset="$2"
machine="$3"
binary="target/${target}/release/pixhelf"
cargo zigbuild --release --locked --target "$target"
file "$binary"
if ! readelf -h "$binary" | grep -q "Machine:.*${machine}"; then
echo "Unexpected architecture for ${binary}"
exit 1
fi
if readelf -l "$binary" | grep -q INTERP; then
echo "${binary} unexpectedly requires a dynamic loader"
exit 1
fi
if readelf -d "$binary" 2>/dev/null | grep -q NEEDED; then
echo "${binary} unexpectedly requires a shared library"
exit 1
fi
install -m 0755 "$binary" "dist/${asset}"
}
build_release \
x86_64-unknown-linux-musl \
pixhelf-amd64-linux \
'Advanced Micro Devices X86-64'
build_release \
aarch64-unknown-linux-musl \
pixhelf-arm64-linux \
AArch64
dist/pixhelf-amd64-linux --help >/dev/null
- name: Stage release inputs
if: forgejo.event_name != 'pull_request'
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: release-inputs
path: |
dist/
compose.yml
if-no-files-found: error
publish:
needs: build
if: forgejo.event_name != 'pull_request'
runs-on: docker
container:
image: docker.io/library/node:22-alpine
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
steps:
- name: Install OCI tooling
run: |
set -eu
apk add --no-cache ca-certificates coreutils curl tar
crane_archive=/tmp/go-containerregistry.tar.gz
curl --proto '=https' --tlsv1.2 --silent --show-error --fail --location \
https://github.com/google/go-containerregistry/releases/download/v0.21.7/go-containerregistry_Linux_x86_64.tar.gz \
--output "$crane_archive"
printf '%s %s\n' \
1a57bc98207fa1c0d04bf760699099e26f8383499bfd55b99c1b919a928a7230 \
"$crane_archive" \
| sha256sum --check -
tar -xzf "$crane_archive" -C /usr/local/bin crane
crane version
- name: Download release inputs
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: release-inputs
path: .
- name: Prepare release metadata
run: |
set -eu
ref="${FORGEJO_REF:-${GITHUB_REF:-}}"
ref_name="${FORGEJO_REF_NAME:-${GITHUB_REF_NAME:-}}"
event="${FORGEJO_EVENT_NAME:-${GITHUB_EVENT_NAME:-}}"
revision="${FORGEJO_SHA:-${GITHUB_SHA:-unknown}}"
server="${FORGEJO_SERVER_URL:-${GITHUB_SERVER_URL}}"
repository="${FORGEJO_REPOSITORY:-${GITHUB_REPOSITORY}}"
registry="${server#http://}"
registry="${registry#https://}"
registry="${registry%%/*}"
image="$(printf '%s/%s' "$registry" "$repository" | tr '[:upper:]' '[:lower:]')"
case "${event}:${ref}" in
push:refs/tags/v*)
release_tag="$ref_name"
version="${release_tag#v}"
is_release=true
;;
*)
release_tag=""
version="validation-$(printf '%.12s' "$revision")"
is_release=false
;;
esac
test -n "$version"
test -s dist/pixhelf-amd64-linux
test -s dist/pixhelf-arm64-linux
test -f compose.yml
printf 'IS_RELEASE=%s\n' "$is_release" >> "$GITHUB_ENV"
printf 'RELEASE_TAG=%s\n' "$release_tag" >> "$GITHUB_ENV"
printf 'IMAGE_VERSION=%s\n' "$version" >> "$GITHUB_ENV"
printf 'REVISION=%s\n' "$revision" >> "$GITHUB_ENV"
printf 'REGISTRY=%s\n' "$registry" >> "$GITHUB_ENV"
printf 'IMAGE=%s\n' "$image" >> "$GITHUB_ENV"
printf 'SOURCE_URL=%s/%s\n' "$server" "$repository" >> "$GITHUB_ENV"
- name: Log in to Forgejo Container Registry
if: forgejo.event_name == 'push' && startsWith(forgejo.ref, 'refs/tags/v')
run: |
set -eu
token="${REGISTRY_TOKEN:-${FORGEJO_TOKEN:-${GITHUB_TOKEN:-}}}"
username="${REGISTRY_USERNAME:-${FORGEJO_ACTOR:-${GITHUB_ACTOR:-}}}"
test -n "$token"
test -n "$username"
printf '%s' "$token" \
| crane auth login "$REGISTRY" --username "$username" --password-stdin
- name: Assemble multi-platform OCI image
run: |
set -eu
work=/tmp/pixhelf-oci
local_registry=127.0.0.1:5000
mkdir -p "$work"
crane registry serve --address "$local_registry" >"$work/registry.log" 2>&1 &
registry_pid=$!
cleanup() {
kill "$registry_pid" >/dev/null 2>&1 || true
wait "$registry_pid" >/dev/null 2>&1 || true
}
trap cleanup EXIT
sleep 1
build_platform() {
arch="$1"
asset="$2"
platform_dir="$work/$arch"
rootfs="$platform_dir/rootfs"
base_ref="$local_registry/staging:${arch}-base"
configured_ref="$local_registry/staging:${arch}"
mkdir -p "$rootfs"
install -m 0755 "$asset" "$rootfs/pixhelf"
tar \
--sort=name \
--mtime=@0 \
--owner=0 \
--group=0 \
--numeric-owner \
-cf "$platform_dir/layer.tar" \
-C "$rootfs" \
pixhelf
crane append --insecure --oci-empty-base \
--new_layer "$platform_dir/layer.tar" \
--new_tag "$base_ref"
crane mutate --insecure "$base_ref" \
--tag "$configured_ref" \
--set-platform "linux/$arch" \
--entrypoint /pixhelf \
--workdir /data \
--exposed-ports 3002/tcp \
--label org.opencontainers.image.title=Pixhelf \
--label 'org.opencontainers.image.description=Self-hosted image gallery' \
--label "org.opencontainers.image.version=$IMAGE_VERSION" \
--label "org.opencontainers.image.revision=$REVISION" \
--label "org.opencontainers.image.source=$SOURCE_URL"
crane config --insecure "$configured_ref" \
| ARCH="$arch" node -e '
let input = "";
process.stdin.on("data", (chunk) => input += chunk);
process.stdin.on("end", () => {
const config = JSON.parse(input);
if (config.os !== "linux" || config.architecture !== process.env.ARCH) {
throw new Error(`unexpected platform: ${config.os}/${config.architecture}`);
}
if (config.config.Entrypoint?.[0] !== "/pixhelf") {
throw new Error("unexpected entrypoint");
}
if (config.config.WorkingDir !== "/data") {
throw new Error("unexpected working directory");
}
if (!config.config.ExposedPorts?.["3002/tcp"]) {
throw new Error("missing exposed port");
}
});'
expected="$(sha256sum "$asset" | cut -d ' ' -f 1)"
actual="$(crane export --insecure "$configured_ref" - \
| tar -xOf - pixhelf \
| sha256sum \
| cut -d ' ' -f 1)"
test "$actual" = "$expected"
if [ "$IS_RELEASE" = true ]; then
crane pull --insecure "$configured_ref" "$platform_dir/image.tar"
digest="$(crane digest --tarball "$platform_dir/image.tar")"
crane push "$platform_dir/image.tar" "${IMAGE}@${digest}"
manifest_ref="${IMAGE}@${digest}"
else
manifest_ref="$configured_ref"
fi
}
build_platform amd64 dist/pixhelf-amd64-linux
amd64_manifest="$manifest_ref"
build_platform arm64 dist/pixhelf-arm64-linux
arm64_manifest="$manifest_ref"
if [ "$IS_RELEASE" = true ]; then
crane index append \
--manifest "$amd64_manifest" \
--manifest "$arm64_manifest" \
--tag "${IMAGE}:${IMAGE_VERSION}"
crane tag "${IMAGE}:${IMAGE_VERSION}" "$RELEASE_TAG"
case "$IMAGE_VERSION" in
*-*) ;;
*) crane tag "${IMAGE}:${IMAGE_VERSION}" latest ;;
esac
index_ref="${IMAGE}:${IMAGE_VERSION}"
else
index_ref="$local_registry/pixhelf:validation"
crane index append --insecure \
--manifest "$amd64_manifest" \
--manifest "$arm64_manifest" \
--tag "$index_ref"
fi
if [ "$IS_RELEASE" = true ]; then
index="$(crane index list "$index_ref")"
else
index="$(crane index list --insecure "$index_ref")"
fi
printf '%s\n' "$index"
printf '%s\n' "$index" | grep -q 'linux/amd64'
printf '%s\n' "$index" | grep -q 'linux/arm64'
- name: Create Forgejo release and upload assets
if: forgejo.event_name == 'push' && startsWith(forgejo.ref, 'refs/tags/v')
run: |
set -eu
token="${FORGEJO_TOKEN:-${GITHUB_TOKEN:-}}"
server="${FORGEJO_SERVER_URL:-${GITHUB_SERVER_URL}}"
repository="${FORGEJO_REPOSITORY:-${GITHUB_REPOSITORY}}"
api="${server}/api/v1"
test -n "$token"
release_json="$(mktemp)"
release_status="$(curl --silent --show-error --output "$release_json" --write-out '%{http_code}' \
--header "Authorization: token ${token}" \
"${api}/repos/${repository}/releases/tags/${RELEASE_TAG}")"
case "$release_status" in
200)
release_id="$(RELEASE_JSON="$release_json" node -e \
'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.env.RELEASE_JSON)).id))')"
;;
404)
release_payload="$(RELEASE_TAG="$RELEASE_TAG" node <<'NODE'
const tag = process.env.RELEASE_TAG;
process.stdout.write(JSON.stringify({
tag_name: tag,
target_commitish: tag,
name: tag,
body: `Automated release ${tag}`,
draft: false,
prerelease: tag.includes('-'),
hide_archive_links: true,
}));
NODE
)"
curl --silent --show-error --fail --output "$release_json" \
--request POST \
--header "Authorization: token ${token}" \
--header 'Content-Type: application/json' \
--data-binary "$release_payload" \
"${api}/repos/${repository}/releases"
release_id="$(RELEASE_JSON="$release_json" node -e \
'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.env.RELEASE_JSON)).id))')"
;;
*)
cat "$release_json"
exit 1
;;
esac
curl --silent --show-error --fail \
--request PATCH \
--header "Authorization: token ${token}" \
--header 'Content-Type: application/json' \
--data-binary '{"hide_archive_links":true}' \
"${api}/repos/${repository}/releases/${release_id}" \
>/dev/null
assets_json="$(mktemp)"
curl --silent --show-error --fail --output "$assets_json" \
--header "Authorization: token ${token}" \
"${api}/repos/${repository}/releases/${release_id}/assets"
delete_asset() {
asset_name="$1"
asset_id="$(ASSETS_JSON="$assets_json" ASSET_NAME="$asset_name" node -e \
'const assets = JSON.parse(require("fs").readFileSync(process.env.ASSETS_JSON)); const found = assets.find((asset) => asset.name === process.env.ASSET_NAME); process.stdout.write(found ? String(found.id) : "")')"
if [ -z "$asset_id" ]; then
return
fi
curl --silent --show-error --fail \
--request DELETE \
--header "Authorization: token ${token}" \
"${api}/repos/${repository}/releases/${release_id}/assets/${asset_id}"
echo "Deleted existing release asset: ${asset_name}"
}
delete_asset pixhelf-amd64
delete_asset pixhelf-arm64
for asset in dist/pixhelf-amd64-linux dist/pixhelf-arm64-linux compose.yml; do
asset_name="$(basename "$asset")"
delete_asset "$asset_name"
curl --silent --show-error --fail \
--request POST \
--header "Authorization: token ${token}" \
--header 'Content-Type: application/octet-stream' \
--data-binary "@${asset}" \
"${api}/repos/${repository}/releases/${release_id}/assets?name=${asset_name}"
printf '\n'
done