diff --git a/Dockerfile b/Dockerfile --- a/Dockerfile +++ b/Dockerfile @@ -1,61 +1,54 @@ -ARG SOURCE_DATE_EPOCH=0 - -FROM rust:1.95.0-bookworm@sha256:6258907abe69656e41cd992e0b705cdcfabcbbe3db374f92ed2d47121282d4a1 AS source - -WORKDIR /src -COPY . . - -# The test suite is deliberately absent here. Its store tests refuse to pass -# without a Postgres to run against, and an image build has none; running the -# rest and calling it validation would say "verified" about the part that is -# not. The full suite, database included, runs in pull-request.yml. -FROM source AS validation -ARG TARGETARCH -RUN --mount=type=cache,id=syncode-control-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,id=syncode-control-validation-${TARGETARCH},target=/src/target,sharing=locked \ - --mount=type=secret,id=syncode_git_user,env=SYNCODE_GIT_USER \ - --mount=type=secret,id=syncode_git_token,env=SYNCODE_GIT_TOKEN \ - AUTHORIZATION="$(printf '%s:%s' "${SYNCODE_GIT_USER}" "${SYNCODE_GIT_TOKEN}" | base64 | tr -d '\n')" \ - && git config --global http.https://syncode.sh/.extraheader "AUTHORIZATION: basic ${AUTHORIZATION}" \ - && cargo fmt --check \ - && cargo clippy --workspace --all-targets --all-features -- -D warnings \ - && ./scripts/check-architecture.sh \ - && ./scripts/check-rust-loc.sh \ - && git config --global --unset-all http.https://syncode.sh/.extraheader - -FROM source AS builder -ARG TARGETARCH -RUN --mount=type=cache,id=syncode-control-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \ - --mount=type=cache,id=syncode-control-release-${TARGETARCH},target=/src/target,sharing=locked \ - --mount=type=secret,id=syncode_git_user,env=SYNCODE_GIT_USER \ - --mount=type=secret,id=syncode_git_token,env=SYNCODE_GIT_TOKEN \ - AUTHORIZATION="$(printf '%s:%s' "${SYNCODE_GIT_USER}" "${SYNCODE_GIT_TOKEN}" | base64 | tr -d '\n')" \ - && git config --global http.https://syncode.sh/.extraheader "AUTHORIZATION: basic ${AUTHORIZATION}" \ - && cargo build --locked --release \ - && install -D -m 0755 target/release/syncode-control /out/syncode-control \ - && git config --global --unset-all http.https://syncode.sh/.extraheader - -FROM debian:bookworm-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818 - -ARG VERSION=dev - -LABEL org.opencontainers.image.source="https://syncode.sh/syncode/control" -LABEL org.opencontainers.image.version="${VERSION}" - -COPY --from=source /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=builder /out/syncode-control /usr/local/bin/syncode-control - -# Nodes open their session on one port and the forge delivers events on the -# other. They are separate because they are addressed differently: one is -# reached by runners, the other only by the forge. -EXPOSE 8090 8091 - -USER nobody - -ENTRYPOINT ["/usr/local/bin/syncode-control"] - -# The binary defaults to the loopback, which is right when someone runs it on -# their own machine and useless inside a container: the network is the only way -# in. The image therefore listens on every interface, and the container's own -# isolation is what keeps that private. -CMD ["--listen", "0.0.0.0:8090", "--listen-events", "0.0.0.0:8091"] +ARG SOURCE_DATE_EPOCH=0 + +FROM rust:1.95.0-bookworm@sha256:6258907abe69656e41cd992e0b705cdcfabcbbe3db374f92ed2d47121282d4a1 AS source + +WORKDIR /src +COPY . . + +# The test suite is deliberately absent here. Its store tests refuse to pass +# without a Postgres to run against, and an image build has none; running the +# rest and calling it validation would say "verified" about the part that is +# not. The full suite, database included, runs in pull-request.yml. +FROM source AS validation +ARG TARGETARCH +RUN --mount=type=cache,id=syncode-control-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,id=syncode-control-validation-${TARGETARCH},target=/src/target,sharing=locked \ + --mount=type=secret,id=syncode_git_config,target=/root/.gitconfig,required=true \ + cargo fmt --check \ + && cargo clippy --workspace --all-targets --all-features -- -D warnings \ + && cargo test --workspace --all-targets --all-features \ + && ./scripts/check-architecture.sh \ + && ./scripts/check-rust-loc.sh + +FROM source AS builder +ARG TARGETARCH +RUN --mount=type=cache,id=syncode-control-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,id=syncode-control-release-${TARGETARCH},target=/src/target,sharing=locked \ + --mount=type=secret,id=syncode_git_config,target=/root/.gitconfig,required=true \ + cargo build --locked --release \ + && install -D -m 0755 target/release/syncode-control /out/syncode-control + +FROM debian:bookworm-slim@sha256:7b140f374b289a7c2befc338f42ebe6441b7ea838a042bbd5acbfca6ec875818 + +ARG VERSION=dev + +LABEL org.opencontainers.image.source="https://syncode.sh/syncode/control" +LABEL org.opencontainers.image.version="${VERSION}" + +COPY --from=source /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /out/syncode-control /usr/local/bin/syncode-control + +# Nodes open their session on one port and the forge delivers events on the +# other. They are separate because they are addressed differently: one is +# reached by runners, the other only by the forge. +EXPOSE 8090 8091 + +USER nobody + +ENTRYPOINT ["/usr/local/bin/syncode-control"] + +# The binary defaults to the loopback, which is right when someone runs it on +# their own machine and useless inside a container: the network is the only way +# in. The image therefore listens on every interface, and the container's own +# isolation is what keeps that private. +CMD ["--listen", "0.0.0.0:8090", "--listen-events", "0.0.0.0:8091"] diff --git a/.gitea/workflows/build-image.yml b/.gitea/workflows/build-image.yml --- a/.gitea/workflows/build-image.yml +++ b/.gitea/workflows/build-image.yml @@ -1,224 +1,221 @@ -name: build-image - -on: - push: - branches: [develop, main] - tags: ["v*"] - workflow_dispatch: - -concurrency: - group: control-build-image-${{ github.ref_name }} - cancel-in-progress: ${{ github.ref_type != 'tag' }} - -env: - CONTROL_IMAGE: syncode.sh/syncode/control - -jobs: - resolve: - runs-on: ubuntu-latest - outputs: - operation: ${{ steps.release.outputs.operation }} - product_version: ${{ steps.release.outputs.product_version }} - source: ${{ steps.release.outputs.source }} - tags: ${{ steps.release.outputs.tags }} - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - - id: release - name: Resolve release - run: | - set -euo pipefail - SHA="$(git rev-parse --short HEAD)" - BASE_VERSION="$(tr -d '[:space:]' < VERSION)" - OPERATION=build - NAMES= - case "${GITHUB_REF}" in - refs/tags/*) - VERSION="${BASE_VERSION}" - test "${GITHUB_REF_NAME#v}" = "${BASE_VERSION}" - OPERATION=promote - ;; - refs/heads/develop) - VERSION="${BASE_VERSION}-dev" - NAMES="${SHA} develop ${VERSION}" - ;; - refs/heads/main) - VERSION="${BASE_VERSION}" - NAMES="${SHA} main latest" - ;; - *) - echo "unsupported ref: ${GITHUB_REF}" - exit 1 - ;; - esac - # The digest of the sources the image is built from, so a rebuild of - # the same tree reuses the published image instead of producing a - # second one that differs only by timestamp. - HASH="$( { git ls-files -s -- . \ - ':(exclude).gitea/**' ':(exclude)*.md' ':(exclude)LICENSE' - printf 'version=%s\n' "${VERSION}"; } | git hash-object --stdin | cut -c1-12)" - refs() { - local separator="" - for name in "$@"; do - printf '%s%s:%s' "${separator}" "${CONTROL_IMAGE}" "${name}" - separator=, - done - } - { - echo "operation=${OPERATION}" - echo "product_version=${VERSION}" - echo "source=${CONTROL_IMAGE}:src-${HASH}" - echo "tags=$(refs ${NAMES})" - } >> "${GITHUB_OUTPUT}" - - validate: - needs: resolve - if: ${{ needs.resolve.outputs.operation == 'build' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 - with: - context: . - target: validation - outputs: type=cacheonly - secrets: | - "syncode_git_user=${{ secrets.REGISTRY_USER }}" - "syncode_git_token=${{ secrets.REGISTRY_TOKEN }}" - - published: - needs: resolve - if: ${{ needs.resolve.outputs.operation == 'build' }} - runs-on: ubuntu-latest - outputs: - exists: ${{ steps.check.outputs.exists }} - steps: - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: syncode.sh - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - id: check - name: Look for the image in the registry - env: - SOURCE: ${{ needs.resolve.outputs.source }} - run: | - set -euo pipefail - if docker buildx imagetools inspect "${SOURCE}" >/dev/null 2>&1; then - echo "exists=true" >> "${GITHUB_OUTPUT}" - else - echo "exists=false" >> "${GITHUB_OUTPUT}" - fi - - # Each architecture builds natively on a runner labeled for it, rather than - # one job cross-building both under QEMU — arm64 under emulation was slow - # enough to stall the whole CI queue behind it (meta issue #42). - image-amd64: - needs: [resolve, validate, published] - if: ${{ needs.resolve.outputs.operation == 'build' && needs.published.outputs.exists == 'false' }} - runs-on: syncode-linux-amd64 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: syncode.sh - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 - with: - context: . - platforms: linux/amd64 - provenance: false - build-args: | - SOURCE_DATE_EPOCH=0 - VERSION=${{ needs.resolve.outputs.product_version }} - tags: ${{ needs.resolve.outputs.source }}-amd64 - cache-from: type=registry,ref=syncode.sh/syncode/control:buildcache-amd64 - cache-to: type=registry,ref=syncode.sh/syncode/control:buildcache-amd64,mode=max - outputs: type=registry,rewrite-timestamp=true - secrets: | - "syncode_git_user=${{ secrets.REGISTRY_USER }}" - "syncode_git_token=${{ secrets.REGISTRY_TOKEN }}" - - image-arm64: - needs: [resolve, validate, published] - if: ${{ needs.resolve.outputs.operation == 'build' && needs.published.outputs.exists == 'false' }} - runs-on: syncode-linux-arm64 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: syncode.sh - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 - with: - context: . - platforms: linux/arm64 - provenance: false - build-args: | - SOURCE_DATE_EPOCH=0 - VERSION=${{ needs.resolve.outputs.product_version }} - tags: ${{ needs.resolve.outputs.source }}-arm64 - cache-from: type=registry,ref=syncode.sh/syncode/control:buildcache-arm64 - cache-to: type=registry,ref=syncode.sh/syncode/control:buildcache-arm64,mode=max - outputs: type=registry,rewrite-timestamp=true - secrets: | - "syncode_git_user=${{ secrets.REGISTRY_USER }}" - "syncode_git_token=${{ secrets.REGISTRY_TOKEN }}" - - image: - needs: [resolve, published, image-amd64, image-arm64] - if: ${{ needs.resolve.outputs.operation == 'build' }} - runs-on: ubuntu-latest - steps: - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: syncode.sh - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Combine per-arch images and point the release tags at them - env: - SOURCE: ${{ needs.resolve.outputs.source }} - TAGS: ${{ needs.resolve.outputs.tags }} - EXISTS: ${{ needs.published.outputs.exists }} - run: | - set -euo pipefail - if [ "${EXISTS}" = "false" ]; then - docker buildx imagetools create --tag "${SOURCE}" "${SOURCE}-amd64" "${SOURCE}-arm64" - fi - for REF in ${TAGS//,/ }; do - docker buildx imagetools create --tag "${REF}" "${SOURCE}" - done - - promote: - needs: resolve - if: ${{ needs.resolve.outputs.operation == 'promote' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: syncode.sh - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Promote release manifests - env: - PRODUCT_VERSION: ${{ needs.resolve.outputs.product_version }} - run: | - set -euo pipefail - MAIN_SHA="$(git ls-remote origin refs/heads/main | awk 'NR == 1 {print $1}')" - test -n "${MAIN_SHA}" - test "$(git rev-parse HEAD)" = "${MAIN_SHA}" - docker buildx imagetools create \ - --tag "${CONTROL_IMAGE}:${PRODUCT_VERSION}" \ - "${CONTROL_IMAGE}:main" +name: build-image + +on: + push: + branches: [develop, main] + tags: ["v*"] + workflow_dispatch: + +concurrency: + group: control-build-image-${{ github.ref_name }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +env: + CONTROL_IMAGE: syncode.sh/syncode/control + +jobs: + resolve: + runs-on: ubuntu-latest + outputs: + operation: ${{ steps.release.outputs.operation }} + product_version: ${{ steps.release.outputs.product_version }} + source: ${{ steps.release.outputs.source }} + tags: ${{ steps.release.outputs.tags }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - id: release + name: Resolve release + run: | + set -euo pipefail + SHA="$(git rev-parse --short HEAD)" + BASE_VERSION="$(tr -d '[:space:]' < VERSION)" + OPERATION=build + NAMES= + case "${GITHUB_REF}" in + refs/tags/*) + VERSION="${BASE_VERSION}" + test "${GITHUB_REF_NAME#v}" = "${BASE_VERSION}" + OPERATION=promote + ;; + refs/heads/develop) + VERSION="${BASE_VERSION}-dev" + NAMES="${SHA} develop ${VERSION}" + ;; + refs/heads/main) + VERSION="${BASE_VERSION}" + NAMES="${SHA} main latest" + ;; + *) + echo "unsupported ref: ${GITHUB_REF}" + exit 1 + ;; + esac + # The digest of the sources the image is built from, so a rebuild of + # the same tree reuses the published image instead of producing a + # second one that differs only by timestamp. + HASH="$( { git ls-files -s -- . \ + ':(exclude).gitea/**' ':(exclude)*.md' ':(exclude)LICENSE' + printf 'version=%s\n' "${VERSION}"; } | git hash-object --stdin | cut -c1-12)" + refs() { + local separator="" + for name in "$@"; do + printf '%s%s:%s' "${separator}" "${CONTROL_IMAGE}" "${name}" + separator=, + done + } + { + echo "operation=${OPERATION}" + echo "product_version=${VERSION}" + echo "source=${CONTROL_IMAGE}:src-${HASH}" + echo "tags=$(refs ${NAMES})" + } >> "${GITHUB_OUTPUT}" + + validate: + needs: resolve + if: ${{ needs.resolve.outputs.operation == 'build' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + with: + context: . + target: validation + outputs: type=cacheonly + secrets: | + "syncode_git_config=${{ secrets.SYNCODE_GIT_CONFIG }}" + + published: + needs: resolve + if: ${{ needs.resolve.outputs.operation == 'build' }} + runs-on: ubuntu-latest + outputs: + exists: ${{ steps.check.outputs.exists }} + steps: + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: syncode.sh + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - id: check + name: Look for the image in the registry + env: + SOURCE: ${{ needs.resolve.outputs.source }} + run: | + set -euo pipefail + if docker buildx imagetools inspect "${SOURCE}" >/dev/null 2>&1; then + echo "exists=true" >> "${GITHUB_OUTPUT}" + else + echo "exists=false" >> "${GITHUB_OUTPUT}" + fi + + # Each architecture builds natively on a runner labeled for it, rather than + # one job cross-building both under QEMU — arm64 under emulation was slow + # enough to stall the whole CI queue behind it (meta issue #42). + image-amd64: + needs: [resolve, validate, published] + if: ${{ needs.resolve.outputs.operation == 'build' && needs.published.outputs.exists == 'false' }} + runs-on: syncode-linux-amd64 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: syncode.sh + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + with: + context: . + platforms: linux/amd64 + provenance: false + build-args: | + SOURCE_DATE_EPOCH=0 + VERSION=${{ needs.resolve.outputs.product_version }} + tags: ${{ needs.resolve.outputs.source }}-amd64 + cache-from: type=registry,ref=syncode.sh/syncode/control:buildcache-amd64 + cache-to: type=registry,ref=syncode.sh/syncode/control:buildcache-amd64,mode=max + outputs: type=registry,rewrite-timestamp=true + secrets: | + "syncode_git_config=${{ secrets.SYNCODE_GIT_CONFIG }}" + + image-arm64: + needs: [resolve, validate, published] + if: ${{ needs.resolve.outputs.operation == 'build' && needs.published.outputs.exists == 'false' }} + runs-on: syncode-linux-arm64 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: syncode.sh + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + with: + context: . + platforms: linux/arm64 + provenance: false + build-args: | + SOURCE_DATE_EPOCH=0 + VERSION=${{ needs.resolve.outputs.product_version }} + tags: ${{ needs.resolve.outputs.source }}-arm64 + cache-from: type=registry,ref=syncode.sh/syncode/control:buildcache-arm64 + cache-to: type=registry,ref=syncode.sh/syncode/control:buildcache-arm64,mode=max + outputs: type=registry,rewrite-timestamp=true + secrets: | + "syncode_git_config=${{ secrets.SYNCODE_GIT_CONFIG }}" + + image: + needs: [resolve, published, image-amd64, image-arm64] + if: ${{ needs.resolve.outputs.operation == 'build' }} + runs-on: ubuntu-latest + steps: + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: syncode.sh + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Combine per-arch images and point the release tags at them + env: + SOURCE: ${{ needs.resolve.outputs.source }} + TAGS: ${{ needs.resolve.outputs.tags }} + EXISTS: ${{ needs.published.outputs.exists }} + run: | + set -euo pipefail + if [ "${EXISTS}" = "false" ]; then + docker buildx imagetools create --tag "${SOURCE}" "${SOURCE}-amd64" "${SOURCE}-arm64" + fi + for REF in ${TAGS//,/ }; do + docker buildx imagetools create --tag "${REF}" "${SOURCE}" + done + + promote: + needs: resolve + if: ${{ needs.resolve.outputs.operation == 'promote' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: syncode.sh + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Promote release manifests + env: + PRODUCT_VERSION: ${{ needs.resolve.outputs.product_version }} + run: | + set -euo pipefail + MAIN_SHA="$(git ls-remote origin refs/heads/main | awk 'NR == 1 {print $1}')" + test -n "${MAIN_SHA}" + test "$(git rev-parse HEAD)" = "${MAIN_SHA}" + docker buildx imagetools create \ + --tag "${CONTROL_IMAGE}:${PRODUCT_VERSION}" \ + "${CONTROL_IMAGE}:main"