The fork had CI and nothing after it. v2026.9.20 was tagged and released by hand, and there has never been an image: running INBUXA meant building the tree yourself, or using install.sh to do it for you. This adds the three workflows ihasmail already runs -- weekly release, publish, prune. Monday 10:07 UTC, and nothing on a quiet week. Last of the three, so INBUXA Admin and the webmail release ahead of the server they talk to, and staggered so a bad Monday names one repository rather than three. The version is the difference from ihasmail. ihasmail derives its version from the commit it builds, so its release only reads. INBUXA's lives in the brand_version! macro, deliberately apart from Cargo.toml so upstream's bumps merge without conflicts -- so the release writes it: the bump is committed to main and the tag names that commit. The tree a tag points at therefore reports the version the tag claims, which a tag placed beside an unbumped macro cannot promise. Both the bump and the read are scoped to the macro body and fail if they do not match exactly once. branding.rs holds other string literals, and a bump that silently edited one of those, or an image tagged from one, would be worse than a run that stops. The existing Dockerfile needs nothing: it cross-compiles from BUILDPLATFORM and takes no arguments beyond TARGETPLATFORM, so each architecture builds on its own native runner as ihasmail's does, without docker-bake.hcl. `docker build --check` is clean. Two things to expect from the first run. GHCR creates a package private the first time even in a public repository, and no workflow can change that, so the first image will refuse an anonymous pull until its visibility is set by hand. And a full Rust build of this tree is long; the per-platform GitHub Actions cache is what keeps the second one from being just as long, and it is worth watching that it stays inside the cache limit.
199 lines
8.6 KiB
YAML
199 lines
8.6 KiB
YAML
# Publish the container image to GHCR.
|
|
#
|
|
# The README and the docs site have told people to run
|
|
# `ghcr.io/inbuxa/inbuxa-server:latest` for a long time, and nothing ever
|
|
# pushed it: `docker pull` answered `denied`, because the package did not
|
|
# exist. This is the workflow that makes those instructions true. It is also
|
|
# the prerequisite for the self-hosted app catalogs -- TrueNAS and Unraid
|
|
# both install by pulling an image and neither builds from source.
|
|
#
|
|
# FIRST RUN: a package GHCR creates for the first time is **private**, even in
|
|
# a public repository, and an anonymous `docker pull` will still answer
|
|
# `denied`. Nothing in a workflow can change that -- the visibility is set once
|
|
# by hand under the package's settings, and until it is, this looks like it
|
|
# worked while the docs stay just as wrong as before. Check with a logged-out
|
|
# pull, not with one from a machine that has credentials.
|
|
#
|
|
# Two architectures, each built on its own native runner rather than under
|
|
# QEMU. Emulated arm64 has to run `npm ci` and the Vite build through
|
|
# instruction translation, which takes tens of minutes and occasionally runs
|
|
# out of memory; `ubuntu-24.04-arm` is free for public repositories and does
|
|
# the same work at native speed. The cost is the by-digest dance below: each
|
|
# runner pushes an untagged image, and a final job joins the two digests into
|
|
# one multi-arch tag.
|
|
name: Publish image
|
|
|
|
on:
|
|
release:
|
|
types: [published]
|
|
# Callable, so release.yml can build the release it just cut. This is not a
|
|
# stylistic choice: a release created with GITHUB_TOKEN does **not** raise a
|
|
# `release` event -- GitHub refuses to let a token trigger another workflow,
|
|
# to stop a workflow looping on its own output. A scheduled job that cut a
|
|
# release and expected this file to notice would silently never publish. The
|
|
# alternatives are a personal access token kept as a secret, or calling the
|
|
# workflow directly. This is the one that needs no credential.
|
|
workflow_call:
|
|
inputs:
|
|
ref:
|
|
description: "Tag, branch or SHA to build"
|
|
required: true
|
|
type: string
|
|
tag_latest:
|
|
description: "Also move :latest to this build"
|
|
type: boolean
|
|
default: false
|
|
# Same reasoning as ci.yml's dispatch trigger: a run GitHub queues and then
|
|
# orphans can be neither rerun nor canceled, and this workflow otherwise
|
|
# only fires on a release -- which is not something to cut twice because a
|
|
# runner died. `ref` also allows publishing an image for a tag that predates
|
|
# this workflow, which is how the first one gets built.
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: "Tag, branch or SHA to build"
|
|
required: true
|
|
default: main
|
|
tag_latest:
|
|
description: "Also move :latest to this build"
|
|
type: boolean
|
|
default: false
|
|
|
|
env:
|
|
# Hardcoded rather than derived from github.repository, which would have to
|
|
# be lowercased to be a legal registry path. This is the string the docs name.
|
|
IMAGE: ghcr.io/inbuxa/inbuxa-server
|
|
|
|
jobs:
|
|
# The version is read once and handed to both builds, so the two
|
|
# architectures cannot disagree about what they are. It is read from the
|
|
# macro the binary itself compiles in, which the weekly release commits
|
|
# before this runs -- so the image is tagged with the version it reports.
|
|
version:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
version: ${{ steps.v.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: ${{ inputs.ref || github.ref }}
|
|
- id: v
|
|
run: |
|
|
set -euo pipefail
|
|
# Scoped to the macro body: branding.rs holds other string literals,
|
|
# and tagging an image from one of those would be worse than failing.
|
|
V="$(awk '/macro_rules! brand_version/,/^}/' crates/types/src/branding.rs \
|
|
| grep -om1 '"[0-9][^"]*"' | tr -d '"')"
|
|
[ -n "$V" ] || { echo "could not read brand_version! from branding.rs" >&2; exit 1; }
|
|
# A date version carries nothing a Docker tag objects to, so there is
|
|
# no second, sanitized form of it here.
|
|
echo "version=$V" >> "$GITHUB_OUTPUT"
|
|
echo "version $V"
|
|
|
|
build:
|
|
needs: version
|
|
runs-on: ${{ matrix.runner }}
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: linux/amd64
|
|
runner: ubuntu-latest
|
|
- platform: linux/arm64
|
|
runner: ubuntu-24.04-arm
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: ${{ inputs.ref || github.ref }}
|
|
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
|
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Build and push by digest
|
|
id: push
|
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
|
with:
|
|
context: .
|
|
platforms: ${{ matrix.platform }}
|
|
# Attestations are off deliberately: they add manifests of their own
|
|
# to the index, and `imagetools create` below expects the two entries
|
|
# it pushed rather than four.
|
|
provenance: false
|
|
sbom: false
|
|
cache-from: type=gha,scope=${{ matrix.platform }}
|
|
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
|
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
|
- name: Save the digest
|
|
run: |
|
|
mkdir -p /tmp/digests
|
|
# The prefix is stripped here and put back in the merge job, so the
|
|
# filename is the bare hash. Leaving it on produces
|
|
# `image@sha256:sha256:...` when the reference is rebuilt.
|
|
digest="${{ steps.push.outputs.digest }}"
|
|
touch "/tmp/digests/${digest#sha256:}"
|
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
# One artifact per platform; the merge job globs them back together.
|
|
name: digest-${{ strategy.job-index }}
|
|
path: /tmp/digests/*
|
|
retention-days: 1
|
|
if-no-files-found: error
|
|
|
|
# Joins the per-architecture digests into a single tagged manifest, so
|
|
# `docker pull ghcr.io/inbuxa/inbuxa-server:<tag>` resolves on both.
|
|
publish:
|
|
needs: [version, build]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: /tmp/digests
|
|
pattern: digest-*
|
|
merge-multiple: true
|
|
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
|
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Create the manifest
|
|
run: |
|
|
# Arrays rather than a string: the tags and the digest references
|
|
# have to reach docker as separate arguments, and building them by
|
|
# word-splitting an unquoted variable is the version of this that
|
|
# breaks the day a value contains a space.
|
|
tags=(-t "${IMAGE}:${{ needs.version.outputs.version }}")
|
|
# :latest follows real releases only. A prerelease that moved it
|
|
# would hand every `:latest` deployment an unfinished build, and a
|
|
# dispatch run has to ask for it on purpose.
|
|
if [ "${{ github.event_name }}" = "release" ] && [ "${{ github.event.release.prerelease }}" = "false" ]; then
|
|
tags+=(-t "${IMAGE}:latest")
|
|
elif [ "${{ inputs.tag_latest }}" = "true" ]; then
|
|
tags+=(-t "${IMAGE}:latest")
|
|
fi
|
|
refs=()
|
|
for f in /tmp/digests/*; do
|
|
refs+=("${IMAGE}@sha256:$(basename "$f")")
|
|
done
|
|
echo "tags: ${tags[*]}"
|
|
echo "refs: ${refs[*]}"
|
|
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
|
- name: Show what landed
|
|
run: docker buildx imagetools inspect "${IMAGE}:${{ needs.version.outputs.version }}"
|
|
|
|
# Runs only after a successful publish, because that is the only moment the
|
|
# package grows. See cleanup.yml for why this is not the obvious one-liner.
|
|
prune:
|
|
needs: publish
|
|
permissions:
|
|
packages: write
|
|
uses: ./.github/workflows/cleanup.yml
|