Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Published image. Local builds use the same Compose file and can override this.
TRUTHGATE_IMAGE=ghcr.io/magiccodingman/truthgate-ipfs:master
# Tested multi-platform release. Override this to pin a version or use a local image.
TRUTHGATE_IMAGE=magiccodingman/truthgate-ipfs:stable

# Build inputs. Both official base images are multi-platform.
DOTNET_VERSION=10.0
Expand Down
235 changes: 235 additions & 0 deletions .github/workflows/docker-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
name: Reusable Docker appliance CI

on:
workflow_call:

permissions:
contents: read

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_truthgate_portal() {
local asset=/tmp/truthgate-blazor.web.js

curl \
--silent \
--show-error \
--fail \
--insecure \
--header 'Accept-Encoding: identity' \
--output "${asset}" \
https://127.0.0.1:18443/_framework/blazor.web.js

local size
size="$(wc -c <"${asset}")"
echo "Blazor bootstrap size: ${size} bytes"
test "${size}" -gt 10000
}

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_truthgate_portal
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_truthgate_portal
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
1 change: 1 addition & 0 deletions .github/workflows/docker-repo-migration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Docker repository migration
on:
pull_request:
workflow_dispatch:
workflow_call:

permissions:
contents: read
Expand Down
Loading
Loading