411 lines
15 KiB
YAML
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
|