Files
deepseek-harness/.gitea/workflows/sync-upstream.yml
T
Miles WardandClaude Fable 5 7a5e8a4ca0
Sync upstream and build images / sync-and-build (push) Successful in 25s
Releases: mark the pinned deployment version automatically
The generator now receives the Dockerfile's default DSH_VERSION and calls out
that release as the pinned deployment version, so the Releases page says
which tag the Unraid template runs without hand-editing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-10 17:18:50 -04:00

210 lines
9.8 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 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 since the newest existing build of that version, builds a
# <version>-r<run> revision tag with the updated toolchain.
# 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.
on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
inputs:
version:
description: Build this exact dsh version too (e.g. 0.1.5-alpha.2); latest/next are always considered
required: false
default: ''
push:
branches: [main]
paths:
- Dockerfile
- web.cordis.patch.yml
- .gitea/workflows/sync-upstream.yml
- .gitea/scripts/publish-releases.js
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
# Branch only: upstream's dsh-v* tags are deliberately NOT mirrored, so
# this repo's tag list is exclusively its own v<image tag> releases.
- name: Mirror upstream master into this repository
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eux
git clone --bare --single-branch --branch master "$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'
- name: Plan builds (new dsh versions + pinned-version freshness)
id: plan
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REQUESTED: ${{ inputs.version }}
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")
echo "dsh latest=$DSH_LATEST next=$DSH_NEXT requested='$REQUESTED' 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. 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).
for V in $(printf '%s\n%s\n%s\n' "$DSH_LATEST" "$DSH_NEXT" "$REQUESTED" | grep -v '^$' | sort -u); do
if ! tag_exists "$V"; then
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)
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 }}
CI_RUN: ${{ github.run_number }}
run: |
set -eux
echo "$TOKEN" | docker login "$REGISTRY" -u "${{ github.repository_owner }}" --password-stdin
GIT_SHA=$(git rev-parse HEAD)
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
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}" \
--build-arg "GIT_SHA=${GIT_SHA}" \
--build-arg "CI_RUN=${CI_RUN}" \
--build-arg "BUILD_DATE=${BUILD_DATE}" \
-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 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.
- name: Publish releases for image tags without one
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REG_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.repository }}
RUN_NUMBER: ${{ github.run_number }}
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}" \
"$BASE_IMAGE" node - < .gitea/scripts/publish-releases.js