From 08a8d79265e3ec3eb07640a2ea2c62b146420ccc Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 03:30:49 -0700 Subject: [PATCH 1/6] feat(ingress): record source metadata in the image Add OCI labels, manifest annotations and /etc/dstack-ingress/build-info pointing back to the source repository, commit, version and pinned base image, so a published digest can be traced to the code that built it. All values derive from the git checkout (commit, the new VERSION file, the Dockerfile FROM line), so rebuilding a clean checkout of the same commit still yields the same digest. The build script warns and marks the revision -dirty when the tree has uncommitted or untracked changes; CI passes --require-clean and checks that the release tag matches VERSION. The entrypoint prints the build info as its first log line. Also add a .dockerignore so untracked local artifacts (oci.tar, __pycache__, .pytest_cache) neither inflate the build context nor leak into the image. --- .github/workflows/dstack-ingress-release.yml | 33 ++++- custom-domain/dstack-ingress/.dockerignore | 11 ++ custom-domain/dstack-ingress/.gitignore | 1 + custom-domain/dstack-ingress/Dockerfile | 5 +- custom-domain/dstack-ingress/README.md | 24 +++- custom-domain/dstack-ingress/VERSION | 1 + custom-domain/dstack-ingress/build-image.sh | 136 +++++++++++++++--- .../dstack-ingress/scripts/entrypoint.sh | 8 ++ 8 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 custom-domain/dstack-ingress/.dockerignore create mode 100644 custom-domain/dstack-ingress/VERSION diff --git a/.github/workflows/dstack-ingress-release.yml b/.github/workflows/dstack-ingress-release.yml index d4a20256..7d143fab 100644 --- a/.github/workflows/dstack-ingress-release.yml +++ b/.github/workflows/dstack-ingress-release.yml @@ -28,6 +28,13 @@ jobs: echo "Unable to parse version from ref: ${GITHUB_REF}" >&2 exit 1 fi + # The image records its version from the committed VERSION file so a + # plain checkout reproduces the digest; the tag must agree with it. + FILE_VERSION=$(tr -d '[:space:]' < custom-domain/dstack-ingress/VERSION) + if [ "${VERSION}" != "${FILE_VERSION}" ]; then + echo "Tag version ${VERSION} does not match custom-domain/dstack-ingress/VERSION (${FILE_VERSION})" >&2 + exit 1 + fi echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "IMAGE_REFERENCE=${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${VERSION}" >> "$GITHUB_ENV" echo "Parsed version: ${VERSION}" @@ -49,7 +56,7 @@ jobs: env: IMAGE_REFERENCE: ${{ env.IMAGE_REFERENCE }} run: | - ./build-image.sh --push "${IMAGE_REFERENCE}" + ./build-image.sh --require-clean --push "${IMAGE_REFERENCE}" - name: Capture image digest id: capture-digest @@ -79,6 +86,7 @@ jobs: echo "" echo "- Tag: \`${IMAGE_REFERENCE}\`" echo "- Digest: \`${IMAGE_DIGEST}\`" + echo "- Source: \`${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}/custom-domain/dstack-ingress\`" echo "- Sigstore: https://search.sigstore.dev/?hash=${IMAGE_DIGEST}" } >> "$GITHUB_STEP_SUMMARY" @@ -90,4 +98,25 @@ jobs: | Image | Digest | Verification | |---|---|---| - | ${{ env.IMAGE_REFERENCE }} | ${{ steps.capture-digest.outputs.digest }} | [Verify on Sigstore](https://search.sigstore.dev/?hash=${{ steps.capture-digest.outputs.digest }}) | \ No newline at end of file + | ${{ env.IMAGE_REFERENCE }} | ${{ steps.capture-digest.outputs.digest }} | [Verify on Sigstore](https://search.sigstore.dev/?hash=${{ steps.capture-digest.outputs.digest }}) | + + ## Source + + Built from [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}/custom-domain/dstack-ingress). The image records its source repository, commit and version as OCI labels and manifest annotations: + + ```bash + skopeo inspect docker://${{ env.IMAGE_REFERENCE }} | jq .Labels + skopeo inspect --raw docker://${{ env.IMAGE_REFERENCE }} | jq .annotations + ``` + + ## Reproducible Build + + ```bash + git clone ${{ github.server_url }}/${{ github.repository }}.git + cd dstack-examples/custom-domain/dstack-ingress + git checkout ${{ github.sha }} + ./build-image.sh + skopeo inspect oci-archive:./oci.tar | jq -r '.Digest' + ``` + + Expected digest: `${{ steps.capture-digest.outputs.digest }}` \ No newline at end of file diff --git a/custom-domain/dstack-ingress/.dockerignore b/custom-domain/dstack-ingress/.dockerignore new file mode 100644 index 00000000..05d8282d --- /dev/null +++ b/custom-domain/dstack-ingress/.dockerignore @@ -0,0 +1,11 @@ +# Keep the build context identical to the committed tree, so untracked local +# artifacts can neither leak into the image nor change its digest. +.git +.gitignore +.dockerignore +.claude/ +CLAUDE.md +test/ +oci.tar +.pytest_cache/ +**/__pycache__/ diff --git a/custom-domain/dstack-ingress/.gitignore b/custom-domain/dstack-ingress/.gitignore index 08665848..48da6584 100644 --- a/custom-domain/dstack-ingress/.gitignore +++ b/custom-domain/dstack-ingress/.gitignore @@ -3,3 +3,4 @@ /test/ __pycache__ /oci.tar +/.BUILD_INFO diff --git a/custom-domain/dstack-ingress/Dockerfile b/custom-domain/dstack-ingress/Dockerfile index c3165e2a..a28c1016 100644 --- a/custom-domain/dstack-ingress/Dockerfile +++ b/custom-domain/dstack-ingress/Dockerfile @@ -97,7 +97,10 @@ RUN --mount=type=bind,source=scripts,target=/tmp/scripts,ro \ ENV PATH="/scripts:$PATH" ENV PYTHONPATH="/scripts" ENV PYTHONUNBUFFERED=1 -COPY --chmod=666 .GIT_REV /etc/ +# Source metadata generated by build-image.sh (same key=value set as the OCI +# labels and manifest annotations), so a running container can identify its +# own source revision. Printed by the entrypoint at startup. +COPY --chmod=644 .BUILD_INFO /etc/dstack-ingress/build-info ENTRYPOINT ["/scripts/entrypoint.sh"] CMD ["haproxy", "-W", "-f", "/etc/haproxy/haproxy.cfg"] diff --git a/custom-domain/dstack-ingress/README.md b/custom-domain/dstack-ingress/README.md index e80ce9f7..a08c980c 100644 --- a/custom-domain/dstack-ingress/README.md +++ b/custom-domain/dstack-ingress/README.md @@ -299,7 +299,29 @@ To disable the built-in evidence endpoint and serve evidence files only through ./build-image.sh --push yourusername/dstack-ingress:tag ``` -The build script ensures reproducibility via pinned packages, deterministic timestamps, and specific buildkit version. +The build script ensures reproducibility via pinned packages, deterministic timestamps, and specific buildkit version. Building the same commit from a clean checkout produces the same image digest; CI runs the same script with `--require-clean`. + +### Image metadata + +Every image records where it came from, using the standard [OCI image annotation keys](https://github.com/opencontainers/image-spec/blob/main/annotations.md). The values are derived from the git checkout only (commit, the `VERSION` file, the Dockerfile base image), so they do not disturb reproducibility. The same key/value set is written to three places: + +| Location | How to read it | +|---|---| +| Image config labels | `skopeo inspect docker://dstacktee/dstack-ingress: \| jq .Labels` or `docker inspect --format '{{json .Config.Labels}}' ` | +| Image manifest annotations | `skopeo inspect --raw docker://dstacktee/dstack-ingress: \| jq .annotations` | +| `/etc/dstack-ingress/build-info` inside the image | `docker run --rm --entrypoint cat /etc/dstack-ingress/build-info`; also printed as the first line of the container log | + +| Key | Value | +|---|---| +| `org.opencontainers.image.source` | Repository URL (`SOURCE_URL` env when building from a fork) | +| `org.opencontainers.image.revision` | Git commit; suffixed with `-dirty` when built from an unclean tree | +| `org.opencontainers.image.version` | Contents of `VERSION`; the release tag `dstack-ingress-v` must match | +| `org.opencontainers.image.url` / `.documentation` | This directory / README at that exact commit | +| `org.opencontainers.image.base.name` / `.base.digest` | The pinned haproxy base image | + +To reproduce a published image, check out the commit from its `revision` label and run `./build-image.sh`; the digest printed at the end must match the registry. Releases are additionally signed with SLSA provenance, verifiable with `gh attestation verify oci://docker.io/dstacktee/dstack-ingress: --owner Dstack-TEE`. + +Bumping the version is a source change: update `VERSION`, commit, then tag `dstack-ingress-v`. ## License diff --git a/custom-domain/dstack-ingress/VERSION b/custom-domain/dstack-ingress/VERSION new file mode 100644 index 00000000..95e3ba81 --- /dev/null +++ b/custom-domain/dstack-ingress/VERSION @@ -0,0 +1 @@ +2.5 diff --git a/custom-domain/dstack-ingress/build-image.sh b/custom-domain/dstack-ingress/build-image.sh index 7ccad4d3..24d31756 100755 --- a/custom-domain/dstack-ingress/build-image.sh +++ b/custom-domain/dstack-ingress/build-image.sh @@ -1,23 +1,52 @@ #!/bin/bash +# +# Reproducible image build. The same script runs in CI and on a developer +# machine, so everything that ends up in the image -- including the OCI +# labels/annotations that point back to the source -- is derived from the git +# checkout only. Nothing here may depend on wall-clock time, the CI run, or the +# build host, or the digest stops being reproducible. + +set -euo pipefail + +usage() { + echo "Usage: $0 [--push [:]] [--require-clean]" + echo "" + echo " --push Push the built image to the given registry reference." + echo " --require-clean Fail instead of warn when the working tree has" + echo " uncommitted or untracked changes (used by CI)." + echo "" + echo "Environment:" + echo " SOURCE_URL Repository URL recorded in the image metadata." + echo " Defaults to the canonical upstream repository; set it" + echo " when building from a fork." +} -# Parse command line arguments PUSH=false REPO="" +REQUIRE_CLEAN=false while [[ $# -gt 0 ]]; do case $1 in --push) PUSH=true - REPO="$2" + REPO="${2:-}" if [ -z "$REPO" ]; then - echo "Error: --push requires a repository argument" - echo "Usage: $0 [--push [:]]" + echo "Error: --push requires a repository argument" >&2 + usage >&2 exit 1 fi shift 2 ;; + --require-clean) + REQUIRE_CLEAN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; *) - echo "Usage: $0 [--push [:]]" + usage >&2 exit 1 ;; esac @@ -35,23 +64,97 @@ for required in docker skopeo jq git; do require_command "$required" done +cd "$(dirname "$0")" + +# --------------------------------------------------------------------------- +# Source metadata. Every value below is a function of the checked-out commit +# (plus SOURCE_URL for forks), so a rebuild of the same commit yields the same +# labels and therefore the same digest. +# --------------------------------------------------------------------------- +SOURCE_URL="${SOURCE_URL:-https://github.com/Dstack-TEE/dstack-examples}" +SOURCE_URL="${SOURCE_URL%/}" +SUBDIR="$(git rev-parse --show-prefix)" +SUBDIR="${SUBDIR%/}" +GIT_REV="$(git rev-parse HEAD)" +VERSION="$(tr -d '[:space:]' < VERSION)" +if [ -z "$VERSION" ]; then + echo "Error: VERSION file is empty" >&2 + exit 1 +fi + +# Untracked files count as dirty: scripts/ is copied wholesale into the image. +DIRTY="$(git status --porcelain --untracked-files=all -- .)" +if [ -n "$DIRTY" ]; then + if [ "$REQUIRE_CLEAN" = true ]; then + echo "Error: working tree is not clean; refusing to build a release image:" >&2 + echo "$DIRTY" >&2 + exit 1 + fi + echo "Warning: working tree is not clean; the image will be marked dirty and" >&2 + echo " its digest will not match a build of commit ${GIT_REV}." >&2 + GIT_REV="${GIT_REV}-dirty" +fi + +# Base image, kept in sync with the Dockerfile FROM line. +BASE_REF="$(sed -n 's/^FROM[[:space:]][[:space:]]*\([^[:space:]][^[:space:]]*\).*/\1/p' Dockerfile | head -n1)" +BASE_NAME="${BASE_REF%%@*}" +BASE_DIGEST="${BASE_REF#*@}" +case "$BASE_NAME" in + */*) ;; + *) BASE_NAME="docker.io/library/${BASE_NAME}" ;; +esac +if [ "$BASE_DIGEST" = "$BASE_REF" ]; then + echo "Error: Dockerfile FROM must pin the base image by digest" >&2 + exit 1 +fi + +# OCI standard keys: https://github.com/opencontainers/image-spec/blob/main/annotations.md +METADATA=( + "org.opencontainers.image.title=dstack-ingress" + "org.opencontainers.image.description=TLS ingress for dstack TEE applications with ACME certificates and attestation evidence" + "org.opencontainers.image.source=${SOURCE_URL}" + "org.opencontainers.image.revision=${GIT_REV}" + "org.opencontainers.image.version=${VERSION}" + "org.opencontainers.image.url=${SOURCE_URL}/tree/${GIT_REV%-dirty}/${SUBDIR}" + "org.opencontainers.image.documentation=${SOURCE_URL}/blob/${GIT_REV%-dirty}/${SUBDIR}/README.md" + "org.opencontainers.image.licenses=MIT" + "org.opencontainers.image.base.name=${BASE_NAME}" + "org.opencontainers.image.base.digest=${BASE_DIGEST}" +) + +# The same key=value set goes to three places: image config labels (docker +# inspect, skopeo inspect), image manifest annotations (visible in the registry +# without fetching the config) and a file inside the image (readable from the +# running container, printed by the entrypoint). +METADATA_ARGS=() +for kv in "${METADATA[@]}"; do + METADATA_ARGS+=(--label "$kv" --annotation "manifest:$kv") +done + +BUILD_INFO=.BUILD_INFO +cleanup() { + rm -f "$BUILD_INFO" + docker rmi "$TEMP_TAG" >/dev/null 2>&1 || true +} +TEMP_TAG="dstack-ingress-temp:$(date +%s)" +trap cleanup EXIT + +printf '%s\n' "${METADATA[@]}" > "$BUILD_INFO" + +echo "Image metadata:" +sed 's/^/ /' "$BUILD_INFO" +echo "" + # Check if buildkit_20 already exists before creating it if ! docker buildx inspect buildkit_20 &>/dev/null; then docker buildx create --use --driver-opt image=moby/buildkit:v0.20.2 --name buildkit_20 fi touch pinned-packages.txt -git rev-parse HEAD > .GIT_REV -TEMP_TAG="dstack-ingress-temp:$(date +%s)" docker buildx build --builder buildkit_20 --no-cache --build-arg SOURCE_DATE_EPOCH="0" \ + "${METADATA_ARGS[@]}" \ --output type=oci,dest=./oci.tar,rewrite-timestamp=true \ --output type=docker,name="$TEMP_TAG" . -if [ "$?" -ne 0 ]; then - echo "Build failed" - rm .GIT_REV - exit 1 -fi - echo "Build completed, manifest digest:" echo "" skopeo inspect oci-archive:./oci.tar | jq .Digest @@ -71,7 +174,7 @@ else echo " skopeo copy --insecure-policy oci-archive:./oci.tar docker://[:]" echo "" echo " Pushing image to dstacktee org:" - echo " skopeo copy --insecure-policy oci-archive:./oci.tar docker://dstacktee/dstack-ingress:$(date +%Y%m%d) --authfile ~/.docker/config.json" + echo " skopeo copy --insecure-policy oci-archive:./oci.tar docker://dstacktee/dstack-ingress:${VERSION} --authfile ~/.docker/config.json" fi echo "" @@ -80,8 +183,3 @@ echo "Extracting package information from built image: $TEMP_TAG" docker run --rm --entrypoint bash "$TEMP_TAG" -c "dpkg -l | grep '^ii' | awk '{print \$2\"=\"\$3}' | sort" > pinned-packages.txt echo "Package information extracted to pinned-packages.txt ($(wc -l < pinned-packages.txt) packages)" - -# Clean up the temporary image from Docker daemon -docker rmi "$TEMP_TAG" 2>/dev/null || true - -rm .GIT_REV diff --git a/custom-domain/dstack-ingress/scripts/entrypoint.sh b/custom-domain/dstack-ingress/scripts/entrypoint.sh index 1f564e75..64e0edf0 100644 --- a/custom-domain/dstack-ingress/scripts/entrypoint.sh +++ b/custom-domain/dstack-ingress/scripts/entrypoint.sh @@ -9,6 +9,14 @@ set -e +# Identify the build first, so the line is there even if validation below +# fails. The same values are in the image labels and manifest annotations. +if [ -r /etc/dstack-ingress/build-info ]; then + echo "dstack-ingress $(sed -n 's/^org.opencontainers.image.version=//p' /etc/dstack-ingress/build-info)" \ + "revision $(sed -n 's/^org.opencontainers.image.revision=//p' /etc/dstack-ingress/build-info)" \ + "source $(sed -n 's/^org.opencontainers.image.source=//p' /etc/dstack-ingress/build-info)" +fi + source /scripts/functions.sh PORT=${PORT:-443} From d8cfe8a8c2bb5171d68916a4c98e5346c5253c92 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 18:50:04 +0800 Subject: [PATCH 2/6] Fix comment formatting in build-image.sh Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- custom-domain/dstack-ingress/build-image.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom-domain/dstack-ingress/build-image.sh b/custom-domain/dstack-ingress/build-image.sh index 24d31756..00f3a6e2 100755 --- a/custom-domain/dstack-ingress/build-image.sh +++ b/custom-domain/dstack-ingress/build-image.sh @@ -3,8 +3,8 @@ # Reproducible image build. The same script runs in CI and on a developer # machine, so everything that ends up in the image -- including the OCI # labels/annotations that point back to the source -- is derived from the git -# checkout only. Nothing here may depend on wall-clock time, the CI run, or the -# build host, or the digest stops being reproducible. +# checkout only. Nothing that ends up in the image may depend on wall-clock +# time, the CI run, or the build host, or the digest stops being reproducible. set -euo pipefail From 7f6537644ac1eb21765a25104e18b12462b0deff Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 18:50:14 +0800 Subject: [PATCH 3/6] Update comments in .dockerignore for clarity Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- custom-domain/dstack-ingress/.dockerignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom-domain/dstack-ingress/.dockerignore b/custom-domain/dstack-ingress/.dockerignore index 05d8282d..03ba6b70 100644 --- a/custom-domain/dstack-ingress/.dockerignore +++ b/custom-domain/dstack-ingress/.dockerignore @@ -1,5 +1,5 @@ -# Keep the build context identical to the committed tree, so untracked local -# artifacts can neither leak into the image nor change its digest. +# Keep the build context independent of local artifacts, so untracked local +# files can neither leak into the image nor change its digest. .git .gitignore .dockerignore From d2bf427fbb94ff8440f65a92aa9833c252039920 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 04:00:57 -0700 Subject: [PATCH 4/6] ci(ingress): require a matching release tag The version gate parsed the version out of GITHUB_REF unconditionally, so a workflow_dispatch run took the literal string 'refs/heads/main' as the version and only failed later at the registry push. Derive the version from the committed VERSION file, which is what the image records, and require the ref to be a dstack-ingress-v* tag that matches it. --- .github/workflows/dstack-ingress-release.yml | 33 +++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dstack-ingress-release.yml b/.github/workflows/dstack-ingress-release.yml index 7d143fab..7762c8da 100644 --- a/.github/workflows/dstack-ingress-release.yml +++ b/.github/workflows/dstack-ingress-release.yml @@ -21,20 +21,31 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Parse version from tag + - name: Parse and check version run: | - VERSION=${GITHUB_REF#refs/tags/dstack-ingress-v} + # The image records its version from the committed VERSION file, so + # that a plain checkout reproduces the digest. The release tag only + # selects which commit to build and must agree with that file. + VERSION=$(tr -d '[:space:]' < custom-domain/dstack-ingress/VERSION) if [ -z "${VERSION}" ]; then - echo "Unable to parse version from ref: ${GITHUB_REF}" >&2 - exit 1 - fi - # The image records its version from the committed VERSION file so a - # plain checkout reproduces the digest; the tag must agree with it. - FILE_VERSION=$(tr -d '[:space:]' < custom-domain/dstack-ingress/VERSION) - if [ "${VERSION}" != "${FILE_VERSION}" ]; then - echo "Tag version ${VERSION} does not match custom-domain/dstack-ingress/VERSION (${FILE_VERSION})" >&2 + echo "custom-domain/dstack-ingress/VERSION is empty" >&2 exit 1 fi + case "${GITHUB_REF}" in + refs/tags/dstack-ingress-v*) + TAG_VERSION=${GITHUB_REF#refs/tags/dstack-ingress-v} + if [ "${TAG_VERSION}" != "${VERSION}" ]; then + echo "Tag dstack-ingress-v${TAG_VERSION} does not match custom-domain/dstack-ingress/VERSION (${VERSION})." >&2 + echo "Update VERSION and re-tag, so the image version matches the release." >&2 + exit 1 + fi + ;; + *) + echo "This workflow builds a release and must run on a dstack-ingress-v* tag." >&2 + echo "Got ref: ${GITHUB_REF}. Re-run it selecting the release tag." >&2 + exit 1 + ;; + esac echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "IMAGE_REFERENCE=${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${VERSION}" >> "$GITHUB_ENV" echo "Parsed version: ${VERSION}" @@ -119,4 +130,4 @@ jobs: skopeo inspect oci-archive:./oci.tar | jq -r '.Digest' ``` - Expected digest: `${{ steps.capture-digest.outputs.digest }}` \ No newline at end of file + Expected digest: `${{ steps.capture-digest.outputs.digest }}` From a66b950375e550b1095249947bcc40f973a4f7db Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 04:34:28 -0700 Subject: [PATCH 5/6] docs(ingress): state the build host requirement for reproduction The reproduce instructions dropped the prerequisite line the v2.4 notes carried. The pinned toolchain only reproduces the published digest on a native linux/amd64 host, so someone following the steps on arm64 would get a mismatch with nothing explaining why. --- .github/workflows/dstack-ingress-release.yml | 2 ++ custom-domain/dstack-ingress/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dstack-ingress-release.yml b/.github/workflows/dstack-ingress-release.yml index 7762c8da..b56d6fbb 100644 --- a/.github/workflows/dstack-ingress-release.yml +++ b/.github/workflows/dstack-ingress-release.yml @@ -122,6 +122,8 @@ jobs: ## Reproducible Build + Build on a native Linux amd64 host with Docker Buildx, Skopeo, jq and Git installed: + ```bash git clone ${{ github.server_url }}/${{ github.repository }}.git cd dstack-examples/custom-domain/dstack-ingress diff --git a/custom-domain/dstack-ingress/README.md b/custom-domain/dstack-ingress/README.md index a08c980c..96fdf69a 100644 --- a/custom-domain/dstack-ingress/README.md +++ b/custom-domain/dstack-ingress/README.md @@ -319,7 +319,7 @@ Every image records where it came from, using the standard [OCI image annotation | `org.opencontainers.image.url` / `.documentation` | This directory / README at that exact commit | | `org.opencontainers.image.base.name` / `.base.digest` | The pinned haproxy base image | -To reproduce a published image, check out the commit from its `revision` label and run `./build-image.sh`; the digest printed at the end must match the registry. Releases are additionally signed with SLSA provenance, verifiable with `gh attestation verify oci://docker.io/dstacktee/dstack-ingress: --owner Dstack-TEE`. +To reproduce a published image, check out the commit from its `revision` label and run `./build-image.sh` on a native Linux amd64 host with Docker Buildx, Skopeo, jq and Git installed; the digest printed at the end must match the registry. Releases are additionally signed with SLSA provenance, verifiable with `gh attestation verify oci://docker.io/dstacktee/dstack-ingress: --owner Dstack-TEE`. Bumping the version is a source change: update `VERSION`, commit, then tag `dstack-ingress-v`. From d1cc23ab612f62194286785181f84f81d0ed1797 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 5 Sep 2026 05:25:09 -0700 Subject: [PATCH 6/6] fix(ingress): set VERSION to the next unreleased version dstack-ingress-v2.5 was tagged and published on 2026-09-03, so 2.5 is taken: docker.io/dstacktee/dstack-ingress:2.5 already exists at sha256:97285855. Releasing 2.5 again would either fail on the existing tag or overwrite a published digest that users may have pinned. --- custom-domain/dstack-ingress/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-domain/dstack-ingress/VERSION b/custom-domain/dstack-ingress/VERSION index 95e3ba81..5154b3f6 100644 --- a/custom-domain/dstack-ingress/VERSION +++ b/custom-domain/dstack-ingress/VERSION @@ -1 +1 @@ -2.5 +2.6