name: Build and publish Docker image on: pull_request: paths: - "apps/api/**" - "apps/web/**" - "packages/**" - "docs/**" - "scripts/self-hosted-*.mjs" - "migrations/**" - "patches/**" - "Dockerfile" - ".dockerignore" - "compose.yaml" - "bunfig.toml" - "package.json" - "bun.lock" - "tailwind.config.ts" - "tsconfig.json" - ".github/workflows/docker-image.yml" - ".github/workflows/docker-tcr-mirror.yml" - ".cnb.yml" - "scripts/publish-tcr-image.sh" - "scripts/prune-tcr-sha-tags.mjs" push: branches: - main paths: - "apps/api/**" - "apps/web/**" - "packages/**" - "docs/**" - "scripts/self-hosted-*.mjs" - "migrations/**" - "patches/**" - "Dockerfile" - ".dockerignore" - "compose.yaml" - "bunfig.toml" - "package.json" - "bun.lock" - "tailwind.config.ts" - "tsconfig.json" - ".github/workflows/docker-image.yml" - ".github/workflows/docker-tcr-mirror.yml" - ".cnb.yml" - "scripts/publish-tcr-image.sh" - "scripts/prune-tcr-sha-tags.mjs" workflow_dispatch: inputs: release_tag: description: "Draft release tag to publish (for example v1.33.0); leave empty for main" required: false type: string release: types: - published permissions: contents: read env: GHCR_IMAGE_NAME: ghcr.io/tianma-if/edgeever jobs: container-test: if: github.repository == 'tianma-if/edgeever' && github.event_name != 'release' name: Build and verify container runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: Build local image run: | docker build \ --build-arg EDGE_EVER_BUILD_ID="${GITHUB_SHA}" \ --build-arg EDGE_EVER_BUILD_CREATED_AT="2026-01-02T03:04:05Z" \ --build-arg EDGE_EVER_BUILD_TRIGGER=main_push \ --build-arg EDGE_EVER_BUILD_METHOD=github_actions \ --tag edgeever:ci . docker build --tag edgeever:ci-default . - name: Start container run: | docker run --detach --name edgeever-ci \ --env EDGE_EVER_AUTH_PASSWORD=container-ci-password \ --env EDGE_EVER_DEPLOYMENT_TRIGGER=manual \ --env EDGE_EVER_DEPLOYMENT_METHOD=docker \ --publish 127.0.0.1:8787:8787 \ edgeever:ci - name: Verify shared API and persistent storage run: | for attempt in $(seq 1 60); do if health="$(curl --fail --silent --show-error http://127.0.0.1:8787/api/health)"; then break fi sleep 1 done echo "${health}" | jq -e '.ok == true and .runtime == "self-hosted-bun" and .authMode == "required"' echo "${health}" | jq -e '.buildCreatedAt == "2026-01-02T03:04:05.000Z" and .buildProvenance == {"trigger": "main_push", "method": "github_actions"}' echo "${health}" | jq -e '.deployment == {"trigger": "manual", "method": "docker"} and .deploymentVersionCreatedAt == null and .containerImageSource == "unknown"' curl --fail --silent --show-error http://127.0.0.1:8787/ | grep -F '' docker exec edgeever-ci test -f /data/edgeever.sqlite docker exec edgeever-ci test -d /data/resources docker exec edgeever-ci test -s /app/scripts/self-hosted-server.js docker exec edgeever-ci test -s /app/scripts/self-hosted-server.mjs docker exec edgeever-ci cmp -s /app/scripts/self-hosted-server.js /app/scripts/self-hosted-server.mjs docker exec edgeever-ci test ! -e /app/node_modules docker exec edgeever-ci test ! -e /app/apps/api docker exec edgeever-ci test ! -e /app/packages - name: Stop container gracefully run: | docker stop --timeout 30 edgeever-ci docker logs edgeever-ci | grep -F '[self-hosted] shutdown complete' - name: Verify a default self-built image reports unknown build metadata run: | docker run --detach --name edgeever-ci-default \ --env EDGE_EVER_AUTH_PASSWORD=container-ci-password \ --env EDGE_EVER_BUILD_TRIGGER=not-a-trigger \ --env EDGE_EVER_BUILD_CREATED_AT=not-a-date \ --publish 127.0.0.1:8788:8787 \ edgeever:ci-default for attempt in $(seq 1 60); do if health="$(curl --fail --silent --show-error http://127.0.0.1:8788/api/health)"; then break fi sleep 1 done echo "${health}" | jq -e '.build == "unknown" and .buildCreatedAt == null and .buildProvenance == {"trigger": "unknown", "method": "unknown"}' echo "${health}" | jq -e '.deployment == {"trigger": "unknown", "method": "unknown"} and .deploymentVersionCreatedAt == null' docker stop --timeout 30 edgeever-ci-default - name: Show container logs on failure if: failure() run: | docker logs edgeever-ci || true docker logs edgeever-ci-default || true publish: if: >- github.repository == 'tianma-if/edgeever' && github.event_name != 'pull_request' && github.event_name != 'release' needs: container-test name: Publish official multi-platform image runs-on: ubuntu-latest timeout-minutes: 60 permissions: contents: write packages: write steps: - name: Checkout uses: actions/checkout@v5 - name: Resolve image tags id: image env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ inputs.release_tag }} run: | echo "created_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}" if [[ -n "${RELEASE_TAG}" ]]; then if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "release_tag must match vX.Y.Z" >&2 exit 1 fi release_json="$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json isDraft,isPrerelease,targetCommitish)" if [[ "$(jq -r '.isDraft' <<<"${release_json}")" != "true" ]] || [[ "$(jq -r '.isPrerelease' <<<"${release_json}")" != "false" ]]; then echo "${RELEASE_TAG} must identify a non-prerelease Draft release" >&2 exit 1 fi version="${RELEASE_TAG#v}" expected_version="$(jq -r '.version' package.json)" target_commitish="$(jq -r '.targetCommitish' <<<"${release_json}")" if [[ "${version}" != "${expected_version}" ]] || [[ "${target_commitish}" != "${GITHUB_SHA}" ]]; then echo "Draft tag, package version, target commit, and workflow commit must match" >&2 exit 1 fi { echo "build_trigger=github_release" echo "tags<> "${GITHUB_OUTPUT}" else short_sha="${GITHUB_SHA:0:12}" # A dispatch without release_tag is an ad-hoc rebuild of main. if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then build_trigger=main_push else build_trigger=manual fi { echo "build_trigger=${build_trigger}" echo "tags<> "${GITHUB_OUTPUT}" fi - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Sign in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and publish image uses: docker/build-push-action@v6 with: context: . build-args: | EDGE_EVER_BUILD_ID=${{ github.sha }} EDGE_EVER_BUILD_CREATED_AT=${{ steps.image.outputs.created_at }} EDGE_EVER_BUILD_TRIGGER=${{ steps.image.outputs.build_trigger }} EDGE_EVER_BUILD_METHOD=github_actions platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.image.outputs.tags }} labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.created=${{ steps.image.outputs.created_at }} org.opencontainers.image.version=${{ steps.image.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max - name: Verify anonymous GHCR image access run: | docker logout ghcr.io docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" docker buildx imagetools inspect --raw "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" | jq -e '([.manifests[].platform.architecture] | index("amd64") != null) and ([.manifests[].platform.architecture] | index("arm64") != null)' docker pull "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" container_name=edgeever-published-audit trap 'docker rm --force "${container_name}" >/dev/null 2>&1 || true' EXIT docker run --detach --name "${container_name}" \ --env EDGE_EVER_AUTH_PASSWORD=published-container-audit-password \ --publish 127.0.0.1:8787:8787 \ "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}" for attempt in $(seq 1 60); do if health="$(curl --fail --silent --show-error http://127.0.0.1:8787/api/health)"; then break fi sleep 1 done echo "${health}" | jq -e '.ok == true and .runtime == "self-hosted-bun" and .authMode == "required"' echo "${health}" | jq -e --arg trigger "${{ steps.image.outputs.build_trigger }}" '.buildProvenance == {"trigger": $trigger, "method": "github_actions"} and (.buildCreatedAt | type) == "string"' curl --fail --silent --show-error http://127.0.0.1:8787/ | grep -F '' docker exec "${container_name}" test -s /app/scripts/self-hosted-server.js docker exec "${container_name}" test -s /app/scripts/self-hosted-server.mjs docker exec "${container_name}" cmp -s /app/scripts/self-hosted-server.js /app/scripts/self-hosted-server.mjs docker exec "${container_name}" test ! -e /app/node_modules docker exec "${container_name}" test ! -e /app/apps/api docker exec "${container_name}" test ! -e /app/packages docker stop --timeout 30 "${container_name}" docker logs "${container_name}" | grep -F '[self-hosted] shutdown complete' docker rm "${container_name}" trap - EXIT release-audit: if: github.repository == 'tianma-if/edgeever' && github.event_name == 'release' name: Audit published Docker image runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Verify prepared public image tags env: RELEASE_TAG: ${{ github.event.release.tag_name }} run: | release_inspect="$(docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${RELEASE_TAG}")" latest_inspect="$(docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:latest")" release_digest="$(sed -n 's/^Digest:[[:space:]]*//p' <<<"${release_inspect}" | head -n 1)" latest_digest="$(sed -n 's/^Digest:[[:space:]]*//p' <<<"${latest_inspect}" | head -n 1)" test -n "${release_digest}" test "${release_digest}" = "${latest_digest}" docker buildx imagetools inspect --raw "${GHCR_IMAGE_NAME}:${RELEASE_TAG}" | jq -e '([.manifests[].platform.architecture] | index("amd64") != null) and ([.manifests[].platform.architecture] | index("arm64") != null)'