Track the newest release candidate: :latest alias, current-version freshness, on-demand revisions
Sync upstream and build images / sync-and-build (push) Successful in 2m4s
Sync upstream and build images / sync-and-build (push) Successful in 2m4s
Operating model change, at the operator's request: the deployment follows the newest release candidate rather than a hand-pinned version. - :latest is now published, as a manifest copy (identical digest) of the newest build of the newest RC version. It is the ONLY tag that moves; every <version>[-rN] tag stays immutable for anyone who wants to pin. - Freshness (pnpm / base image / recipe hash) now applies to the current version derived from the registry, not to a Dockerfile constant, so an image fix reaches the deployed line automatically. The `revise` dispatch input rebuilds any older versions with the current recipe on demand. - Smoke test now runs on a fresh named volume (root-owned, like a new Unraid appdata dir) and asserts pid 1 is uid 1000 - the ownership fix is exercised on every build, not just locally. - Releases: `latest` gets no release; the "pinned deployment" callout is gone (the README and the alias are the source of truth now). - 0.1.1-rc.2 is deprecated; its -r2/-r49/-r52 revisions were removed (images, tags, releases). The original tag remains as history. - README/template: :latest in the Repository field, auto-update warning, versions table and upgrade procedure rewritten for the new model. Forensic workflow's trusted default -> :latest. inspect-deployment default container name -> Deepseek. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
19e87258ce
commit
76112eadf6
6 files changed
+176
-119
No files matched your search
@@ -28,8 +28,6 @@ const GITEA_TOKEN = env('GITEA_TOKEN');
|
||||
const REG_TOKEN = env('REG_TOKEN');
|
||||
const MAIN_SHA = env('MAIN_SHA');
|
||||
const RUN_NUMBER = env('RUN_NUMBER', '');
|
||||
// The Dockerfile's default DSH_VERSION — the version the deployment is pinned to.
|
||||
const PINNED = env('PINNED', '');
|
||||
const DRY_RUN = process.env.DRY_RUN === '1';
|
||||
const REGISTRY = new URL(GITEA).host;
|
||||
const UPSTREAM = 'deepseek-ai/deepseek-harness';
|
||||
@@ -176,25 +174,18 @@ async function imageFacts(tag) {
|
||||
|
||||
function renderBody({ tag, ver, facts, builtFrom, notes }) {
|
||||
const pull = `gitea.milesward.dev/${IMAGE}:${tag}`;
|
||||
const pinned = PINNED !== '' && ver === PINNED;
|
||||
// A revision says only what is certain: same upstream release, different
|
||||
// image build. Which input moved (pnpm, the base image, or the container's
|
||||
// own tooling) is visible in the labels and the commit this release links to.
|
||||
let intro = isRevision(tag)
|
||||
const intro = isRevision(tag)
|
||||
? `Image revision of **DeepSeek Harness (dsh) ${ver}** — the same upstream release, rebuilt from an updated image recipe (a patched base image, a newer pnpm, or a fix to the container's own tooling). Safe to move to from any other build of ${ver}.`
|
||||
: `Container build of **DeepSeek Harness (dsh) ${ver}** from the official npm release, built and smoke-tested by CI.`;
|
||||
if (pinned) {
|
||||
intro += isRevision(tag)
|
||||
? ' This is a refresh of the **pinned deployment version**.'
|
||||
: ' **This is the pinned deployment version** — the Dockerfile default and the tag the Unraid template runs. Newer versions are published as their own tags and adopted only by editing the template.';
|
||||
}
|
||||
|
||||
const lines = [
|
||||
intro, '',
|
||||
'```', `docker pull ${pull}`, '```', '',
|
||||
"### What's inside", '',
|
||||
'| | |', '|---|---|',
|
||||
...(pinned ? ['| role | **Pinned deployment version** |'] : []),
|
||||
`| dsh | \`${ver}\` — [upstream release notes](https://github.com/${UPSTREAM}/releases/tag/dsh-v${ver}) |`,
|
||||
`| pnpm | \`${facts.pnpm || 'unknown'}\` |`,
|
||||
`| base image | \`${facts.base || 'unknown'}\` |`,
|
||||
@@ -222,7 +213,7 @@ function renderBody({ tag, ver, facts, builtFrom, notes }) {
|
||||
'- **Post Arguments** (replace the image command entirely): `web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host <host> [--trusted-host <host2>]`',
|
||||
'- First run: `chown -R 1000:1000 /mnt/user/appdata/deepseek-harness`',
|
||||
'',
|
||||
`Image tags are immutable — this release will never be rebuilt or moved. See the [README](${GITEA}/${REPO}#versions-and-upgrades) for the upgrade procedure.`,
|
||||
`This tag is immutable — it will never be rebuilt or moved. \`:latest\` always points at the newest build of the newest release candidate; pin a version tag if you want a fixed deployment. See the [README](${GITEA}/${REPO}#versions-and-upgrades) for the upgrade procedure.`,
|
||||
);
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -239,8 +230,9 @@ async function registryTags() {
|
||||
const name = IMAGE.split('/')[1];
|
||||
const { status, json } = await gitea('GET', `/packages/${OWNER}?type=container&q=${name}&limit=100`);
|
||||
if (status !== 200 || !Array.isArray(json)) throw new Error(`package list: HTTP ${status}`);
|
||||
// Version-shaped tags only: digests and the moving `latest` alias get no release.
|
||||
return json
|
||||
.filter((p) => p.name === name && !p.version.startsWith('sha256:'))
|
||||
.filter((p) => p.name === name && /^\d/.test(p.version))
|
||||
.map((p) => p.version)
|
||||
.sort(compareVersions);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
inputs:
|
||||
container:
|
||||
description: Container name to inspect
|
||||
default: DeepSeek-Harness
|
||||
default: Deepseek
|
||||
|
||||
jobs:
|
||||
inspect:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Locate container
|
||||
run: |
|
||||
set -eu
|
||||
C="${{ inputs.container || 'DeepSeek-Harness' }}"
|
||||
C="${{ inputs.container || 'Deepseek' }}"
|
||||
if ! docker inspect "$C" > /dev/null 2>&1; then
|
||||
echo "::error::no container named $C on this host"
|
||||
echo "== containers on this host whose image mentions deepseek =="
|
||||
@@ -43,6 +43,6 @@ jobs:
|
||||
- name: Probe tooling inside the running container
|
||||
run: |
|
||||
set -eu
|
||||
C="${{ inputs.container || 'DeepSeek-Harness' }}"
|
||||
C="${{ inputs.container || 'Deepseek' }}"
|
||||
echo "== inside $C =="
|
||||
docker exec "$C" sh -c 'echo "uid=$(id -u) HOME=$HOME PATH=$PATH"; echo "dsh: $(command -v dsh || echo MISSING) $(dsh --version 2>/dev/null)"; echo "pnpm: $(command -v pnpm || echo MISSING) $(pnpm --version 2>/dev/null)"; echo "node: $(node --version)"; ls -la /usr/local/bin | grep -E "dsh|pnpm" || true'
|
||||
@@ -2,23 +2,22 @@ name: Sync upstream and build images
|
||||
|
||||
# Keeps this repository current with the official upstream
|
||||
# (https://github.com/deepseek-ai/deepseek-harness) and the registry stocked
|
||||
# with IMMUTABLE version tags:
|
||||
# with immutable version tags plus one moving convenience tag:
|
||||
# 1. Mirrors upstream master into this repo's master branch.
|
||||
# 2. Builds a new <version> tag for each @deepseek-ai/dsh release that npm
|
||||
# publishes as `latest` or `next` (plus any version named in a manual
|
||||
# dispatch) and the registry lacks.
|
||||
# 3. Freshness check for the PINNED default version (ARG DSH_VERSION in the
|
||||
# Dockerfile): if a newer pnpm release or a patched node base image has
|
||||
# shipped, or the build recipe itself changed (Dockerfile, entrypoint,
|
||||
# dsh wrapper, bind patch), builds a <version>-r<run> revision tag. This
|
||||
# is how an image-level fix reaches the pinned deployment without waiting
|
||||
# for the next upstream dsh release.
|
||||
# 4. Publishes a v<tag> git tag + Gitea release (digest, toolchain, upstream
|
||||
# highlights, deployment caveats) for every image tag without one.
|
||||
#
|
||||
# This workflow NEVER pushes or moves `latest`, and never re-pushes an
|
||||
# existing tag: every upgrade of the running deployment is a deliberate
|
||||
# template edit to a new immutable tag.
|
||||
# 3. Freshness check for the CURRENT version (the newest release-candidate
|
||||
# build in the registry): if a newer pnpm, a patched node base image, or
|
||||
# a changed build recipe (Dockerfile, entrypoint, wrapper, bind patch)
|
||||
# has appeared since its newest build, builds <version>-r<run>. Older
|
||||
# versions can be rebuilt with the current recipe on demand via the
|
||||
# `revise` input.
|
||||
# 4. Points `:latest` at the newest build of the current version. This is
|
||||
# the ONLY tag that ever moves; every <version>[-rN] tag is immutable, so
|
||||
# pinning one is always available to anyone who wants a fixed deployment.
|
||||
# 5. Publishes a v<tag> git tag + Gitea release (digest, toolchain, upstream
|
||||
# highlights, deployment caveats) for every version tag without one.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -29,6 +28,10 @@ on:
|
||||
description: Build this exact dsh version too (e.g. 0.1.5-alpha.2); latest/next are always considered
|
||||
required: false
|
||||
default: ''
|
||||
revise:
|
||||
description: Space-separated existing versions to rebuild with the current recipe as <version>-r<run> (e.g. "0.1.2-rc.1 0.1.5-rc.1")
|
||||
required: false
|
||||
default: ''
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
@@ -66,35 +69,40 @@ jobs:
|
||||
git remote add mirror "https://oauth2:${TOKEN}@${REGISTRY}/${IMAGE}.git"
|
||||
git push mirror '+refs/heads/master:refs/heads/master'
|
||||
|
||||
- name: Plan builds (new dsh versions + pinned-version freshness)
|
||||
- name: Plan builds (new versions, current-version freshness, requested revisions)
|
||||
id: plan
|
||||
env:
|
||||
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
REQUESTED: ${{ inputs.version }}
|
||||
REVISE: ${{ inputs.revise }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
set -eu
|
||||
DSH_LATEST=$(npm view @deepseek-ai/dsh dist-tags.latest)
|
||||
DSH_NEXT=$(npm view @deepseek-ai/dsh dist-tags.next || true)
|
||||
PNPM_LATEST=$(npm view pnpm dist-tags.latest)
|
||||
PINNED=$(sed -n 's/^ARG DSH_VERSION=//p' Dockerfile | head -1)
|
||||
docker pull -q "$BASE_IMAGE" > /dev/null
|
||||
BASE_DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "$BASE_IMAGE")
|
||||
RECIPE_HASH=$(cat Dockerfile docker-entrypoint.sh run-as-runtime-user.sh web.cordis.patch.yml | sha256sum | cut -c1-12)
|
||||
echo "dsh latest=$DSH_LATEST next=$DSH_NEXT requested='$REQUESTED' pinned=$PINNED pnpm=$PNPM_LATEST base=$BASE_DIGEST recipe=$RECIPE_HASH"
|
||||
echo "dsh latest=$DSH_LATEST next=$DSH_NEXT requested='$REQUESTED' revise='$REVISE' pnpm=$PNPM_LATEST base=$BASE_DIGEST recipe=$RECIPE_HASH"
|
||||
|
||||
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'
|
||||
tag_exists() {
|
||||
[ "$(curl -s -o /dev/null -w '%{http_code}' -u "${{ github.repository_owner }}:${TOKEN}" \
|
||||
[ "$(curl -s -o /dev/null -w '%{http_code}' -u "${OWNER}:${TOKEN}" \
|
||||
-H "Accept: $ACCEPT" "https://${REGISTRY}/v2/${IMAGE}/manifests/$1")" = 200 ]
|
||||
}
|
||||
# Every version-shaped tag in the registry (no digests, no `latest`).
|
||||
registry_tags() {
|
||||
curl -s -H "Authorization: token ${TOKEN}" \
|
||||
"https://${REGISTRY}/api/v1/packages/${OWNER}?type=container&q=deepseek-harness&limit=100" \
|
||||
| tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | grep -E '^[0-9]' || true
|
||||
}
|
||||
# Provenance labels from a tag's image config ("pnpm|base|recipe").
|
||||
# Empty output means "treat as stale". A multi-arch tag resolves to an
|
||||
# index, whose first child manifest is followed to reach the config —
|
||||
# without that, such a tag would look stale on every run and spam a
|
||||
# new revision build every six hours.
|
||||
# index, whose first child manifest is followed to reach the config.
|
||||
fetch_manifest() {
|
||||
curl -s -u "${{ github.repository_owner }}:${TOKEN}" -H "Accept: $ACCEPT" \
|
||||
"https://${REGISTRY}/v2/${IMAGE}/manifests/$1"
|
||||
curl -s -u "${OWNER}:${TOKEN}" -H "Accept: $ACCEPT" "https://${REGISTRY}/v2/${IMAGE}/manifests/$1"
|
||||
}
|
||||
first_digest() { printf '%s' "$1" | grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 | grep -o 'sha256:[a-f0-9]*'; }
|
||||
tag_toolchain() {
|
||||
@@ -106,7 +114,7 @@ jobs:
|
||||
fi
|
||||
CFG=$(first_digest "$MAN") || return 0
|
||||
[ -n "$CFG" ] || return 0
|
||||
BLOB=$(curl -s -u "${{ github.repository_owner }}:${TOKEN}" "https://${REGISTRY}/v2/${IMAGE}/blobs/$CFG") || return 0
|
||||
BLOB=$(curl -s -u "${OWNER}:${TOKEN}" "https://${REGISTRY}/v2/${IMAGE}/blobs/$CFG") || return 0
|
||||
P=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.pnpm-version":"[^"]*"' | cut -d'"' -f4 || true)
|
||||
B=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.base-digest":"[^"]*"' | cut -d'"' -f4 || true)
|
||||
R=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.recipe-hash":"[^"]*"' | cut -d'"' -f4 || true)
|
||||
@@ -114,6 +122,8 @@ jobs:
|
||||
}
|
||||
|
||||
BUILDS=""
|
||||
planned() { case " $BUILDS " in *" $1="*) return 0;; *) return 1;; esac; }
|
||||
|
||||
# 1. Every candidate release the registry lacks -> new version tag.
|
||||
# Candidates: npm `latest`, npm `next`, and an explicitly requested
|
||||
# version (alphas are only built on request).
|
||||
@@ -122,31 +132,41 @@ jobs:
|
||||
BUILDS="$BUILDS $V=$V"
|
||||
fi
|
||||
done
|
||||
# 2. Pinned version: build it if missing; otherwise compare the newest
|
||||
# existing build's toolchain labels and cut a -rN revision when
|
||||
# pnpm or the base image moved.
|
||||
case " $BUILDS " in *" $PINNED=$PINNED "*) PINNED_PLANNED=1;; *) PINNED_PLANNED="";; esac
|
||||
if [ -z "$PINNED_PLANNED" ] && ! tag_exists "$PINNED"; then
|
||||
BUILDS="$BUILDS $PINNED=$PINNED"
|
||||
elif [ -z "$PINNED_PLANNED" ]; then
|
||||
NEWEST=$(curl -s -H "Authorization: token ${TOKEN}" \
|
||||
"https://${REGISTRY}/api/v1/packages/${{ github.repository_owner }}?type=container&q=deepseek-harness&limit=100" \
|
||||
| tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' \
|
||||
| grep -E "^${PINNED}(-r[0-9]+)?$" | sort -V | tail -1)
|
||||
|
||||
# 2. CURRENT = newest release-candidate version (registry + planned;
|
||||
# alphas never qualify). Its newest build must carry the current
|
||||
# toolchain and recipe, or a -r<run> revision is cut.
|
||||
EXISTING=$(registry_tags)
|
||||
CURRENT=$( { printf '%s\n' $EXISTING; for e in $BUILDS; do printf '%s\n' "${e#*=}"; done; } \
|
||||
| sed 's/-r[0-9]*$//' | grep -v alpha | sort -uV | tail -1)
|
||||
echo "current version: $CURRENT"
|
||||
if [ -n "$CURRENT" ] && ! planned "$CURRENT"; then
|
||||
NEWEST=$(printf '%s\n' $EXISTING | grep -E "^${CURRENT}(-r[0-9]+)?$" | sort -V | tail -1)
|
||||
TOOL=$(tag_toolchain "$NEWEST")
|
||||
echo "newest build of pinned: $NEWEST toolchain='$TOOL'"
|
||||
echo "newest build of current: $NEWEST toolchain='$TOOL'"
|
||||
if [ "$TOOL" != "${PNPM_LATEST}|${BASE_DIGEST}|${RECIPE_HASH}" ]; then
|
||||
echo "stale (want ${PNPM_LATEST}|${BASE_DIGEST}|${RECIPE_HASH}) -> revision build"
|
||||
BUILDS="$BUILDS ${PINNED}-r${{ github.run_number }}=$PINNED"
|
||||
BUILDS="$BUILDS ${CURRENT}-r${RUN_NUMBER}=$CURRENT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Requested revisions of other existing versions.
|
||||
for V in $REVISE; do
|
||||
if planned "$V" || planned "${V}-r${RUN_NUMBER}"; then continue; fi
|
||||
if tag_exists "$V"; then
|
||||
BUILDS="$BUILDS ${V}-r${RUN_NUMBER}=$V"
|
||||
else
|
||||
echo "::warning::revise: $V is not in the registry; use the version input to build it first"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "planned builds: '${BUILDS}'"
|
||||
{
|
||||
echo "builds=${BUILDS}"
|
||||
echo "pnpm=${PNPM_LATEST}"
|
||||
echo "base_digest=${BASE_DIGEST}"
|
||||
echo "recipe_hash=${RECIPE_HASH}"
|
||||
echo "current=${CURRENT}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build, smoke-test, and push
|
||||
@@ -177,14 +197,17 @@ jobs:
|
||||
-t "$REF" .
|
||||
|
||||
# Smoke: versions match, and the web server actually boots with
|
||||
# the baked patch and answers HTTP on its non-loopback interface.
|
||||
# 200 = pre-token versions; 303/401 = 0.1.2+ launch-token auth.
|
||||
# the baked patch and answers HTTP on its non-loopback interface —
|
||||
# on a ROOT-owned state volume, which is what a fresh Unraid appdata
|
||||
# directory looks like. 200 = pre-token versions; 303/401 = 0.1.2+.
|
||||
SMOKE="dsh-smoke-${{ github.run_id }}"
|
||||
docker rm -f "$SMOKE" 2>/dev/null || true
|
||||
docker run -d --name "$SMOKE" "$REF"
|
||||
docker volume rm -f "$SMOKE" 2>/dev/null || true
|
||||
docker run -d --name "$SMOKE" --mount "type=volume,src=$SMOKE,dst=/home/node/.dsh" "$REF"
|
||||
sleep 3
|
||||
[ "$(docker exec "$SMOKE" dsh --version)" = "$VER" ]
|
||||
[ "$(docker exec "$SMOKE" pnpm --version)" = "$PNPM_VERSION" ]
|
||||
[ "$(docker exec "$SMOKE" awk '/^Uid:/{print $2}' /proc/1/status)" = "1000" ]
|
||||
OK=""
|
||||
for i in $(seq 1 30); do
|
||||
if docker exec "$SMOKE" node -e '
|
||||
@@ -200,18 +223,48 @@ jobs:
|
||||
if [ -z "$OK" ]; then
|
||||
echo "::error::smoke test failed for $REF (server never answered on the container interface)"
|
||||
docker logs "$SMOKE" | tail -50
|
||||
docker rm -f "$SMOKE"
|
||||
docker rm -f "$SMOKE"; docker volume rm -f "$SMOKE" || true
|
||||
exit 1
|
||||
fi
|
||||
docker rm -f "$SMOKE"
|
||||
docker rm -f "$SMOKE"; docker volume rm -f "$SMOKE" || true
|
||||
|
||||
docker push "$REF"
|
||||
done
|
||||
|
||||
# Every image tag in the registry gets a matching v<tag> git tag + Gitea
|
||||
# `:latest` is the one moving tag: the newest build of the current
|
||||
# (newest release-candidate) version. Implemented as a manifest copy
|
||||
# through the registry API, so the digest is byte-identical to the
|
||||
# version tag it points at and nothing is rebuilt or re-pushed.
|
||||
- name: Point :latest at the newest build of the current version
|
||||
env:
|
||||
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
CURRENT: ${{ steps.plan.outputs.current }}
|
||||
run: |
|
||||
set -eu
|
||||
[ -n "$CURRENT" ] || { echo "no current version"; exit 0; }
|
||||
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'
|
||||
TARGET=$(curl -s -H "Authorization: token ${TOKEN}" \
|
||||
"https://${REGISTRY}/api/v1/packages/${OWNER}?type=container&q=deepseek-harness&limit=100" \
|
||||
| tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' \
|
||||
| grep -E "^${CURRENT}(-r[0-9]+)?$" | sort -V | tail -1)
|
||||
[ -n "$TARGET" ] || { echo "::error::no build of ${CURRENT} in the registry"; exit 1; }
|
||||
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 "$TARGET")
|
||||
HAVE=$(digest_of latest || true)
|
||||
echo "latest -> $TARGET ($WANT); currently ${HAVE:-unset}"
|
||||
if [ "$WANT" = "$HAVE" ]; then echo "latest already current"; exit 0; fi
|
||||
curl -s -u "${OWNER}:${TOKEN}" -H "Accept: $ACCEPT" -D /tmp/hdr -o /tmp/manifest "https://${REGISTRY}/v2/${IMAGE}/manifests/${TARGET}"
|
||||
CT=$(grep -i '^content-type' /tmp/hdr | awk '{print $2}' | tr -d '\r')
|
||||
CODE=$(curl -s -o /tmp/put.out -w '%{http_code}' -X PUT -u "${OWNER}:${TOKEN}" -H "Content-Type: ${CT}" \
|
||||
--data-binary @/tmp/manifest "https://${REGISTRY}/v2/${IMAGE}/manifests/latest")
|
||||
[ "$CODE" = 201 ] || { echo "::error::retag failed: HTTP $CODE"; cat /tmp/put.out; exit 1; }
|
||||
echo "latest now -> $TARGET ($(digest_of latest))"
|
||||
|
||||
# Every version tag in the registry gets a matching v<tag> git tag + Gitea
|
||||
# release with digest, toolchain, upstream highlights and deployment
|
||||
# caveats. Idempotent (only tags without a release are published), and it
|
||||
# runs on the image base so the script needs nothing from the runner.
|
||||
# caveats (`latest` is an alias and gets none). Idempotent; runs on the
|
||||
# image base so the script needs nothing from the runner.
|
||||
- name: Publish releases for image tags without one
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -222,8 +275,7 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
PINNED=$(sed -n 's/^ARG DSH_VERSION=//p' Dockerfile | head -1)
|
||||
docker run --rm -i \
|
||||
-e "GITEA=https://${REGISTRY}" -e OWNER -e REPO -e IMAGE -e GITEA_TOKEN -e REG_TOKEN \
|
||||
-e "MAIN_SHA=${MAIN_SHA}" -e RUN_NUMBER -e "PINNED=${PINNED}" \
|
||||
-e "MAIN_SHA=${MAIN_SHA}" -e RUN_NUMBER \
|
||||
"$BASE_IMAGE" node - < .gitea/scripts/publish-releases.js
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
default: runzhliu/deepseek-harness:0.1.1-rc.2
|
||||
trusted:
|
||||
description: Trusted image reference
|
||||
default: gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2
|
||||
default: gitea.milesward.dev/mward4/deepseek-harness:latest
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
SUSPECT="${{ inputs.suspect || 'runzhliu/deepseek-harness:0.1.1-rc.2' }}"
|
||||
TRUSTED="${{ inputs.trusted || 'gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2' }}"
|
||||
TRUSTED="${{ inputs.trusted || 'gitea.milesward.dev/mward4/deepseek-harness:latest' }}"
|
||||
docker pull -q "$TRUSTED"
|
||||
mkdir -p /tmp/suspect /tmp/trusted
|
||||
s=$(docker create "$SUSPECT")
|
||||
|
||||
+7
-7
@@ -12,10 +12,10 @@
|
||||
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
# DSH_VERSION default is the pinned, known-good deployment version: 0.1.2+
|
||||
# introduced launch-token auth and the @Remote gateway (breaks lanmode).
|
||||
# Moving off it is a deliberate template edit, never a side effect of CI.
|
||||
ARG DSH_VERSION=0.1.1-rc.2
|
||||
# DSH_VERSION default: the current release candidate, for local builds. CI
|
||||
# passes every version it builds explicitly and derives "current" from the
|
||||
# registry, so this default never steers the pipeline.
|
||||
ARG DSH_VERSION=0.1.5-rc.2
|
||||
# pnpm is required on PATH by dsh's plugin manager (`dsh plugin --profile web
|
||||
# add|update|remove`). CI passes the current release; the scheduled freshness
|
||||
# check rebuilds a -rN revision tag when a newer pnpm or base image ships.
|
||||
@@ -87,9 +87,9 @@ ARG GIT_SHA=""
|
||||
ARG CI_RUN=""
|
||||
ARG BUILD_DATE=""
|
||||
# RECIPE_HASH: digest of the build inputs (this Dockerfile, the entrypoint,
|
||||
# the dsh wrapper, the bind patch). The freshness check cuts a -rN revision of
|
||||
# the pinned version when it changes, so an image-recipe fix actually reaches
|
||||
# the deployment instead of waiting for the next upstream release.
|
||||
# the wrapper, the bind patch). The freshness check cuts a -rN revision of the
|
||||
# current version when it changes, so an image-recipe fix actually reaches the
|
||||
# deployment instead of waiting for the next upstream release.
|
||||
ARG RECIPE_HASH=""
|
||||
LABEL org.opencontainers.image.title="deepseek-harness" \
|
||||
org.opencontainers.image.description="DeepSeek Harness (dsh) web UI, built from the official npm release" \
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="pinned" src="https://img.shields.io/badge/deployed-0.1.1--rc.2-4d6bfe" />
|
||||
<img alt="newest" src="https://img.shields.io/gitea/v/release/mward4/deepseek-harness?gitea_url=https%3A%2F%2Fgitea.milesward.dev&include_prereleases&sort=semver&label=newest%20build&color=2874d7" />
|
||||
<img alt="tags" src="https://img.shields.io/badge/tags-immutable%20%C2%B7%20no%20%3Alatest-59824f" />
|
||||
<img alt="latest" src="https://img.shields.io/gitea/v/release/mward4/deepseek-harness?gitea_url=https%3A%2F%2Fgitea.milesward.dev&include_prereleases&sort=semver&label=%3Alatest&color=4d6bfe" />
|
||||
<img alt="tags" src="https://img.shields.io/badge/version%20tags-immutable-59824f" />
|
||||
<img alt="ci" src="https://gitea.milesward.dev/mward4/deepseek-harness/actions/workflows/sync-upstream.yml/badge.svg?branch=main" />
|
||||
<img alt="base" src="https://img.shields.io/badge/base-node%2022%20%C2%B7%20bookworm--slim-339933?logo=node.js&logoColor=white" />
|
||||
<img alt="license" src="https://img.shields.io/badge/license-MIT-330f1f" />
|
||||
@@ -32,11 +31,11 @@ properly: the upstream source is mirrored here, the image installs the exact pin
|
||||
official release on a slim Node base, and CI re-verifies and republishes on a cadence —
|
||||
without ever changing what a running deployment executes.
|
||||
|
||||
> **Status.** The deployed version is pinned to **`0.1.1-rc.2`**. Newer upstream
|
||||
> releases are built and published automatically as their own tags (currently up to
|
||||
> `0.1.5-rc.2`) but nothing moves until the template is edited by hand — see
|
||||
> [Versions and upgrades](#versions-and-upgrades) for why, and for what changed in
|
||||
> `0.1.2+` before you jump.
|
||||
> **Status.** `:latest` tracks the newest release-candidate build (currently
|
||||
> **`0.1.5-rc.2`**) and is the tag the Unraid template runs. Every `<version>[-rN]` tag is
|
||||
> immutable, so pinning one is always an option. `dsh` is a developer preview: read
|
||||
> [Versions and upgrades](#versions-and-upgrades) before applying an update Unraid offers
|
||||
> you — `0.1.2+` added launch-token auth and `0.1.5` migrates session logs irreversibly.
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -64,11 +63,14 @@ Three things went wrong with the alternatives, and this repo is shaped by each:
|
||||
was never going to be re-audited on every update. Here the provenance is a chain you
|
||||
can follow: **upstream tag → upstream npm release → this Dockerfile → this registry**,
|
||||
with the Dockerfile in plain sight and no upstream code modified.
|
||||
- **`:latest` silently upgraded a live deployment.** A routine container restart pulled
|
||||
a moving tag and jumped from `0.1.1-rc.2` to the `0.1.2` line, which added mandatory
|
||||
launch-token auth and swapped the API gateway — breaking remote access and a plugin in
|
||||
one go. So this registry publishes **immutable version tags only**. There is no
|
||||
`:latest`, and CI is incapable of re-pushing an existing tag.
|
||||
- **A moving tag once upgraded a live deployment by surprise.** An update pulled `:latest`
|
||||
and jumped from `0.1.1-rc.2` to the `0.1.2` line, which added mandatory launch-token
|
||||
auth and swapped the API gateway — breaking remote access and a plugin in one go. So
|
||||
every **version tag here is immutable** and CI is incapable of re-pushing one; `:latest`
|
||||
exists as a convenience alias to the newest release-candidate build, and the rule for
|
||||
living with it is simple: **do not enable Unraid auto-update on this container**, and
|
||||
read the release notes before clicking "update". Anyone who wants a fixed deployment
|
||||
pins a version tag instead.
|
||||
- **Upstream refuses to bind `0.0.0.0` from the CLI**, on purpose (the web UI can execute
|
||||
code). The image doesn't patch that out; it uses upstream's own supported opt-in — a
|
||||
`--patch` launcher layer — so the safety intent survives and exposure is decided
|
||||
@@ -80,17 +82,20 @@ Three things went wrong with the alternatives, and this repo is shaped by each:
|
||||
- **Official release, pinned exactly.** `npm install -g @deepseek-ai/dsh@<version>` from
|
||||
`registry.npmjs.org`, nothing else. The version is a build argument with a known-good
|
||||
default.
|
||||
- **Immutable tags.** `<version>` for each upstream release, `<version>-r<N>` for a
|
||||
toolchain refresh of the same release. A tag's digest never changes.
|
||||
- **Immutable version tags, one moving alias.** `<version>` for each upstream release,
|
||||
`<version>-r<N>` for a rebuild of the same release; a version tag's digest never
|
||||
changes. `:latest` is a manifest-level alias to the newest build of the newest release
|
||||
candidate — same bytes, same digest as the version tag it points at.
|
||||
- **Smoke-tested before push.** Every build must report the intended `dsh` and `pnpm`
|
||||
versions *and* boot the web server with the baked patch and answer HTTP on its
|
||||
non-loopback interface — so a release the patch no longer applies to fails in CI,
|
||||
loudly, not on your NAS.
|
||||
- **Toolchain and recipe freshness.** CI compares the newest build of the pinned version
|
||||
- **Toolchain and recipe freshness.** CI compares the newest build of the current version
|
||||
against the current pnpm release, the current `node:22-bookworm-slim` digest (where
|
||||
`npm` itself comes from), and a hash of the build recipe; if any moved, it publishes a
|
||||
`-r<N>` revision — so an image-level fix reaches the deployment without waiting for an
|
||||
upstream release.
|
||||
`-r<N>` revision and moves `:latest` to it — so a security patch or an image-level fix
|
||||
reaches the deployment without waiting for an upstream release. Older versions can be
|
||||
rebuilt with the current recipe on demand.
|
||||
|
||||
#### Runtime hygiene
|
||||
- **Volume ownership repairs itself.** A bind mount arrives with the *host's* ownership,
|
||||
@@ -128,7 +133,7 @@ mkdir -p dsh/state dsh/workspace
|
||||
docker run -d --name dsh -p 3080:3080 \
|
||||
-v "$PWD/dsh/state:/home/node/.dsh" \
|
||||
-v "$PWD/dsh/workspace:/workspace" \
|
||||
gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2-r2 \
|
||||
gitea.milesward.dev/mward4/deepseek-harness:latest \
|
||||
web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open --trusted-host $(hostname)
|
||||
|
||||
open http://$(hostname):3080
|
||||
@@ -154,7 +159,9 @@ Add the container from the template below (Docker tab → Add Container → past
|
||||
drop it in `/boot/config/plugins/dockerMan/templates-user/`). Two fields are the whole
|
||||
operating contract:
|
||||
|
||||
- **Repository** is an explicit version tag. Never `:latest` — it doesn't exist here.
|
||||
- **Repository** is `:latest` (newest release-candidate build) or an explicit version tag
|
||||
if you want a fixed deployment. Either way, **leave Unraid's auto-update off** for this
|
||||
container: dsh versions can change behaviour, and the release notes tell you what.
|
||||
- **Post Arguments replace the image command entirely.** Always start with
|
||||
`web --patch /opt/deepseek-harness/web.cordis.patch.yml --no-open`, then add one
|
||||
`--trusted-host` per name you'll browse from.
|
||||
@@ -163,7 +170,7 @@ operating contract:
|
||||
<?xml version="1.0"?>
|
||||
<Container version="2">
|
||||
<Name>DeepSeek-Harness</Name>
|
||||
<Repository>gitea.milesward.dev/mward4/deepseek-harness:0.1.1-rc.2-r2</Repository>
|
||||
<Repository>gitea.milesward.dev/mward4/deepseek-harness:latest</Repository>
|
||||
<Registry>https://gitea.milesward.dev/mward4/deepseek-harness</Registry>
|
||||
<Network>bridge</Network>
|
||||
<Shell>sh</Shell>
|
||||
@@ -183,7 +190,7 @@ Without a matching --trusted-host, non-loopback requests get HTTP 403 on /api -
|
||||
|
||||
The Settings/credentials pages are additionally restricted to loopback pages by upstream design; edit them via SSH tunnel (ssh -L 3080:127.0.0.1:3080 root@unraid-ip, then browse http://127.0.0.1:3080). The @goodandready/dsh-lanmode community plugin can lift that restriction on dsh 0.1.1 (it is NOT compatible with 0.1.2+, and it is third-party code that disables an upstream security fence - review it before trusting it with your credentials).
|
||||
|
||||
VERSIONS: this registry publishes immutable version tags only - NO :latest. dsh is a developer preview with breaking changes between releases (0.1.2 adds mandatory launch-token auth: token in docker logs, rotates every restart; 0.1.5 migrates session logs to a format that cannot be downgraded). Upgrade by editing this tag deliberately, after backing up appdata. Tags like 0.1.1-rc.2-rN are the same dsh version rebuilt with a patched toolchain or an image fix - safe to move to within the same dsh version.
|
||||
VERSIONS: :latest follows the newest release-candidate build; every version tag (e.g. 0.1.5-rc.2, or 0.1.5-rc.2-rN for a rebuild of the same version) is immutable and can be pinned instead. dsh is a developer preview with breaking changes between releases: 0.1.2 added mandatory launch-token auth (token in docker logs, rotates every restart, exchanged for a 30-day cookie bound to the origin you open it on); 0.1.5 migrates session logs to a format that cannot be downgraded. LEAVE AUTO-UPDATE OFF for this container, back up appdata before applying an update, and read the release notes on the Gitea Releases page first.
|
||||
|
||||
PLUGINS: manage them with dsh's own tooling, e.g.
|
||||
docker exec DeepSeek-Harness dsh plugin --profile web add|update|remove PACKAGE
|
||||
@@ -239,31 +246,32 @@ Three rules that are easy to get wrong:
|
||||
|
||||
## Versions and upgrades
|
||||
|
||||
| Tag | dsh | Role |
|
||||
| Tag | dsh | Notes |
|
||||
|---|---|---|
|
||||
| `0.1.1-rc.2-r2` | 0.1.1-rc.2 | **Pinned / deployed.** Adds the self-healing volume ownership fix. |
|
||||
| `0.1.1-rc.2` | 0.1.1-rc.2 | Superseded by `-r1`: needs a manual `chown -R 1000:1000` on appdata. |
|
||||
| `0.1.2-rc.1` | 0.1.2-rc.1 | First launch-token release; Remote gateway replaces the ApiProxy. |
|
||||
| `0.1.5-rc.1` | 0.1.5-rc.1 | Upstream npm `latest`. Session format V3 — **no downgrade** once migrated. |
|
||||
| `0.1.5-rc.2` | 0.1.5-rc.2 | Upstream npm `next`. Feedback-dialog and file-card refinements on top of rc.1. |
|
||||
| `:latest` | → newest RC build | Moving alias; currently the newest build of `0.1.5-rc.2`. |
|
||||
| `0.1.5-rc.2` (+ `-rN`) | 0.1.5-rc.2 | Current. Upstream npm `next`; feedback-dialog and file-card refinements on rc.1. |
|
||||
| `0.1.5-rc.1` (+ `-rN`) | 0.1.5-rc.1 | Upstream npm `latest`. Session format V3 — **no downgrade** once migrated. |
|
||||
| `0.1.2-rc.1` (+ `-rN`) | 0.1.2-rc.1 | First launch-token release; Remote gateway replaces the ApiProxy. |
|
||||
| `0.1.1-rc.2` | 0.1.1-rc.2 | Deprecated. Predates the self-healing entrypoint: needs a manual `chown -R 1000:1000` on appdata. |
|
||||
|
||||
The full list, with digests and notes, is on the [Releases](../../releases) page — one
|
||||
release per image tag, published by CI.
|
||||
release per version tag, published by CI.
|
||||
|
||||
A `-r<N>` suffix means the same dsh version rebuilt with a patched toolchain or an image
|
||||
fix, so moving between `X` and `X-rN` is safe and needs no preparation. Changing the dsh
|
||||
version is the deliberate part.
|
||||
A `-r<N>` suffix means the same dsh version rebuilt from an updated image recipe (patched
|
||||
base image, newer pnpm, or a fix to the container's own tooling), so moving between `X` and
|
||||
`X-rN` is safe and needs no preparation. Changing the **dsh version** is the deliberate
|
||||
part — and with `:latest` in the template, that happens whenever you accept an update
|
||||
Unraid offers. Before you do:
|
||||
|
||||
`dsh` is a developer preview and upstream says so: expect breaking changes between
|
||||
releases. A **version** upgrade here is always three deliberate steps:
|
||||
|
||||
1. **Back up the state volume** (`/mnt/user/appdata/deepseek-harness`). From `0.1.5`
|
||||
1. **Read the release** for the version you're about to land on — every one lists what
|
||||
changed upstream and what it means for this deployment.
|
||||
2. **Back up the state volume** (`/mnt/user/appdata/deepseek-harness`). From `0.1.5`
|
||||
upstream migrates session logs to a new format that cannot be read by older versions.
|
||||
2. **Reconcile plugins first.** `@goodandready/dsh-lanmode` ≤ 0.6.8 breaks the web client
|
||||
3. **Reconcile plugins first.** `@goodandready/dsh-lanmode` ≤ 0.6.8 breaks the web client
|
||||
on `0.1.2+` (`web boot: … waiting for services: remote.*`). Remove or update it *before*
|
||||
changing the tag:
|
||||
`docker exec DeepSeek-Harness dsh plugin --profile web remove @goodandready/dsh-lanmode`
|
||||
3. **Edit the Repository tag** in the template and apply.
|
||||
updating:
|
||||
`docker exec <container> dsh plugin --profile web remove @goodandready/dsh-lanmode`
|
||||
4. **Apply the update** (or edit the Repository tag, if you pin).
|
||||
|
||||
### What changes in `0.1.2+`: launch-token auth
|
||||
|
||||
@@ -334,20 +342,25 @@ runners every six hours (`17 */6 * * *`) and on any change to the build inputs:
|
||||
2. **New releases** — for each `@deepseek-ai/dsh` version npm publishes as `latest` or
|
||||
`next` (release candidates; alphas only when asked for) that has no tag in the
|
||||
registry: build, smoke-test, push. A manual run can name any exact version.
|
||||
3. **Freshness** — reads the labels off the newest build of the pinned version and
|
||||
compares them to the current pnpm release, the current `node:22-bookworm-slim` digest,
|
||||
and a hash of the build recipe (Dockerfile, entrypoint, `dsh` wrapper, bind patch). If
|
||||
any moved, publishes `<version>-r<run>` — the path by which a security patch *or* an
|
||||
image fix reaches the pinned deployment.
|
||||
4. **Releases** — every image tag without a `v<tag>` release gets one, tagged at the
|
||||
3. **Freshness** — reads the labels off the newest build of the *current* version (the
|
||||
newest release candidate in the registry) and compares them to the current pnpm
|
||||
release, the current `node:22-bookworm-slim` digest, and a hash of the build recipe
|
||||
(Dockerfile, entrypoint, wrapper, bind patch). If any moved, publishes
|
||||
`<version>-r<run>` — the path by which a security patch *or* an image fix reaches the
|
||||
deployment. A manual run's `revise` input rebuilds any older versions the same way.
|
||||
4. **`:latest`** — re-pointed at the newest build of the current version whenever that
|
||||
changes, as a manifest copy through the registry API (identical digest, nothing
|
||||
rebuilt). It is the only tag that ever moves.
|
||||
5. **Releases** — every version tag without a `v<tag>` release gets one, tagged at the
|
||||
`main` commit recorded in the image's `org.opencontainers.image.revision` label, with
|
||||
digest and toolchain, the upstream release's English notes, and the caveats that apply
|
||||
to that version (launch token from `0.1.2`, session-format migration from `0.1.5`).
|
||||
Generated by [`publish-releases.js`](.gitea/scripts/publish-releases.js), which runs
|
||||
on the image's own Node base so the runner needs nothing installed.
|
||||
|
||||
Nothing in this pipeline can change an existing tag or publish `latest`. Adopting any
|
||||
new build — release or revision — is a template edit.
|
||||
Nothing in this pipeline can change an existing version tag. Whether a new build reaches
|
||||
your deployment is decided at the Unraid update button — which is why auto-update stays
|
||||
off.
|
||||
|
||||
## Validating a third-party image
|
||||
|
||||
@@ -368,7 +381,7 @@ the official release, upstream safety check intact, no tampering.
|
||||
|---|---|
|
||||
| `master` | Exact mirror of upstream `master`. Never edited here. |
|
||||
| `main` | This branch — the build. |
|
||||
| `Dockerfile` | The image. `ARG DSH_VERSION` is the pinned default. |
|
||||
| `Dockerfile` | The image. `ARG DSH_VERSION` is the default for local builds; CI passes every version explicitly. |
|
||||
| `docker-entrypoint.sh` | Ownership repair + privilege drop, then `exec dsh "$@"`. |
|
||||
| `run-as-runtime-user.sh` | Installed over `dsh`, `pnpm`, `pnpx`, `pn`, `pnx`; re-execs root invocations as the runtime user. |
|
||||
| `web.cordis.patch.yml` | The `0.0.0.0` bind patch, applied via `--patch`. |
|
||||
|
||||
Reference in new issue
Block a user