From b0f40ba8a7080dd5e04c8e3b9a4325d18fc6fd8d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:21:13 -0400 Subject: [PATCH 01/10] Extract reusable Docker appliance CI --- .github/workflows/docker-ci.yml | 235 ++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 .github/workflows/docker-ci.yml diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..fe07ba8 --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -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 From b4ff52323df70524b7b215ff73ed646ad31fdc0e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:21:31 -0400 Subject: [PATCH 02/10] Reuse Docker appliance CI before GHCR publishing --- .github/workflows/docker.yml | 209 +---------------------------------- 1 file changed, 6 insertions(+), 203 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7ec5080..94e0940 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,214 +21,17 @@ 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 + appliance-ci: + name: Validate and smoke test appliance + uses: ./.github/workflows/docker-ci.yml build-and-publish: + name: Publish rolling GHCR image if: github.event_name != 'pull_request' needs: - - validate-compose - - smoke-test + - appliance-ci runs-on: ubuntu-24.04 + steps: - uses: actions/checkout@v4 From 4c4be064dc1f6225f6bd3b66518d26e96f622ded Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:03 -0400 Subject: [PATCH 03/10] Add gated Docker Hub stable release workflow --- .github/workflows/dockerhub-stable.yml | 205 +++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 .github/workflows/dockerhub-stable.yml diff --git a/.github/workflows/dockerhub-stable.yml b/.github/workflows/dockerhub-stable.yml new file mode 100644 index 0000000..fe37b0c --- /dev/null +++ b/.github/workflows/dockerhub-stable.yml @@ -0,0 +1,205 @@ +name: Docker Hub stable release + +on: + push: + branches: + - stable + +permissions: + contents: write + +concurrency: + group: dockerhub-stable-release + cancel-in-progress: false + +jobs: + appliance-ci: + name: Validate and smoke test appliance + uses: ./.github/workflows/docker-ci.yml + + repository-migration: + name: Validate legacy Kubo migration + uses: ./.github/workflows/docker-repo-migration.yml + + tls-lifecycle: + name: Validate TLS lifecycle + uses: ./.github/workflows/tls-lifecycle.yml + + prepare-release: + name: Allocate stable version + needs: + - appliance-ci + - repository-migration + - tls-lifecycle + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + series: ${{ steps.version.outputs.series }} + tag: ${{ steps.version.outputs.tag }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Calculate idempotent patch version + id: version + shell: bash + run: | + set -Eeuo pipefail + + series="$(tr -d '[:space:]' < VERSION)" + if [[ ! "${series}" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "VERSION must contain only a major.minor series such as 0.1." >&2 + exit 1 + fi + + git fetch --force --tags origin + + existing_tag="" + while IFS= read -r candidate; do + [[ -n "${candidate}" ]] || continue + if [[ "${candidate}" =~ ^v${series//./\.}\.([0-9]+)$ ]]; then + existing_tag="${candidate}" + fi + done < <(git tag --points-at "${GITHUB_SHA}" --list "v${series}.*" | sort -V) + + if [[ -n "${existing_tag}" ]]; then + tag="${existing_tag}" + version="${tag#v}" + echo "Reusing ${tag}; this commit was already assigned a stable version." + else + max_patch=-1 + + while IFS= read -r candidate; do + [[ -n "${candidate}" ]] || continue + + if [[ "${candidate}" =~ ^v${series//./\.}\.([0-9]+)$ ]]; then + patch="${BASH_REMATCH[1]}" + if (( patch > max_patch )); then + max_patch="${patch}" + fi + fi + done < <(git tag --list "v${series}.*") + + version="${series}.$((max_patch + 1))" + tag="v${version}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag --annotate "${tag}" --message "TruthGate ${tag}" "${GITHUB_SHA}" + git push origin "${tag}" + + echo "Created stable release tag ${tag}." + fi + + echo "series=${series}" >>"${GITHUB_OUTPUT}" + echo "version=${version}" >>"${GITHUB_OUTPUT}" + echo "tag=${tag}" >>"${GITHUB_OUTPUT}" + + publish-dockerhub: + name: Publish Docker Hub image + needs: + - prepare-release + runs-on: ubuntu-24.04 + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs + VERSION: ${{ needs.prepare-release.outputs.version }} + SERIES: ${{ needs.prepare-release.outputs.series }} + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate stable image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ env.VERSION }},priority=900 + type=raw,value=${{ env.SERIES }},priority=800 + type=raw,value=stable,priority=700 + type=raw,value=latest,priority=600 + type=sha,prefix=sha-,priority=100 + labels: | + org.opencontainers.image.title=TruthGate + org.opencontainers.image.description=Secure, self-hosted IPFS edge gateway with HTTPS, authentication, GUI, and Kubo. + org.opencontainers.image.version=${{ env.VERSION }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + + - name: Publish AMD64 + ARM64 to Docker Hub + 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 + + - name: Verify published multi-platform manifest + shell: bash + run: | + set -Eeuo pipefail + + docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}" \ + | tee dockerhub-manifest.txt + + grep -F 'linux/amd64' dockerhub-manifest.txt + grep -F 'linux/arm64' dockerhub-manifest.txt + + - name: Upload Docker Hub manifest diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: dockerhub-manifest-${{ env.VERSION }} + path: dockerhub-manifest.txt + if-no-files-found: ignore + + update-dockerhub-description: + name: Sync Docker Hub README + needs: + - prepare-release + - publish-dockerhub + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + + - name: Update Docker Hub overview from README + uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs + short-description: Secure, self-hosted IPFS edge gateway with HTTPS, authentication, GUI, and Kubo. + readme-filepath: ./README.md + enable-url-completion: true + + - name: Stable release summary + shell: bash + run: | + { + echo "## TruthGate stable release" + echo + echo "- Git tag: \`${{ needs.prepare-release.outputs.tag }}\`" + echo "- Docker image: \`${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs:${{ needs.prepare-release.outputs.version }}\`" + echo "- Moving tags: \`stable\`, \`latest\`, and \`${{ needs.prepare-release.outputs.series }}\`" + echo "- Architectures: \`linux/amd64\`, \`linux/arm64\`" + echo "- Docker Hub overview synchronized from \`README.md\`" + } >>"${GITHUB_STEP_SUMMARY}" From d61676ed6493361ab4626547596edcebf76e84b3 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:28 -0400 Subject: [PATCH 04/10] Make repository migration tests reusable --- .github/workflows/docker-repo-migration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 69dd808..28e9c40 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -3,6 +3,7 @@ name: Docker repository migration on: pull_request: workflow_dispatch: + workflow_call: permissions: contents: read From 06cea5f512d5f0a0814dea4948095117a5854238 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:43 -0400 Subject: [PATCH 05/10] Make TLS lifecycle tests reusable --- .github/workflows/tls-lifecycle.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 3f1191b..839e3e5 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -11,6 +11,10 @@ on: paths: - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" + workflow_call: + +permissions: + contents: read jobs: test: From 5d99129d234729433d8c37cbbb58eacc2d325926 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:54 -0400 Subject: [PATCH 06/10] Set initial stable release series --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 From 56cdd47a414c53f308ca2f945f9e5475c831d49d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:23:16 -0400 Subject: [PATCH 07/10] Default Compose to Docker Hub stable image --- compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yaml b/compose.yaml index d01c7f3..114ce8f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ name: truthgate services: truthgate: - image: ${TRUTHGATE_IMAGE:-ghcr.io/magiccodingman/truthgate-ipfs:master} + image: ${TRUTHGATE_IMAGE:-magiccodingman/truthgate-ipfs:stable} build: context: . dockerfile: Dockerfile From 271454fbdfaac2e4bc4fad6a38168cca113e9ade Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:23:35 -0400 Subject: [PATCH 08/10] Point example configuration at stable image --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 64c411a..2cdb4ca 100644 --- a/.env.example +++ b/.env.example @@ -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 From 3fab15b23dcc6c5d9e8131962f18f81a7ecb09bc Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:24:13 -0400 Subject: [PATCH 09/10] Document the stable Docker Hub image flow --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 437c784..ece6c62 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,20 @@ container. Application state, Kubo repository metadata, and Kubo blocks are persisted separately by default. ```bash +git clone https://github.com/magiccodingman/TruthGate-IPFS.git +cd TruthGate-IPFS cp .env.example .env -docker compose up --build -d +docker compose pull +docker compose up -d docker compose logs truthgate ``` +The default Compose image is the tested multi-platform stable release: + +```text +magiccodingman/truthgate-ipfs:stable +``` + The first-start logs contain a generated password for the `admin` account. Open `https://localhost`, accept the temporary self-signed fallback certificate, and change the password. @@ -43,10 +52,16 @@ overrides. Inspect the live node with: docker exec truthgate truthgate-kubo-status ``` -For the full persistence contract, Kubo settings, image update flow, -ARM64/AMD64 publishing, and Docker-based development setup, see +For the full persistence contract, Kubo settings, release tags, image update +flow, ARM64/AMD64 publishing, and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. +To build the production image locally instead of pulling the stable release: + +```bash +docker compose up --build -d +``` + Development with hot reload uses the production definition plus a small override: From 8f0a7d20342e4ad92cbbee9e4e2c05ac06a38ae9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:25:11 -0400 Subject: [PATCH 10/10] Document automatic stable release tags --- DOCKER.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 9222885..8c3c118 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -21,12 +21,17 @@ an operator explicitly overrides them. ## Production quick start ```bash +git clone https://github.com/magiccodingman/TruthGate-IPFS.git +cd TruthGate-IPFS cp .env.example .env -docker compose up --build -d +docker compose pull +docker compose up -d ``` -Open `https://localhost` (the first connection uses TruthGate's self-signed -fallback certificate unless a configured domain has an issued certificate). +The default Compose image is `magiccodingman/truthgate-ipfs:stable`. Open +`https://localhost` after the container becomes healthy. The first connection +uses TruthGate's self-signed fallback certificate unless a configured domain +has an issued certificate. On the first boot, retrieve the generated administrator password with: @@ -38,17 +43,47 @@ The username is `admin`. Change its password in the TruthGate UI. Until the configuration is persisted, the bootstrap password is also retained at `data/truthgate/state/bootstrap-admin-password` with restrictive permissions. -## Pulling the published image +To build the production image locally instead of pulling the stable release: + +```bash +docker compose up --build -d +``` + +## Published image tags + +A successful promotion to the protected `stable` branch runs the complete +AMD64/ARM64 appliance, legacy-repository migration, and TLS lifecycle gates +before Docker Hub publishing starts. + +The release workflow publishes: + +```text +magiccodingman/truthgate-ipfs:stable +magiccodingman/truthgate-ipfs:latest +magiccodingman/truthgate-ipfs:0.1 +magiccodingman/truthgate-ipfs:0.1.0 +magiccodingman/truthgate-ipfs:sha- +``` + +`stable` and `latest` move to the newest successful stable release. The +major/minor series tag, such as `0.1`, also moves forward. Full semantic +versions are immutable release identifiers. `VERSION` contains the intentional +major/minor series; every successful stable promotion automatically allocates +the next patch version. + +The same multi-platform tag resolves to the correct `linux/amd64` or +`linux/arm64` image automatically. Replacing the container does not replace +mounted state. -After the multi-platform image has been published by GitHub Actions: +To update an existing deployment: ```bash docker compose pull docker compose up -d ``` -The same `master` tag resolves to the correct `linux/amd64` or `linux/arm64` -image automatically. Replacing the container does not replace mounted state. +Pin `TRUTHGATE_IMAGE` in `.env` to a full version when an installation should +not automatically follow the moving `stable` tag. ## Persistent storage contract