Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 8fff4b4012 fix(ci-claude-refresh): bump divergent pins instead of aborting
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
decide required every consumer to pin the same ci-claude tag and aborted
otherwise (pipeline 10497: [2.1.250 2.1.278]), which needed a hand fix
before the latest version could roll out. Record all distinct tags, bump
whenever any is off latest, and rewrite every version-pinned tag to the
new one with a validated x.y.z version.

Merging this is itself a push touching the pipeline file, so the refresh
runs, builds the latest image, and opens the bump PR.

Refs #889

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:06:56 +02:00
k.renner 0ada3df260 Merge pull request 'fix(ci-claude-refresh): strip CR/LF from the bump token before use' (#901) from fix/ci-claude-bump-token-newline into master
ci/woodpecker/push/ci-claude-refresh Pipeline failed
Reviewed-on: k.renner/homelab#901
2026-09-23 05:03:19 +00:00
claude-botandClaude Opus 5.5 2144554cfc fix(ci-claude-refresh): strip CR/LF from the bump token before use
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Pipeline 10186 failed in bump-pr with 'url contains a newline in its
username component': ci_claude_bump_bot_token carries a trailing newline,
so the push URL (and the PR API auth header) broke. Strip CR/LF once at
the top of the step and fail fast if the token ends up empty. This makes
the step independent of how the secret value was written.

Closes #889

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 06:59:15 +02:00
k.renner dba810b87b Merge pull request 'fix(paperless): raise the memory limit to 4Gi' (#900) from worktree-fix-paperless-memory-limit into master
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/wiki-maintenance Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#900
2026-09-21 19:29:08 +00:00
claude-botandClaude Fable 5.1 f8aa65362b fix(paperless): cap OCR at 5 threads per worker
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Unset, paperless derives cpu_count / TASK_WORKERS, i.e. 20 parallel
ocrmypdf workers on k8s-1. Memory during OCR scales with that count, so
the limit alone is not enough headroom for large scans.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 21:26:35 +02:00
claude-botandClaude Fable 5.1 0ce0391bb1 fix(paperless): raise the memory limit to 4Gi
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
An 18 MB scan from the ADS-1700W has kept the paperless container in an
OOM crash loop since 2026-09-21 10:21: ocrmypdf runs with 20 workers
(paperless default, cpu_count / TASK_WORKERS on a 20-core node) and blows
the 2Gi limit ~40s into OCR. The consume task is re-queued on every
restart, so the same file kills every attempt. 117 restarts so far, web
UI down as a side effect since worker and webserver share the container.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 21:24:36 +02:00
security-bot 4611ea3578 chore(image-automation): bump redis:8.10.2-alpine
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-09-21 18:52:58 +00:00
k.renner 63701fbebb Merge pull request 'ci(wiki-maintenance): verify the PR merged instead of trusting the merge status' (#895) from fix/wiki-maintenance-merge-verify into master
ci/woodpecker/manual/wiki-maintenance Pipeline was successful
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#895
2026-09-21 08:49:10 +00:00
k.renner e8a047eb14 Merge pull request 'ci(pick-model): newest among comparable candidates, policy in code' (#894) from feat/pick-model-newest-among-comparable into master
Reviewed-on: k.renner/homelab#894
2026-09-21 08:48:58 +00:00
claude-botandClaude Fable 5.1 0279210595 ci(wiki-maintenance): verify the PR merged instead of trusting the merge status
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Run 10254: POST .../merge with delete_branch_after_merge returned
HTTP 500 although PR #2 on k.renner/notes was merged, so the step
failed and left the branch behind. The merge call no longer asks Gitea
to delete the branch; the step reads the PR back and requires
merged=true, then deletes the branch as a separate best-effort call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 10:06:23 +02:00
claude-botandClaude Fable 5.1 25f09e9b52 ci(pick-model): newest among comparable candidates, policy in code
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
First live pick after #893 chose claude-sonnet-4-5 at 0.23 over
claude-sonnet-4-6: same-family models split the probability for a
mid-weight task, and "prefer newer" is a policy, not a judgment. New
scripts/ci/choose-model.sh applies it to the distribution: among
candidates within CHOOSE_MODEL_MARGIN (0.7) of the best probability the
most recently released wins; a clear best stays; NONE yields nothing.
The picker logs the top probabilities. Four policy tests added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 09:59:40 +02:00
k.renner 82f2825d3f Merge pull request 'ci(pick-model): choose the model with one TypeSafe Choice, claude as fallback' (#893) from feat/pick-model-typesafe into master
ci/woodpecker/manual/wiki-maintenance Pipeline failed
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#893
2026-09-21 07:56:11 +00:00
7 changed files with 89 additions and 28 deletions
+17 -21
View File
@@ -14,8 +14,8 @@
# and ci-claude:latest. The Containerfile makes this layer
# byte-reproducible, so rebuilding an unchanged version dedups
# in the registry instead of orphaning a fresh ~260 MB copy.
# 3. bump-pr — (only when the version actually changed) rewrite the 5
# consumer pins to ci-claude:<version> and open a PR. A human
# 3. bump-pr — (only when any pin is off the latest version) rewrite every
# consumer pin to ci-claude:<version> and open a PR. A human
# reviews and merges it; nothing auto-merges.
#
# TAG SCHEME — deliberately version tags (ci-claude:<claude-version>), NOT the dated
@@ -107,31 +107,22 @@ steps:
[0-9]*.[0-9]*.[0-9]*) : ;;
*) echo "npm returned an implausible version '$$NEW' — aborting."; exit 1 ;;
esac
# The tag every consumer currently references. They are kept identical,
# so this must resolve to EXACTLY ONE non-empty tag; anything else
# (no match, or a partial/disagreeing set) aborts rather than risk a
# corrupting sed later. Counting with `grep -c .` (non-empty lines)
# instead of `wc -l` is deliberate: an empty result yields 0 here but
# 1 under wc -l, which would slip past the guard.
CUR=$$(grep -hoE 'ci-claude:[0-9A-Za-z._-]+' $$CONSUMERS | sed 's/^ci-claude://' | sort -u)
NTAGS=$$(printf '%s' "$$CUR" | grep -c . || true)
if [ -z "$$CUR" ] || [ "$$NTAGS" -ne 1 ]; then
echo "Expected exactly one ci-claude tag across the consumers, got [$$CUR]."
echo "Refusing to guess — fix the pins by hand."; exit 1
fi
echo "latest=$$NEW current=$$CUR event=$$CI_PIPELINE_EVENT"
# Distinct tags the consumers currently reference. Divergent pins are
# normal (a pipeline bumped on its own); the bump rewrites all of them.
CUR=$$(grep -hoE 'ci-claude:[0-9][0-9A-Za-z._-]*' $$CONSUMERS | sed 's/^ci-claude://' | sort -u | tr '\n' ' ' | sed 's/ $$//')
[ -n "$$CUR" ] || { echo "No ci-claude pin found in the consumers — aborting."; exit 1; }
echo "latest=$$NEW current=[$$CUR] event=$$CI_PIPELINE_EVENT"
printf '%s' "$$NEW" > "$$CI_WORKSPACE/.ci-refresh/version"
printf '%s' "$$CUR" > "$$CI_WORKSPACE/.ci-refresh/current"
# Build when the version changed, or on a push (Containerfile edit must
# ship even if the version is unchanged). Bump the pins only when the
# version the consumers reference is not the latest.
# Build when any pin is off latest, or on a push (Containerfile edit must
# ship even if the version is unchanged). Bump when any pin is off latest.
if [ "$$CUR" != "$$NEW" ] || [ "$$CI_PIPELINE_EVENT" = "push" ]; then
touch "$$CI_WORKSPACE/.ci-refresh/do-build"; echo "will build $$NEW"
else
echo "already on $$NEW — nothing to build"
fi
if [ "$$CUR" != "$$NEW" ]; then
touch "$$CI_WORKSPACE/.ci-refresh/do-bump"; echo "will open bump PR $$CUR -> $$NEW"
touch "$$CI_WORKSPACE/.ci-refresh/do-bump"; echo "will open bump PR [$$CUR] -> $$NEW"
else
echo "consumers already pin $$NEW — no PR"
fi
@@ -175,6 +166,9 @@ steps:
- |
[ -f "$$CI_WORKSPACE/.ci-refresh/do-bump" ] || { echo "no version change — no PR"; exit 0; }
set -eu
# Secret values can carry a trailing newline; it breaks the push URL and the auth header.
PUSH_TOKEN=$$(printf '%s' "$$PUSH_TOKEN" | tr -d '\r\n')
[ -n "$$PUSH_TOKEN" ] || { echo "PUSH_TOKEN is empty — aborting"; exit 1; }
NEW=$$(cat "$$CI_WORKSPACE/.ci-refresh/version")
CUR=$$(cat "$$CI_WORKSPACE/.ci-refresh/current")
# Both were validated non-empty by the decide step; re-assert before a
@@ -190,9 +184,11 @@ steps:
# is not a maintained list.
CONSUMERS=$$(grep -rlE 'ci-claude:[0-9]' .woodpecker)
[ -n "$$CONSUMERS" ] || { echo "No ci-claude consumers found — refusing to continue."; exit 1; }
# Exact-string swap of the current tag for the new one, consumers only.
# NEW goes into a sed replacement: allow only a plain x.y.z version.
printf '%s' "$$NEW" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$' || { echo "refusing non-x.y.z version '$$NEW'"; exit 1; }
# Every version-pinned tag becomes NEW, whatever it was; consumers only.
for f in $$CONSUMERS; do
sed -i "s#ci-claude:$$CUR#ci-claude:$$NEW#g" "$$f"
sed -i "s#ci-claude:[0-9][0-9A-Za-z._-]*#ci-claude:$$NEW#g" "$$f"
done
# Author is claude-bot (the PAT's account); the commit subject makes
# the automated-bump nature clear.
+10 -2
View File
@@ -125,8 +125,16 @@ steps:
echo "POST pull -> HTTP $$code"
[ "$$code" -ge 200 ] && [ "$$code" -lt 300 ] || { cat /tmp/pr-resp.json; exit 1; }
NUM=$$(jq -r '.number' /tmp/pr-resp.json)
# Merge without delete_branch_after_merge: run 10254 got HTTP 500 from that
# call although the merge itself had landed. Verify the PR state instead of
# trusting the status code, then drop the branch as a separate, best-effort step.
code=$$(curl -s -o /tmp/merge-resp.json -w '%{http_code}' -X POST \
-H "Authorization: token $$TOKEN" -H "Content-Type: application/json" \
--data '{"Do":"merge","delete_branch_after_merge":true}' "$$API/pulls/$$NUM/merge")
--data '{"Do":"merge"}' "$$API/pulls/$$NUM/merge")
echo "POST merge #$$NUM -> HTTP $$code"
[ "$$code" -ge 200 ] && [ "$$code" -lt 300 ] || { cat /tmp/merge-resp.json; echo "PR #$$NUM left open"; exit 1; }
MERGED=$$(curl -s -H "Authorization: token $$TOKEN" "$$API/pulls/$$NUM" | jq -r '.merged')
echo "PR #$$NUM merged: $$MERGED"
[ "$$MERGED" = "true" ] || { cat /tmp/merge-resp.json; echo "PR #$$NUM left open"; exit 1; }
code=$$(curl -s -o /tmp/del-resp.json -w '%{http_code}' -X DELETE -H "Authorization: token $$TOKEN" \
"$$API/branches/$$(printf '%s' "$$BRANCH" | sed 's|/|%2F|g')")
echo "DELETE branch $$BRANCH -> HTTP $$code (best effort)"
+3 -1
View File
@@ -27,6 +27,8 @@ data:
PAPERLESS_ALLOWED_HOSTS: "*"
PAPERLESS_TIME_ZONE: "Europe/Berlin"
PAPERLESS_OCR_LANGUAGE: "deu+eng"
# Default is cpu_count / TASK_WORKERS, 20 on k8s-1; OCR memory scales with it.
PAPERLESS_THREADS_PER_WORKER: "5"
PAPERLESS_CONSUMER_RECURSIVE: "true"
PAPERLESS_CONSUMER_DELETE_DUPLICATES: "true"
PAPERLESS_CONSUMER_ENABLE_BARCODES: "true"
@@ -273,7 +275,7 @@ spec:
cpu: 200m
memory: 512Mi
limits:
memory: 2Gi
memory: 4Gi
securityContext:
runAsNonRoot: false
allowPrivilegeEscalation: true
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
type: RuntimeDefault
containers:
- name: redis
image: redis:8.10.1-alpine # {"$imagepolicy": "flux-system:redis-paperless"}
image: redis:8.10.2-alpine # {"$imagepolicy": "flux-system:redis-paperless"}
args:
- redis-server
- --save
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
# Turn the TypeSafe picker's probability distribution into one model id.
#
# The judgment (how well each catalog model fits the task) comes from the
# model; the policy lives here, in code: among the candidates whose probability
# is within MARGIN of the best one, take the most recently released. Several
# same-family models (Sonnet 4.5 / 4.6 / 5) split the probability for a
# mid-weight task, and the newest of a comparable set is the one to run.
#
# Usage: choose-model.sh <catalog-table-file> < <systemone-response.json>
# The table is fetch-models.sh --table output: id \t name \t released=YYYY-MM-DD ...
# Prints the chosen id, or nothing when the answer is NONE / unusable.
# Env: CHOOSE_MODEL_MARGIN (default 0.7): candidate if p >= MARGIN * p_best.
set -eu
TABLE_FILE="${1:?usage: choose-model.sh <catalog-table-file> < response.json}"
MARGIN="${CHOOSE_MODEL_MARGIN:-0.7}"
jq -r --rawfile table "$TABLE_FILE" --argjson margin "$MARGIN" '
($table | split("\n") | map(select(length > 0) | split("\t")
| {key: .[0], value: ((.[2:] | map(select(startswith("released="))) | .[0] // "released=") | ltrimstr("released="))})
| from_entries) as $released
| (.answers.model.probabilities // {}) | to_entries
| map(select(.key != "NONE")) | sort_by(-.value)
| if length == 0 then empty
else (.[0].value) as $best
| map(select(.value >= $margin * $best))
| sort_by([($released[.key] // ""), .value])
| .[-1].key
end' 2>&1
+10 -3
View File
@@ -51,11 +51,18 @@ if [ -n "${TYPESAFE_API_KEY:-}" ]; then
if [ -n "${PICK_MODEL_TYPESAFE_DRYRUN:-}" ]; then printf '%s\n' "$REQ"; exit 0; fi
RESP="$(printf '%s' "$REQ" | curl -sS --max-time 30 -X POST https://api.typesafe.ai/v1/systemone \
-H "Authorization: Bearer $TYPESAFE_API_KEY" -H 'Content-Type: application/json' --data-binary @- || true)"
CHOICE="$(printf '%s' "$RESP" | jq -r '.answers.model.choice // empty' 2>&1 | head -1)"
CONF="$(printf '%s' "$RESP" | jq -r '.answers.model.confidence // empty' 2>&1 | head -1)"
# Policy in code (choose-model.sh): among candidates within a margin of the
# best probability, the most recently released wins. Same-family models split
# the probability for mid-weight tasks (live catalog: Sonnet 4.5 chosen at
# 0.23 over Sonnet 4.6).
TABLE_FILE="$(mktemp)"
printf '%s\n' "$TABLE" > "$TABLE_FILE"
CHOICE="$(printf '%s' "$RESP" | sh "$HERE/choose-model.sh" "$TABLE_FILE" | head -1)"
rm -f "$TABLE_FILE"
TOP="$(printf '%s' "$RESP" | jq -r '(.answers.model.probabilities // {}) | to_entries | sort_by(-.value) | .[0:4] | map("\(.key)=\(.value)") | join(" ")' 2>&1 | head -1)"
VALID="$(printf '%s\n' "$TABLE" | sh "$HERE/valid-model.sh" "$CHOICE")"
if [ -n "$VALID" ]; then
log "typesafe chose '$VALID' (confidence ${CONF:-?}) for: $GUIDANCE"
log "typesafe chose '$VALID' (top: ${TOP:-?}) for: $GUIDANCE"
printf '%s\n' "$VALID"
exit 0
fi
+18
View File
@@ -97,9 +97,27 @@ else
printf 'FAIL typesafe request: keys [%s] task [%s]\n' "$keys" "$task"; fails=$((fails + 1))
fi
# choose-model.sh: newest among comparable candidates, best otherwise, NONE -> nothing.
TB="$(mktemp)"
printf '%s\n' 'claude-sonnet-4-5 Claude Sonnet 4.5 released=2025-09-29 context=200000' \
'claude-sonnet-4-6 Claude Sonnet 4.6 released=2026-02-17 context=200000' \
'claude-opus-5 Claude Opus 5 released=2026-07-24 context=1000000' \
'claude-haiku-4-5 Claude Haiku 4.5 released=2025-10-15 context=200000' > "$TB"
choose() { printf '%s' "$1" | sh "$HERE/choose-model.sh" "$TB" || true; }
got="$(choose '{"answers":{"model":{"probabilities":{"claude-sonnet-4-5":0.30,"claude-sonnet-4-6":0.25,"claude-opus-5":0.10,"NONE":0.05}}}}')"
if [ "$got" = "claude-sonnet-4-6" ]; then printf 'ok newer sibling within margin wins\n'; else printf 'FAIL comparable: got [%s]\n' "$got"; fails=$((fails + 1)); fi
got="$(choose '{"answers":{"model":{"probabilities":{"claude-opus-5":0.90,"claude-haiku-4-5":0.05,"claude-sonnet-4-6":0.04,"NONE":0.01}}}}')"
if [ "$got" = "claude-opus-5" ]; then printf 'ok clear best is kept\n'; else printf 'FAIL clear best: got [%s]\n' "$got"; fails=$((fails + 1)); fi
got="$(choose '{"answers":{"model":{"probabilities":{"claude-haiku-4-5":0.60,"claude-sonnet-4-5":0.35,"NONE":0.05}}}}')"
if [ "$got" = "claude-haiku-4-5" ]; then printf 'ok older best outside margin is kept\n'; else printf 'FAIL margin: got [%s]\n' "$got"; fails=$((fails + 1)); fi
got="$(choose '{"answers":{"model":{"probabilities":{"NONE":1.0}}}}')"
if [ -z "$got" ]; then printf 'ok NONE yields nothing\n'; else printf 'FAIL NONE: got [%s]\n' "$got"; fails=$((fails + 1)); fi
rm -f "$TB"
if [ "$fails" -eq 0 ]; then
echo "All model-selection tests passed."
else
echo "$fails test(s) FAILED."
exit 1
fi