diff --git a/.forgejo/scripts/publish-container.sh b/.forgejo/scripts/publish-container.sh new file mode 100755 index 0000000..7fe0956 --- /dev/null +++ b/.forgejo/scripts/publish-container.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${RELEASE_TAG:?RELEASE_TAG is required}" +: "${IMAGE_REPOSITORY:?IMAGE_REPOSITORY is required}" + +CRANE_BIN="${CRANE_BIN:-crane}" +ALPINE_IMAGE="${ALPINE_IMAGE:-docker.io/library/alpine:latest}" +DIST_DIR="${DIST_DIR:-dist}" +SOURCE_URL="${SOURCE_URL:-}" +REVISION="${REVISION:-$(git rev-parse HEAD)}" +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}" + +version="${RELEASE_TAG#v}" +if [ -z "$version" ]; then + echo "Release tag must contain a version" >&2 + exit 1 +fi +case "$SOURCE_DATE_EPOCH" in + *[!0-9]* | '') + echo "SOURCE_DATE_EPOCH must be an integer" >&2 + exit 1 + ;; +esac + +work_dir="$(mktemp -d /tmp/pixhelf-container.XXXXXX)" +cleanup() { + case "$work_dir" in + /tmp/pixhelf-container.*) rm -rf -- "$work_dir" ;; + *) echo "Refusing to remove unexpected path: $work_dir" >&2 ;; + esac +} +trap cleanup EXIT + +crane_args=() +if [ "${CRANE_INSECURE:-0}" = 1 ]; then + crane_args+=(--insecure) +fi +crane() { + "$CRANE_BIN" "${crane_args[@]}" "$@" +} + +created="$(date --utc --date="@$SOURCE_DATE_EPOCH" '+%Y-%m-%dT%H:%M:%SZ')" +platform_digest='' + +build_platform() { + local arch="$1" + local binary="$2" + local root="$work_dir/root-$arch" + local layer="$work_dir/pixhelf-$arch.tar" + local platform_ref="$IMAGE_REPOSITORY:$version-$arch" + + test -x "$binary" + install -d -m 0755 "$root/usr/local/bin" "$root/gallery" + install -d -m 0777 "$root/cache" + install -m 0755 "$binary" "$root/usr/local/bin/pixhelf" + + tar \ + --sort=name \ + --mtime="@$SOURCE_DATE_EPOCH" \ + --owner=0 \ + --group=0 \ + --numeric-owner \ + -C "$root" \ + -cf "$layer" \ + cache gallery usr + + crane append \ + --platform "linux/$arch" \ + --base "$ALPINE_IMAGE" \ + --new_layer "$layer" \ + --new_tag "$platform_ref" \ + --set-base-image-annotations + + crane mutate "$platform_ref" \ + --platform "linux/$arch" \ + --set-platform "linux/$arch" \ + --entrypoint=/usr/local/bin/pixhelf \ + --cmd=--gallery-dir \ + --cmd=/gallery \ + --cmd=--cache-dir \ + --cmd=/cache \ + --cmd=--listen \ + --cmd=0.0.0.0:3002 \ + --user=65532:65532 \ + --workdir=/ \ + --exposed-ports=3002/tcp \ + --label=org.opencontainers.image.title=Pixhelf \ + --label="org.opencontainers.image.description=Self-hosted image gallery" \ + --label="org.opencontainers.image.version=$version" \ + --label="org.opencontainers.image.revision=$REVISION" \ + --label="org.opencontainers.image.created=$created" \ + --label="org.opencontainers.image.source=$SOURCE_URL" \ + --tag "$platform_ref" + + platform_digest="$(crane digest "$platform_ref")" + echo "Published linux/$arch as $platform_ref@$platform_digest" +} + +build_platform amd64 "$DIST_DIR/pixhelf-amd64-linux" +amd64_digest="$platform_digest" +build_platform arm64 "$DIST_DIR/pixhelf-arm64-linux" +arm64_digest="$platform_digest" + +canonical_ref="$IMAGE_REPOSITORY:$version" +crane index append \ + --manifest "$IMAGE_REPOSITORY@$amd64_digest" \ + --manifest "$IMAGE_REPOSITORY@$arm64_digest" \ + --tag "$canonical_ref" + +if [ "$RELEASE_TAG" != "$version" ]; then + crane tag "$canonical_ref" "$RELEASE_TAG" +fi +case "$version" in + *-*) echo "Prerelease detected; latest tag was not updated" ;; + *) crane tag "$canonical_ref" latest ;; +esac + +crane manifest "$canonical_ref" | node -e ' +let input = ""; +process.stdin.on("data", (chunk) => input += chunk); +process.stdin.on("end", () => { + const index = JSON.parse(input); + const platforms = new Set((index.manifests ?? []).map( + (manifest) => `${manifest.platform?.os}/${manifest.platform?.architecture}`, + )); + for (const expected of ["linux/amd64", "linux/arm64"]) { + if (!platforms.has(expected)) { + throw new Error(`Published image is missing ${expected}`); + } + } + console.log(`Verified multi-platform image: ${[...platforms].sort().join(", ")}`); +}); +' + +echo "Published $canonical_ref" diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml index 696b837..76750bb 100644 --- a/.forgejo/workflows/release.yaml +++ b/.forgejo/workflows/release.yaml @@ -186,7 +186,7 @@ jobs: delete_asset pixhelf-amd64 delete_asset pixhelf-arm64 - for asset in dist/pixhelf-amd64-linux dist/pixhelf-arm64-linux; do + 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 \ @@ -197,3 +197,46 @@ jobs: "${api}/repos/${repository}/releases/${release_id}/assets?name=${asset_name}" printf '\n' done + + - name: Install container publishing tool + if: startsWith(forgejo.ref, 'refs/tags/v') + run: | + set -eu + crane_version=0.21.9 + crane_archive="go-containerregistry_Linux_x86_64.tar.gz" + crane_sha256=5c16d8ddb971cb1d5e6ed8b1e743da8224414eeba2c2762d8f1a61b2f095699e + crane_url="https://github.com/google/go-containerregistry/releases/download/v${crane_version}/${crane_archive}" + + curl --location --silent --show-error --fail \ + --output "/tmp/${crane_archive}" \ + "$crane_url" + printf '%s %s\n' "$crane_sha256" "/tmp/${crane_archive}" \ + | sha256sum --check + tar -xzf "/tmp/${crane_archive}" -C /usr/local/bin crane + crane version + + - name: Publish multi-platform Alpine container + if: startsWith(forgejo.ref, 'refs/tags/v') + run: | + set -eu + token="${FORGEJO_TOKEN:-${GITHUB_TOKEN:-}}" + actor="${FORGEJO_ACTOR:-${GITHUB_ACTOR}}" + server="${FORGEJO_SERVER_URL:-${GITHUB_SERVER_URL}}" + repository="${FORGEJO_REPOSITORY:-${GITHUB_REPOSITORY}}" + registry="${server#*://}" + registry="${registry%%/*}" + image_path="$(printf '%s' "$repository" | tr '[:upper:]' '[:lower:]')" + image_repository="${registry}/${image_path}" + test -n "$token" + + export DOCKER_CONFIG=/tmp/pixhelf-container-auth + install -d -m 0700 "$DOCKER_CONFIG" + printf '%s' "$token" \ + | crane auth login "$registry" --username "$actor" --password-stdin + + export RELEASE_TAG + export IMAGE_REPOSITORY="$image_repository" + export SOURCE_URL="${server}/${repository}" + export REVISION + REVISION="$(git rev-parse HEAD)" + .forgejo/scripts/publish-container.sh diff --git a/.gitignore b/.gitignore index fbba87c..64204ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /pic/ /.pixhelf-cache/ /target/ +/dist/ /frontend/node_modules/ /frontend/dist/ *.log - diff --git a/README.md b/README.md index 9358967..3749c77 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,33 @@ cargo build --release 两个 Release 产物都是无扩展名的独立可执行文件,下载后需要执行 `chmod +x pixhelf-amd64-linux` 或 `chmod +x pixhelf-arm64-linux`。 +## Docker Compose + +发布工作流还会将同一批 AMD64、ARM64 二进制分别装入 `alpine:latest`,并发布为 +`git.pixhelf.com/adminroot/pixhelf` 多架构镜像。最简安装方式: + +```bash +curl -LO https://git.pixhelf.com/adminroot/pixhelf/raw/branch/master/compose.yml +mkdir -p pic +docker compose up -d +``` + +打开 `http://localhost:3002`。`compose.yml` 默认只读挂载当前目录的 `./pic`,并用 +Docker volume 保存缩略图缓存。更新时执行: + +```bash +docker compose pull +docker compose up -d +``` + +默认使用 `latest`,也可以固定版本: + +```bash +PIXHELF_IMAGE=git.pixhelf.com/adminroot/pixhelf:0.1.7 docker compose up -d +``` + +容器以 UID/GID `65532:65532` 运行,因此图库目录和图片需要对该用户可读。 + ## 参数 ```text diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..dd551fb --- /dev/null +++ b/compose.yml @@ -0,0 +1,25 @@ +services: + pixhelf: + image: ${PIXHELF_IMAGE:-git.pixhelf.com/adminroot/pixhelf:latest} + pull_policy: always + restart: unless-stopped + init: true + ports: + - "${PIXHELF_PORT:-3002}:3002" + volumes: + - "${PIXHELF_GALLERY:-./pic}:/gallery:ro" + - pixhelf-cache:/cache + read_only: true + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:3002/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + pixhelf-cache: