- assets/: replace the white-background avatar with a transparent 512px PNG (brand blue #4d6bfe, tight-cropped) plus a 128px header variant. Same path, so the Unraid template icon keeps working. - CI: every image tag in the registry now gets a v<tag> git tag on main and a Gitea release carrying digest, dsh/pnpm/base versions, deploy notes and the 0.1.2+ launch-token caveat. Idempotent, so it backfills the three existing tags on the next run. - README rewritten to match OpenPXE/Discord-Authenticator: centered brand header, badges, contents, why/highlights, quick start, Unraid template, image contract, versions & upgrade procedure (0.1.5 session-format V3 has no downgrade), plugins, security model, pipeline, layout. - LICENSE: MIT for the build tooling. Co-Authored-By: Claude Fable 5 <[email protected]>
259 lines
13 KiB
YAML
259 lines
13 KiB
YAML
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:
|
|
# 1. Mirrors upstream master and dsh-v* tags into this repo.
|
|
# 2. Builds a new <version> tag when a new @deepseek-ai/dsh appears on npm
|
|
# and the registry lacks it.
|
|
# 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 since the newest existing build of that version, builds a
|
|
# <version>-r<run> revision tag with the updated toolchain.
|
|
#
|
|
# 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.
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '17 */6 * * *'
|
|
workflow_dispatch: {}
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- Dockerfile
|
|
- web.cordis.patch.yml
|
|
- .gitea/workflows/sync-upstream.yml
|
|
|
|
env:
|
|
UPSTREAM: https://github.com/deepseek-ai/deepseek-harness.git
|
|
REGISTRY: gitea.milesward.dev
|
|
IMAGE: mward4/deepseek-harness
|
|
BASE_IMAGE: node:22-bookworm-slim
|
|
|
|
jobs:
|
|
sync-and-build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout CI branch
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: main
|
|
|
|
- name: Mirror upstream refs into this repository
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -eux
|
|
git clone --bare "$UPSTREAM" /tmp/upstream
|
|
cd /tmp/upstream
|
|
git remote add mirror "https://oauth2:${TOKEN}@${REGISTRY}/${IMAGE}.git"
|
|
git push mirror '+refs/heads/master:refs/heads/master'
|
|
git push mirror '+refs/tags/dsh-v*:refs/tags/dsh-v*'
|
|
|
|
- name: Plan builds (new dsh version + pinned-version freshness)
|
|
id: plan
|
|
env:
|
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
DSH_LATEST=$(npm view @deepseek-ai/dsh@latest version)
|
|
PNPM_LATEST=$(npm view pnpm@latest version)
|
|
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")
|
|
echo "dsh latest=$DSH_LATEST pinned=$PINNED pnpm=$PNPM_LATEST base=$BASE_DIGEST"
|
|
|
|
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}" \
|
|
-H "Accept: $ACCEPT" "https://${REGISTRY}/v2/${IMAGE}/manifests/$1")" = 200 ]
|
|
}
|
|
# Labels of a single-arch manifest's config blob ("pnpm|base"); empty
|
|
# output (index manifests, missing labels) means "treat as stale".
|
|
tag_toolchain() {
|
|
MAN=$(curl -s -u "${{ github.repository_owner }}:${TOKEN}" \
|
|
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \
|
|
"https://${REGISTRY}/v2/${IMAGE}/manifests/$1") || return 0
|
|
CFG=$(printf '%s' "$MAN" | grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 | grep -o 'sha256:[a-f0-9]*') || return 0
|
|
[ -n "$CFG" ] || return 0
|
|
BLOB=$(curl -s -u "${{ github.repository_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)
|
|
echo "${P}|${B}"
|
|
}
|
|
|
|
BUILDS=""
|
|
# 1. New official release the registry lacks -> new version tag.
|
|
if ! tag_exists "$DSH_LATEST"; then
|
|
BUILDS="$DSH_LATEST=$DSH_LATEST"
|
|
fi
|
|
# 2. Pinned version: missing entirely -> build it; otherwise compare
|
|
# the newest existing build's toolchain labels and cut a -rN
|
|
# revision when pnpm or the base image moved.
|
|
if [ "$PINNED" != "$DSH_LATEST" ] && ! tag_exists "$PINNED"; then
|
|
BUILDS="$BUILDS $PINNED=$PINNED"
|
|
elif tag_exists "$PINNED"; 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)
|
|
TOOL=$(tag_toolchain "$NEWEST")
|
|
echo "newest build of pinned: $NEWEST toolchain='$TOOL'"
|
|
if [ "$TOOL" != "${PNPM_LATEST}|${BASE_DIGEST}" ]; then
|
|
echo "toolchain stale (want ${PNPM_LATEST}|${BASE_DIGEST}) -> revision build"
|
|
BUILDS="$BUILDS ${PINNED}-r${{ github.run_number }}=$PINNED"
|
|
fi
|
|
fi
|
|
|
|
echo "planned builds: '${BUILDS}'"
|
|
{
|
|
echo "builds=${BUILDS}"
|
|
echo "pnpm=${PNPM_LATEST}"
|
|
echo "base_digest=${BASE_DIGEST}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build, smoke-test, and push
|
|
if: steps.plan.outputs.builds != ''
|
|
env:
|
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
PNPM_VERSION: ${{ steps.plan.outputs.pnpm }}
|
|
BASE_DIGEST: ${{ steps.plan.outputs.base_digest }}
|
|
run: |
|
|
set -eux
|
|
echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.repository_owner }}" --password-stdin
|
|
for entry in ${{ steps.plan.outputs.builds }}; do
|
|
TAG=${entry%%=*}
|
|
VER=${entry#*=}
|
|
REF="${REGISTRY}/${IMAGE}:${TAG}"
|
|
docker build \
|
|
--build-arg "DSH_VERSION=${VER}" \
|
|
--build-arg "PNPM_VERSION=${PNPM_VERSION}" \
|
|
--build-arg "BASE_DIGEST=${BASE_DIGEST}" \
|
|
-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.
|
|
SMOKE="dsh-smoke-${{ github.run_id }}"
|
|
docker rm -f "$SMOKE" 2>/dev/null || true
|
|
docker run -d --name "$SMOKE" "$REF"
|
|
sleep 3
|
|
[ "$(docker exec "$SMOKE" dsh --version)" = "$VER" ]
|
|
[ "$(docker exec "$SMOKE" pnpm --version)" = "$PNPM_VERSION" ]
|
|
OK=""
|
|
for i in $(seq 1 30); do
|
|
if docker exec "$SMOKE" node -e '
|
|
const os = require("os");
|
|
const nic = Object.values(os.networkInterfaces()).flat()
|
|
.find(i => i && i.family === "IPv4" && !i.internal);
|
|
fetch("http://" + nic.address + ":3080/").then(r => {
|
|
process.exit([200, 303, 401].includes(r.status) ? 0 : 1);
|
|
}).catch(() => process.exit(2));'
|
|
then OK=1; break; fi
|
|
sleep 2
|
|
done
|
|
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"
|
|
exit 1
|
|
fi
|
|
docker rm -f "$SMOKE"
|
|
|
|
docker push "$REF"
|
|
done
|
|
|
|
# Every image tag in the registry gets a matching v<tag> git tag + Gitea
|
|
# release on main (the Dockerfile state that built it). Idempotent: only
|
|
# tags without a release are published, so this also backfills.
|
|
- name: Publish releases for image tags without one
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REG_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
REPO: ${{ github.repository }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
run: |
|
|
set -eu
|
|
API="https://${REGISTRY}/api/v1"
|
|
MAIN_SHA=$(git rev-parse HEAD)
|
|
json_escape() { sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | awk '{printf "%s\\n", $0}'; }
|
|
|
|
TAGS=$(curl -s -H "Authorization: token ${REG_TOKEN}" \
|
|
"$API/packages/${OWNER}?type=container&q=deepseek-harness&limit=100" \
|
|
| tr ',' '\n' | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | grep -v '^sha256:' | sort -V)
|
|
|
|
for TAG in $TAGS; do
|
|
REL="v${TAG}"
|
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token ${TOKEN}" "$API/repos/${REPO}/releases/tags/${REL}")
|
|
[ "$CODE" = 404 ] || { echo "$REL: release exists"; continue; }
|
|
echo "$REL: publishing"
|
|
|
|
DIGEST=$(curl -sI -u "${OWNER}:${REG_TOKEN}" \
|
|
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \
|
|
"https://${REGISTRY}/v2/${IMAGE}/manifests/${TAG}" | grep -i '^docker-content-digest' | awk '{print $2}' | tr -d '\r')
|
|
MAN=$(curl -s -u "${OWNER}:${REG_TOKEN}" \
|
|
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json' \
|
|
"https://${REGISTRY}/v2/${IMAGE}/manifests/${TAG}")
|
|
CFG=$(printf '%s' "$MAN" | grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 | grep -o 'sha256:[a-f0-9]*' || true)
|
|
BLOB=$([ -n "$CFG" ] && curl -s -u "${OWNER}:${REG_TOKEN}" "https://${REGISTRY}/v2/${IMAGE}/blobs/$CFG" || echo '{}')
|
|
PNPMV=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.pnpm-version":"[^"]*"' | cut -d'"' -f4 || true)
|
|
BASEV=$(printf '%s' "$BLOB" | grep -o '"dev.milesward.base-digest":"[^"]*"' | cut -d'"' -f4 || true)
|
|
VER=$(printf '%s' "$TAG" | sed 's/-r[0-9]*$//')
|
|
PRE=false; case "$VER" in *alpha*) PRE=true;; esac
|
|
case "$TAG" in
|
|
*-r[0-9]*) KIND="Toolchain refresh of dsh ${VER}: same upstream release, rebuilt with the current pnpm and a freshly patched \`node:22-bookworm-slim\` base."
|
|
TITLE="${REL} — dsh ${VER} toolchain refresh (pnpm ${PNPMV:-?})";;
|
|
*) KIND="Container build of **DeepSeek Harness (dsh) ${VER}** from the official npm release, built and smoke-tested by CI."
|
|
TITLE="${REL} — dsh ${VER} · pnpm ${PNPMV:-?} · node 22";;
|
|
esac
|
|
case "$VER" in
|
|
0.1.0*|0.1.1*) TOKEN_NOTE="";;
|
|
*) TOKEN_NOTE="> **Launch-token auth (dsh 0.1.2+):** the Web UI requires a one-time token printed in \`docker logs\`; it rotates on every restart and is exchanged for a ~30-day cookie bound to the origin you open it on. \`@goodandready/dsh-lanmode\` ≤0.6.8 is incompatible with this line.";;
|
|
esac
|
|
|
|
BODY=$(cat <<EOF
|
|
${KIND}
|
|
|
|
\`\`\`
|
|
docker pull gitea.milesward.dev/mward4/deepseek-harness:${TAG}
|
|
\`\`\`
|
|
|
|
### What's inside
|
|
|
|
| | |
|
|
|---|---|
|
|
| dsh | \`${VER}\` — [upstream release notes](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v${VER}) |
|
|
| pnpm | \`${PNPMV:-unknown}\` |
|
|
| base image | \`${BASEV:-unknown}\` |
|
|
| image digest | \`${DIGEST}\` |
|
|
| built from | \`main@${MAIN_SHA}\` (published by CI run #${RUN_NUMBER}) |
|
|
|
|
### Deploy on Unraid
|
|
|
|
- **Repository:** \`gitea.milesward.dev/mward4/deepseek-harness:${TAG}\`
|
|
- **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\`
|
|
|
|
${TOKEN_NOTE}
|
|
|
|
Image tags are immutable — this release will never be rebuilt or moved. See the [README](https://gitea.milesward.dev/mward4/deepseek-harness#versions-and-upgrades) for the upgrade procedure.
|
|
EOF
|
|
)
|
|
BODY_JSON=$(printf '%s' "$BODY" | json_escape)
|
|
|
|
# Tag main at the commit that built the image (ignore "already exists").
|
|
curl -s -o /dev/null -w "tag ${REL}: HTTP %{http_code}\n" -X POST \
|
|
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
|
"$API/repos/${REPO}/tags" \
|
|
-d "{\"tag_name\":\"${REL}\",\"target\":\"${MAIN_SHA}\",\"message\":\"deepseek-harness image ${TAG}\"}"
|
|
curl -s -o /tmp/rel.json -w "release ${REL}: HTTP %{http_code}\n" -X POST \
|
|
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
|
"$API/repos/${REPO}/releases" \
|
|
-d "{\"tag_name\":\"${REL}\",\"target_commitish\":\"${MAIN_SHA}\",\"name\":\"${TITLE}\",\"body\":\"${BODY_JSON}\",\"draft\":false,\"prerelease\":${PRE}}"
|
|
grep -q '"id"' /tmp/rel.json || { echo "::error::release creation failed for ${REL}"; cat /tmp/rel.json; exit 1; }
|
|
done
|