Skip to content

Add single-container Docker appliance and multi-arch publishing #38

Add single-container Docker appliance and multi-arch publishing

Add single-container Docker appliance and multi-arch publishing #38

Workflow file for this run

name: Docker appliance
on:
pull_request:
push:
branches:
- master
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: docker-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE_NAME: ghcr.io/magiccodingman/truthgate-ipfs
jobs:
validate-compose:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Validate production Compose
run: docker compose -f compose.yaml config
- name: Validate development override
run: docker compose -f compose.yaml -f compose.dev.yaml config
- name: Validate container scripts
run: |
bash -n docker/entrypoint.sh
bash -n docker/healthcheck.sh
bash -n docker/kubo-configure.sh
bash -n docker/kubo-status.sh
smoke-test:
strategy:
fail-fast: false
matrix:
include:
- architecture: amd64
runner: ubuntu-24.04
- architecture: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- name: Build production image (${{ matrix.architecture }})
shell: bash
run: |
set -o pipefail
docker build \
--progress=plain \
--platform linux/${{ matrix.architecture }} \
--target production \
--tag truthgate-ipfs:test-${{ matrix.architecture }} \
. 2>&1 | tee production-image-build-${{ matrix.architecture }}.log
- name: Upload production build diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: production-image-build-${{ matrix.architecture }}
path: production-image-build-${{ matrix.architecture }}.log
if-no-files-found: error
- name: Build development image (${{ matrix.architecture }})
shell: bash
run: |
set -o pipefail
docker build \
--progress=plain \
--platform linux/${{ matrix.architecture }} \
--target development \
--tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \
. 2>&1 | tee development-image-build-${{ matrix.architecture }}.log
- name: Upload development build diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: development-image-build-${{ matrix.architecture }}
path: development-image-build-${{ matrix.architecture }}.log
if-no-files-found: ignore
- name: Start, replace, and revalidate the appliance
shell: bash
run: |
exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1
set -Eeuo pipefail
export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }}
export TRUTHGATE_HTTP_PORT=18080
export TRUTHGATE_HTTPS_PORT=18443
export IPFS_SWARM_PORT=14001
export TRUTHGATE_KUBO_PUBLIC_IPV4=off
export TRUTHGATE_KUBO_PUBLIC_IPV6=off
export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate
export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo
export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks
cleanup() {
docker compose logs --no-color 2>/dev/null || true
docker compose down --remove-orphans 2>/dev/null || true
}
trap cleanup EXIT
wait_for_health() {
for attempt in $(seq 1 60); do
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)"
if [[ "${status}" == "healthy" ]]; then
return 0
fi
if [[ "${status}" == "unhealthy" ]]; then
echo "TruthGate became unhealthy."
return 1
fi
sleep 5
done
echo "TruthGate did not become healthy in time."
return 1
}
read_peer_id() {
docker exec truthgate \
ipfs --api=/ip4/127.0.0.1/tcp/5001 id \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])'
}
read_bootstrap_hash() {
docker exec truthgate \
sha256sum /data/truthgate/state/bootstrap-admin-password \
| cut -d' ' -f1
}
read_settings_hash() {
docker exec truthgate \
sha256sum /data/truthgate/config/kubo-settings.json \
| cut -d' ' -f1
}
validate_kubo_server_defaults() {
test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver"
test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true"
test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true"
test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true"
test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all"
test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h"
test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true"
test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true"
test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90"
storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)"
test "${storage_max}" != "10GB"
[[ "${storage_max}" =~ ^[0-9]+B$ ]]
docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1
docker exec truthgate test -s /data/truthgate/config/kubo-settings.json
docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json
docker exec truthgate \
ipfs config --json Addresses.Swarm \
| jq -e '
index("/ip4/0.0.0.0/tcp/4001") and
index("/ip6/::/tcp/4001") and
index("/ip4/0.0.0.0/udp/4001/quic-v1") and
index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and
index("/ip6/::/udp/4001/quic-v1") and
index("/ip6/::/udp/4001/quic-v1/webtransport")
'
test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001"
test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010"
docker exec truthgate truthgate-kubo-status
}
mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks
docker compose up --detach --no-build
wait_for_health
# Read protected state from inside the container. The host runner is
# intentionally unable to inspect the bootstrap secret directly.
docker exec truthgate test -s /data/ipfs/repo/config
docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password
validate_kubo_server_defaults
peer_id_before="$(read_peer_id)"
bootstrap_hash_before="$(read_bootstrap_hash)"
settings_hash_before="$(read_settings_hash)"
# Replace the container while retaining only the documented bind-mounted
# state. Kubo identity, managed settings, and first-run credentials must
# remain stable.
docker compose down --remove-orphans
docker compose up --detach --no-build
wait_for_health
docker exec truthgate test -s /data/ipfs/repo/config
docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password
validate_kubo_server_defaults
peer_id_after="$(read_peer_id)"
bootstrap_hash_after="$(read_bootstrap_hash)"
settings_hash_after="$(read_settings_hash)"
test "${peer_id_before}" = "${peer_id_after}"
test "${bootstrap_hash_before}" = "${bootstrap_hash_after}"
test "${settings_hash_before}" = "${settings_hash_after}"
docker compose logs --no-color
- name: Upload runtime diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: runtime-smoke-${{ matrix.architecture }}
path: runtime-smoke-${{ matrix.architecture }}.log
if-no-files-found: ignore
build-and-publish:
if: github.event_name != 'pull_request'
needs:
- validate-compose
- smoke-test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }}
type=sha,prefix=sha-
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Publish AMD64 + ARM64 under one image tag
uses: docker/build-push-action@v6
with:
context: .
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true