Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af110766d | ||
|
|
8363be933d | ||
|
|
128ca9d8b3 |
No files matched your search
@@ -13,8 +13,16 @@ name: Sync upstream and build images
|
||||
# 4. Publish one Gitea release per built version on the mirrored upstream
|
||||
# tag, titled like upstream, carrying upstream's own notes plus a short
|
||||
# image footer. Updated in place when the image is rebuilt.
|
||||
# 5. A version that fails to build or push fails the run: a missed upstream
|
||||
# release must be visible, not a warning in a green run.
|
||||
# 5. A version that fails to build or push fails the run, but only when the
|
||||
# registry has never had it: a missed upstream release must be visible,
|
||||
# not a warning in a green run. A *refresh* rebuild — the registry
|
||||
# already has an image for that version and is only being moved onto a
|
||||
# newer pnpm/base/recipe — is best-effort: it warns, leaves the existing
|
||||
# image alone, and cannot turn the run red. Old versions cannot always
|
||||
# be rebuilt any more, because their floating dependency ranges now
|
||||
# resolve to packages that have moved past them, and a doomed 0.1.x
|
||||
# refresh must not hide the 0.2.x release that did ship. Every failure
|
||||
# stays listed in the run either way.
|
||||
#
|
||||
# Images are pushed with regctl, in 50 MB chunks. The registry sits behind
|
||||
# Cloudflare, which rejects any request body over 100 MB; `docker push` sends a
|
||||
@@ -143,23 +151,35 @@ jobs:
|
||||
done < <(sort -V /tmp/upstream-versions)
|
||||
echo "candidates:$CANDIDATES"
|
||||
|
||||
BUILDS=""
|
||||
# Two kinds of build, and they do not matter equally. NEW is a
|
||||
# version the registry has never had — an upstream release that has
|
||||
# not shipped here yet, which is the whole point of this job.
|
||||
# REFRESH is an already-published version being moved onto newer
|
||||
# toolchain. Plan NEW first, so a new release lands in the first
|
||||
# minutes of the run instead of queueing behind refreshes that may
|
||||
# all be unbuildable.
|
||||
NEW=""
|
||||
REFRESH=""
|
||||
for V in $CANDIDATES; do
|
||||
if ! tag_exists "$V"; then
|
||||
echo "$V: missing -> build"; BUILDS="$BUILDS $V"
|
||||
echo "$V: missing -> build"; NEW="$NEW $V"
|
||||
elif forced "$V"; then
|
||||
echo "$V: rebuild requested"; BUILDS="$BUILDS $V"
|
||||
echo "$V: rebuild requested"; REFRESH="$REFRESH $V"
|
||||
else
|
||||
HAVE=$(tag_toolchain "$V")
|
||||
if [ "$HAVE" != "$WANT" ]; then echo "$V: stale ($HAVE) -> rebuild"; BUILDS="$BUILDS $V"; fi
|
||||
if [ "$HAVE" != "$WANT" ]; then echo "$V: stale ($HAVE) -> rebuild"; REFRESH="$REFRESH $V"; fi
|
||||
fi
|
||||
done
|
||||
BUILDS="$NEW $REFRESH"
|
||||
# Newest release candidate = what :latest points at.
|
||||
CURRENT=$(printf '%s\n' $CANDIDATES | grep -v alpha | sort -V | tail -1)
|
||||
echo "new (must ship):$NEW"
|
||||
echo "refresh (best-effort):$REFRESH"
|
||||
echo "planned builds:$BUILDS"
|
||||
echo "current (:latest): $CURRENT"
|
||||
{
|
||||
echo "builds=${BUILDS}"
|
||||
echo "new=${NEW}"
|
||||
echo "current=${CURRENT}"
|
||||
echo "pnpm=${PNPM_LATEST}"
|
||||
echo "base_digest=${BASE_DIGEST}"
|
||||
@@ -174,7 +194,6 @@ jobs:
|
||||
PNPM_VERSION: ${{ steps.plan.outputs.pnpm }}
|
||||
BASE_DIGEST: ${{ steps.plan.outputs.base_digest }}
|
||||
RECIPE_HASH: ${{ steps.plan.outputs.recipe_hash }}
|
||||
CURRENT: ${{ steps.plan.outputs.current }}
|
||||
CI_RUN: ${{ github.run_number }}
|
||||
run: |
|
||||
set -u
|
||||
@@ -262,6 +281,7 @@ jobs:
|
||||
)
|
||||
|
||||
FAILED=""
|
||||
MISSING=""
|
||||
for V in ${{ steps.plan.outputs.builds }}; do
|
||||
if build_one "$V"; then
|
||||
echo "::notice::built ${IMAGE}:${V}"
|
||||
@@ -269,17 +289,24 @@ jobs:
|
||||
docker rm -f "$SMOKE" 2>/dev/null || true; docker volume rm -f "$SMOKE" 2>/dev/null || true
|
||||
echo "::warning::${V} did not build or push; any existing image for it is left untouched"
|
||||
FAILED="$FAILED $V"
|
||||
# Missing from the registry AND failed to build = an upstream
|
||||
# release we failed to ship. Record it for the final gate.
|
||||
case " ${{ steps.plan.outputs.new }} " in *" $V "*) MISSING="$MISSING $V";; esac
|
||||
fi
|
||||
done
|
||||
echo "failed=${FAILED}" >> "$GITHUB_OUTPUT"
|
||||
if [ -n "$FAILED" ]; then
|
||||
echo "failed versions:$FAILED"
|
||||
case " $FAILED " in *" $CURRENT "*) echo "::error::the current version (${CURRENT}) failed to build"; exit 1;; esac
|
||||
fi
|
||||
echo "missing=${MISSING}" >> "$GITHUB_OUTPUT"
|
||||
# Deliberately no exit here. Steps 5-7 still have to run so the
|
||||
# releases for every version that did ship are in place even when
|
||||
# this run ends red; the single red/green decision belongs to the
|
||||
# last step. Aborting here is what used to leave a new release
|
||||
# unpublished.
|
||||
[ -z "$FAILED" ] || echo "failed versions:$FAILED"
|
||||
|
||||
# `:latest` = the newest release-candidate version. A manifest copy
|
||||
# through the registry API: identical digest, nothing rebuilt.
|
||||
- name: Point :latest at the current version
|
||||
id: latest
|
||||
env:
|
||||
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
@@ -290,7 +317,9 @@ jobs:
|
||||
ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json'
|
||||
digest_of() { curl -sI -u "${OWNER}:${TOKEN}" -H "Accept: $ACCEPT" "https://${REGISTRY}/v2/${IMAGE}/manifests/$1" | grep -i '^docker-content-digest' | awk '{print $2}' | tr -d '\r'; }
|
||||
WANT=$(digest_of "$CURRENT")
|
||||
[ -n "$WANT" ] || { echo "::error::${CURRENT} has no image in the registry"; exit 1; }
|
||||
# Do not exit non-zero here either: that would skip both the prune
|
||||
# and the release publish. Record the miss, let the gate decide.
|
||||
[ -n "$WANT" ] || { echo "::error::${CURRENT} has no image in the registry, so :latest cannot be moved"; echo "latest_failed=1" >> "$GITHUB_OUTPUT"; exit 0; }
|
||||
HAVE=$(digest_of latest || true)
|
||||
echo "latest -> ${CURRENT} (${WANT}); currently ${HAVE:-unset}"
|
||||
[ "$WANT" != "$HAVE" ] || { echo "latest already current"; exit 0; }
|
||||
@@ -349,9 +378,14 @@ jobs:
|
||||
"$BASE_IMAGE" node - < .gitea/scripts/publish-releases.js
|
||||
|
||||
# Last, so :latest and the releases for everything that did ship are
|
||||
# already in place when the run turns red.
|
||||
- name: Fail the run if a version did not ship
|
||||
if: steps.build.outputs.failed != ''
|
||||
# already in place before the run turns red — and the only thing that
|
||||
# turns it red is a version this registry has never published. Refreshes
|
||||
# of versions we already ship stay warnings.
|
||||
- name: Fail the run if a new version did not ship
|
||||
if: steps.build.outputs.missing != '' || steps.latest.outputs.latest_failed != ''
|
||||
run: |
|
||||
echo "::error::these versions did not build or push:${{ steps.build.outputs.failed }}"
|
||||
[ -z "${{ steps.build.outputs.missing }}" ] || \
|
||||
echo "::error::never-published versions did not build or push:${{ steps.build.outputs.missing }}"
|
||||
[ "${{ steps.latest.outputs.latest_failed }}" != "1" ] || \
|
||||
echo "::error::${{ steps.plan.outputs.current }} has no image in the registry, so :latest was not moved"
|
||||
exit 1
|
||||
+7
-3
@@ -6,9 +6,13 @@
|
||||
# image installs that exact pinned release from registry.npmjs.org, so its
|
||||
# provenance chain is: upstream tag -> upstream npm release -> this Dockerfile.
|
||||
#
|
||||
# No upstream code is patched. The all-interfaces bind goes through upstream's
|
||||
# supported `--patch` launcher layer (/opt/deepseek-harness/web.cordis.patch.yml);
|
||||
# the UI is only reachable through ports you explicitly publish on the host.
|
||||
# No upstream code is patched. The bind address comes from the `--patch`
|
||||
# launcher layer (/opt/deepseek-harness/web.cordis.patch.yml), which the
|
||||
# entrypoint renders at startup: dsh 0.2.1-alpha.2+ rejects the unspecified
|
||||
# address in the webserver schema and gets the container's own IPv4 instead,
|
||||
# while releases whose schema still only accepts 127.0.0.1 or 0.0.0.0 keep
|
||||
# 0.0.0.0. The UI is reachable only through ports you explicitly publish on the
|
||||
# host.
|
||||
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
|
||||
Regular → Executable
+65
@@ -38,6 +38,71 @@ else
|
||||
-*) set -- web --patch "$PATCH" --no-open "$@" ;;
|
||||
esac
|
||||
fi
|
||||
# 0.2.1-alpha.2 replaced the webserver's old `host: "127.0.0.1" | "0.0.0.0"`
|
||||
# union with a config transform that rejects the unspecified address outright —
|
||||
# "host … is an unspecified (wildcard) address, which is not supported … bind
|
||||
# one concrete IPv4 or IPv6 address of a local interface instead" — and a
|
||||
# schema-level check cannot be opted out of from the launcher. The shipped patch
|
||||
# is therefore a template: render it onto this container's own address and hand
|
||||
# dsh the rendered copy, so a port published on the host still reaches the UI
|
||||
# while the socket binds one concrete interface instead of every interface.
|
||||
# Releases whose schema still accepts the wildcard keep 0.0.0.0, so old images
|
||||
# rebuild exactly as before. DSH_WEB_HOST overrides the address.
|
||||
if [ -r "$PATCH" ]; then
|
||||
BIND_HOST="${DSH_WEB_HOST:-}"
|
||||
if [ -z "$BIND_HOST" ]; then
|
||||
# Detect the installed webserver schema rather than guessing from a
|
||||
# version string.
|
||||
dsh_root="${DSH_INSTALL_ROOT:-/usr/local/lib/node_modules/@deepseek-ai/dsh}"
|
||||
ws="$dsh_root/node_modules/@deepseek-ai/dsh-host-webserver/lib/index.js"
|
||||
if [ -f "$ws" ] && grep -qs 'unspecified (wildcard) address' "$ws"; then
|
||||
wildcard_rejected=1
|
||||
elif [ ! -f "$ws" ] && grep -rqs 'unspecified (wildcard) address' "$dsh_root" 2>/dev/null; then
|
||||
wildcard_rejected=1
|
||||
else
|
||||
wildcard_rejected=0
|
||||
fi
|
||||
|
||||
if [ "$wildcard_rejected" = 1 ]; then
|
||||
# Same discovery dsh's own boot smoke test uses: first non-internal IPv4.
|
||||
BIND_HOST="$(node -e 'const os=require("os");const n=Object.values(os.networkInterfaces()).flat().find(i=>i&&i.family==="IPv4"&&!i.internal);process.stdout.write(n?n.address:"")' 2>/dev/null || true)"
|
||||
[ -n "$BIND_HOST" ] || BIND_HOST=127.0.0.1
|
||||
else
|
||||
BIND_HOST=0.0.0.0
|
||||
fi
|
||||
fi
|
||||
|
||||
render_patch() {
|
||||
sed -e "s|__DSH_BIND_HOST__|${BIND_HOST}|g" \
|
||||
-e "s|^\([[:space:]]*host:[[:space:]]*\)0\.0\.0\.0[[:space:]]*$|\1${BIND_HOST}|" "$PATCH"
|
||||
}
|
||||
|
||||
mkdir -p "$STATE" 2>/dev/null || true
|
||||
RUNTIME_PATCH="$STATE/web.cordis.patch.yml"
|
||||
if ! render_patch > "$RUNTIME_PATCH" 2>/dev/null; then
|
||||
RUNTIME_PATCH=/tmp/web.cordis.patch.yml
|
||||
render_patch > "$RUNTIME_PATCH"
|
||||
fi
|
||||
chmod 0644 "$RUNTIME_PATCH" 2>/dev/null || true
|
||||
log "webserver binds ${BIND_HOST}; runtime patch: ${RUNTIME_PATCH}"
|
||||
|
||||
# Point every --patch argument that names the shipped template at the
|
||||
# rendered copy. Rebuilt with a rotating `set --`, so quoting survives.
|
||||
expect=0
|
||||
left=$#
|
||||
while [ "$left" -gt 0 ]; do
|
||||
arg="$1"
|
||||
shift
|
||||
if [ "$expect" = 1 ]; then
|
||||
if [ "$arg" = "$PATCH" ]; then arg="$RUNTIME_PATCH"; fi
|
||||
expect=0
|
||||
elif [ "$arg" = "--patch" ]; then
|
||||
expect=1
|
||||
fi
|
||||
set -- "$@" "$arg"
|
||||
left=$((left - 1))
|
||||
done
|
||||
fi
|
||||
|
||||
# True when anything under $1 is not owned by PUID:PGID. `-print -quit` stops
|
||||
# at the first hit, so the common "already correct" case costs one stat.
|
||||
|
||||
+14
-6
@@ -1,10 +1,18 @@
|
||||
# Launcher patch layer for containerized `dsh web` (passed via --patch).
|
||||
# Upstream's CLI refuses `--host 0.0.0.0` (deliberate friction: the web UI can
|
||||
# execute code), but the webserver plugin's schema supports exactly
|
||||
# '127.0.0.1' | '0.0.0.0'; this file is the supported opt-in. Inside a
|
||||
# container an all-interfaces bind is only reachable through ports you
|
||||
# explicitly publish on the Docker host.
|
||||
#
|
||||
# The host is a placeholder, not a value. From 0.2.1-alpha.2 the webserver
|
||||
# schema rejects the unspecified address outright ("host … is an unspecified
|
||||
# (wildcard) address, which is not supported … bind one concrete IPv4 or IPv6
|
||||
# address of a local interface instead") and that check is a config transform,
|
||||
# so no launcher flag can opt out of it; a static 0.0.0.0 — which the earlier
|
||||
# `"127.0.0.1" | "0.0.0.0"` schema accepted — now fails the boot smoke test.
|
||||
#
|
||||
# The image entrypoint renders this file at startup: onto the container's own
|
||||
# IPv4 address when the installed schema rejects the wildcard, otherwise back
|
||||
# to 0.0.0.0 so older images build unchanged. A port published on the Docker
|
||||
# host still reaches the UI either way, because the published port is forwarded
|
||||
# to exactly that address. Set DSH_WEB_HOST to pin a different address.
|
||||
- id: webserver
|
||||
config:
|
||||
host: 0.0.0.0
|
||||
host: __DSH_BIND_HOST__
|
||||
port: 3080
|
||||
Reference in new issue
Block a user