Skip to content

Merging 0.5: introducing major bug fixes, security and stability improvements - #28

Merged
LoredCast merged 26 commits into
mainfrom
claude/server-frontend-optimization-qnpwnd
Sep 27, 2026
Merged

LoredCast merged 26 commits into
mainfrom
claude/server-frontend-optimization-qnpwnd

Conversation

@LoredCast

Copy link
Copy Markdown
Owner

Updating to 0.5.0 fixes major bugs and issues, 0.5.1 brings additional performance and stability.

claude and others added 26 commits September 25, 2026 12:19
Starlette 1.x removed the TemplateResponse(name, {"request": ...}) form.
Because requirements are unpinned, fresh image builds pull Starlette 1.x
and every page render fails with "TypeError: unhashable type: 'dict'".
The request-first signature works on all Starlette versions >= 0.29.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- CORS allowed every origin with credentials (ENV defaults to "dev"), so any
  website a user visited could call the API, including /settings/save.
  CORS is now off unless ALLOWED_ORIGINS lists explicit origins.
- New SecurityMiddleware rejects state-changing requests whose Origin/Referer
  host doesn't match the app (Host, X-Forwarded-Host, app_public_url or
  CSRF_TRUSTED_ORIGINS). Clients that send neither header (curl, n8n) are
  unaffected.
- Security headers: nosniff, Referrer-Policy, frame protection (configurable
  via FRAME_ANCESTORS for dashboard embedding) and a strict CSP.
- Optional ALLOWED_HOSTS enables TrustedHostMiddleware (DNS rebinding).
- Stored XSS: file names (including names of entries inside uploaded ZIPs)
  and tool error output were inserted into innerHTML unescaped. All dynamic
  values in the job table now go through escapeHtml(), and the duplicated
  details markup is built in one place.
- choices.js 11.2.4 and Font Awesome 4.7.0 are vendored instead of loaded
  unpinned from CDNs, and the inline APP_CONFIG script became a JSON data
  block, so the CSP can restrict scripts to 'self' (and the UI works offline).
- Locales that Intl rejects (e.g. "en-US@posix") no longer break the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- GET /settings had no auth check: in OIDC mode anyone could load it and read
  the OIDC client secret and webhook bearer token from the form. The page now
  requires login, shows the configuration only to admins, and never renders
  secrets (an empty secret field keeps the stored value).
- /settings/save only accepts keys the settings form has; everything else
  (TTS command templates, model paths, ...) must be edited in settings.yml.
  Changing or adding conversion command templates from the browser, which
  chooses the programs the server runs, requires ALLOW_COMMAND_EDITS=true;
  the form shows them read-only otherwise. Numeric fields are validated
  (a blank field used to be saved as null and break config loading).
- New auth_settings.allowed_users / allowed_domains restrict who may log in
  (checked at login, per request and for API bearer tokens). Admin and
  allowlist matching ignore emails the IdP marks as unverified.
- The session key was generated per process when SECRET_KEY was unset, so
  with 4 gunicorn workers OAuth state checks failed randomly. A generated
  key is now persisted to config/.secret_key (0600) and shared.
  SESSION_COOKIE_SECURE=true sets the Secure flag on the session cookie.
- Logout sends id_token_hint and client_id and URL-encodes the redirect,
  fixing RP-initiated logout with Keycloak (#20).
- Workers pick up settings.yml changes on their next request instead of
  serving stale config until restart.
- The index page only receives tool names and formats, not command templates.
- LOCAL_ONLY API calls use the same identity as the UI, and /download,
  /job/{id} and /api/v1/jobs/status accept bearer tokens, so webhook
  download_url links and status polling work for API clients.
- Removed the /test-websocket-notification debug endpoint; forward-auth
  returns 404 in LOCAL_ONLY mode instead of crashing.
- Removed the second deep_merge definition, which shadowed the first and
  crashed when a merged value was None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- Chunked uploads (the path the UI uses) ignored max_file_size_mb entirely.
  The limit now applies across all chunks of an upload and when stitching.
- upload_id is validated strictly and chunk directories are namespaced per
  user. An id such as ".." used to sanitize to "" and resolve to the shared
  chunk root, and finalizing it deleted everyone's in-progress uploads.
- Finalize validates task type, output format, Whisper model and TTS voice
  before stitching, so rejected requests leave no files behind.
- Whisper models are restricted to allowed_models on every path (UI,
  batches, API, and inside the task). faster-whisper also accepts local
  paths and arbitrary Hugging Face repo ids.
- TTS voice names are validated, so they can't be passed to the Piper
  download CLI as options or paths.
- ZIP batches: limits on member count (max_zip_members, default 500) and
  total uncompressed size (max_zip_uncompressed_mb); symlink entries and
  __MACOSX/.DS_Store junk are skipped.
- File names keep their extension when the stem has no ASCII characters
  ("文件.pdf" used to become "pdf" and fail with a 500).
- ZIP parent jobs could end up stuck on "processing" when children
  finished before dispatching was done; the parent now stays pending until
  all children exist.
- /api/v1/process validates before saving (no orphaned uploads), writes
  image-OCR results as .pdf, and callback_url from chunked API uploads is
  now stored so the webhook fires.
- /convert-file and /api/v1/process rejected every tool whose id contains
  '_' (ghostscript_pdf, ffmpeg_audio, ...); they now match known tool ids.
- A periodic task removes abandoned chunk uploads and extraction
  directories of finished batches (STALE_UPLOAD_HOURS, default 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
Process handling
- Tool output went to pipes that were only read after exit, so any tool
  printing more than ~64 KB (ffmpeg progress, calibre, docling) blocked
  until the timeout and the job failed as "timed out" (likely #21). Output
  now goes to temp files.
- Cancelling a running conversion had no effect: the poll loop re-read the
  job inside one SQLite read transaction and never saw the cancellation,
  so the tool ran until its timeout. Status is now read from a fresh
  transaction, and a cancelled job is no longer overwritten as "failed".
- Tools run in their own process group; cancel and timeout kill the whole
  tree (e.g. soffice.bin behind the soffice wrapper) and reap it.
- The preexec hook logged from the forked child of a multi-threaded
  process, which can deadlock; limits are now computed in the parent and
  clamped to the hard limits. CHILD_CPU_LIMIT_SECONDS and
  CHILD_MEMORY_LIMIT_MB (0 disables) make them configurable.
- TeX shell escape (\write18) is disabled for all child processes.

Conversions
- LibreOffice/docling write "<stem>.<ext>" into {output_dir}, which was the
  uploads directory: every conversion failed after a 30 s wait and left the
  result behind in uploads (#16). Such tools now get a private output
  directory whose result is moved into place, and LibreOffice gets a
  per-job profile so concurrent jobs don't silently hand off to another
  instance.
- Ghostscript PDF presets (screen/ebook/printer/prepress/PDF-A) were
  silently dropped; they now map to -dPDFSETTINGS / -dPDFA.
- pandoc txt/md/tex outputs failed (pandoc 3 has no such writer names);
  they now use plain/markdown/latex.
- SoX "44k" presets produced 44000 Hz instead of 44100 Hz.
- Tools whose program is not installed are hidden from the UI, reported
  on the settings page and rejected with a clear error.

Housekeeping
- The huey consumer now loads settings and runs the Whisper cache eviction
  thread itself (models are loaded there, so they were never unloaded).
- Kokoro model downloads: one worker downloads, via .part + rename (all
  four gunicorn workers used to write the same files concurrently).
- "Delete Processed Files" only covered the newest 100 jobs, and "Clear
  History" orphaned every file; both now cover all of the user's jobs and
  clearing history removes the files too.
- Batch ZIP downloads are built in a worker thread in a spooled temp file
  instead of in memory on the event loop, with unique entry names and a
  proper Content-Disposition.
- Job status notifications are only queued when WebSockets are enabled
  (the table grew on every progress update otherwise).
- /api/formats/count reflects the active config and installed tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- Dropping a single file opened the Convert dialog with an empty format
  list (the list was filtered before the dialog's Choices widget was
  recreated), and later single-file drops showed the unfiltered list. The
  widget is now created first, and filtering only touches the widget it
  was asked to update instead of also replacing the main form's list.
- A single .zip is a batch whose files can use any format, so it now gets
  the full list instead of only the academic Pandoc formats.
- Selecting several files resets the main list to all formats.
- run.sh no longer aborts when .env does not exist (#13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
OCR language (#26)
- New default language setting (ocr_settings.ocrmypdf.language, e.g.
  "eng" or "eng+spa") and a per-job language picker next to "Start OCR",
  filled from the Tesseract languages installed on the server
  (GET /api/v1/ocr-languages). Both PDF (ocrmypdf) and image OCR use it;
  /api/v1/process accepts ocr_language. Unknown or malformed languages are
  rejected. Spanish text such as "niño" was read as "nifto" with the fixed
  English model.

History management (#25, #5)
- Every finished job (including failed and cancelled ones) can be selected;
  "Delete Selected" removes the jobs, their files and, for ZIP batches, all
  sub-jobs (POST /jobs/delete). Running jobs are cancelled first so their
  tools are killed. "Download Selected" only takes jobs with a result.
- Optional retention: app_settings.retention_days deletes finished jobs and
  their files after N days (0 = keep everything, the default).
- Conversion tool timeouts can be edited on the settings page (#21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
jobs.db, huey.db and app.log were written next to main.py. In the Docker
image that is inside the container, so the job history was lost whenever
the container was recreated, and the app directory must stay writable.
DATA_DIR (default: the app directory, so manual installs are unchanged)
moves them to a data directory; the Docker image points it at a volume.
File logging is skipped with a warning if the log can't be opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- Converter memory limit uses RLIMIT_DATA instead of RLIMIT_AS. Calibre's
  PDF output renders with Chromium (QtWebEngine), which reserves far more
  address space than it uses and failed under the 4 GB address-space
  limit; RLIMIT_DATA still stops processes that actually allocate too much.
- run.sh starts the worker with `python -m huey.bin.huey_consumer`: current
  huey releases install `huey_consumer`, not `huey_consumer.py`.
- /health reports the image version and variant.
- With ALLOWED_HOSTS set, loopback host names stay allowed so container
  health checks keep working (DNS rebinding arrives with its own host name).
- New conversion tool "rsvg" (librsvg's rsvg-convert: SVG to PNG/PDF/PS/EPS),
  since resvg is not packaged for Ubuntu 24.04.
- Requirements: removed packages the app never imports (opencv, bleach,
  mechanize, html5_parser, css_parser, the "dotenv" duplicate of
  python-dotenv; the last three are calibre's own dependencies, which apt
  installs) and marker-pdf from the CUDA set. The CUDA set is now the full
  set plus the CUDA 12 cuBLAS runtime used by CTranslate2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
The previous Dockerfile could no longer build as written (the CUDA stage
installs python3.12-distutils/-pip, which don't exist for Python 3.12, and
copies site-packages from the wrong path), used unpinned packages and ran
everything as root.

- One Dockerfile for all variants, selected with VARIANT=full|small|cuda,
  on Ubuntu 24.04 (Python 3.12 from the distribution, same base for CPU
  and CUDA images). full and small build for linux/amd64 and linux/arm64
  (#14, #18); cuda for amd64.
- Reproducible: base images pinned by digest, apt packages from a dated
  snapshot.ubuntu.com snapshot (amd64; there are no public snapshots of the
  arm64 ports archive), Python packages from hash-pinned lock files
  (locks/, generated by scripts/lock-requirements.sh), SOURCE_DATE_EPOCH
  for timestamps created during the build. Two --no-cache builds of the
  small image with rewrite-timestamp=true produced byte-identical images.
- Wheels only (no compilers in the image); PyTorch for Docling is the CPU
  build from the PyTorch index instead of PyPI's CUDA build (~3 GB less).
- The cuda image is the full image plus the CUDA 12 cuBLAS runtime that
  CTranslate2 loads (from the nvidia-cublas-cu12 wheel), and transcribes on
  the GPU when one is passed in (TRANSCRIPTION_DEVICE=auto). The old image
  shipped cuDNN 8 while current CTranslate2 needs CUDA 12 libraries (#5).
- Runs as an unprivileged user. PUID/PGID (default 1000:1000, Unraid
  99:100) select the uid/gid; mounted folders written by older root-run
  images are handed over on start. docker run --user also works.
- Calibre can render PDFs in the container (QtWebEngine sandbox disabled
  inside the container, offscreen platform), fixing #9.
- Job history database and log live in /app/data, models in /app/models
  (HF_HOME), so both can be kept on volumes across updates.
- Tesseract language packs selectable with TESSERACT_LANGS (default: eng
  deu fra spa ita por nld pol) (#26); new rsvg-convert, cjxl, cjpeg and
  potrace are now actually installed.
- Health check, OCI labels, whitelist .dockerignore, supervisord config in
  docker/ (worker started with python -m huey.bin.huey_consumer).
- Optional build secret build_ca for building behind TLS-inspecting proxies.
- docker/smoke-test.sh starts an image and checks the web UI, the worker,
  the non-root user, OCR languages, pandoc and LibreOffice conversions and,
  for full/cuda, the torch/docling and cuBLAS imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- .github/workflows/docker-publish.yml builds every variant on native
  amd64 and arm64 runners, smoke-tests each image, then pushes by digest
  and assembles the multi-arch tags. Pushing a tag vX.Y.Z publishes
  X.Y.Z, X.Y and latest plus the -small/-cuda equivalents and the floating
  small, cuda and latest-cuda tags (#5); pre-releases only get their
  version tags. Pull requests that touch the image build and test the
  small image; "Run workflow" can publish edge builds. Needs the
  DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets.
- Unraid templates for the CPU and CUDA images (#5), with PUID 99 /
  PGID 100 and appdata paths.
- docs/docker.md: variants, volumes, PUID/PGID, GPU setup, local builds and
  build arguments, releasing (automated and manual buildx commands),
  reproducibility and how to verify it, updating locks/snapshots/base
  images, troubleshooting.
- README: current image tags incl. arm64, updated install instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
Tested by upgrading a real 0.4 install (published image, its compose file,
edited settings, finished jobs) to the new image and rolling back.

- Entrypoint: also re-own folders whose files (not just the folder) belong
  to someone else, e.g. a job history copied in as root; tolerate storage
  that refuses chown, and stop with a clear message when a folder is not
  writable instead of crash-looping on a Python traceback.
- docker-compose.yml: keep the service name "web" from 0.4; renaming it
  left the old container running on the same port.
- Keep publishing the X.Y-latest tag 0.4 users pinned.
- Docs: "Upgrading from 0.4" (ownership, carrying the job history over,
  settings, reverse proxies, OIDC, tags, older converter versions, rollback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
…logs

- compute_type "auto" (new default and settings option) picks int8 on CPUs
  and float16 on GPUs. The CUDA image no longer forces it through
  TRANSCRIPTION_COMPUTE_TYPE, which made the settings page choice ineffective.
- Failed uploads show the server's reason (e.g. file too large) as tooltip.
- Silence Authlib's deprecation warning about its own httpx integration,
  printed once per process on every start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
- `scripts/docker-release.sh test 0.5.0` builds all variants for amd64 and
  arm64 (cuda: amd64) and pushes them as test, test-small and test-cuda.
- `promote 0.5.0` gives the tested images their release tags with
  `buildx imagetools create`: nothing is rebuilt, the release tags point to
  the exact images that were tested. It refuses test images built for
  another version.
- `tags 0.5.0` prints the release tags; the workflow now uses it too, so
  the tag scheme lives in one place.
- Workflow: also triggers on tags without "v" and with two parts (0.5),
  the style of the existing releases; without Docker Hub secrets it builds
  and tests but skips publishing (with a warning) instead of failing.
- docs/docker.md: step-by-step release from your machine (test, try,
  promote, tag) next to the GitHub Actions route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
… tags

- Export with unpack=false: with the default "docker" builder on the
  containerd image store, rewrite-timestamp otherwise fails with
  'exporter option "rewrite-timestamp" conflicts with "unpack"'.
- The closing hints list the tags that were actually pushed/promoted
  instead of assuming the full image was built.

Tested end to end against a local registry: small image for amd64 and
arm64 built and pushed as test-small (with provenance and SBOM), smoke
tests passed on both architectures, promote gave 0.5.0-small, 0.5-small
and small the same index digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjPx3yn1u3MzoCV7DMYKwB
ENABLE_WEBSOCKETS was off on both sides, so the connection manager, the
notification table and its 10 Hz queue processor, the /ws/jobs endpoint
and the browser's reconnect logic never ran. The browser half only worked
by accident: pollForJobUpdates() read a `let` that was declared further
down the file. The job history keeps using polling, which the next
commits make incremental.

Existing databases keep their (unused) notifications table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
… voices

- GET /jobs?since=<timestamp> returns only jobs created or changed since then,
  so the page no longer downloads its whole history every 1.5 s. Without
  `since`, the 100-job limit counts top-level jobs and each ZIP batch comes
  with all its sub-jobs (a big batch used to push its own parent out).
- Indexes for the history and its polling (user_id+created_at,
  user_id+updated_at) and for download lookups; they are also added to
  existing databases on startup.
- ZIP batch progress is counted with one GROUP BY instead of loading every
  sub-job each time one of them finishes. Batch conversions skip files the
  chosen tool does not accept instead of failing them.
- Image OCR runs Tesseract once per page for both the PDF and the text
  (it ran twice), converts one TIFF frame at a time instead of holding all
  of them in memory, can be cancelled between pages, and merges pages with
  pypdf (PyPDF2 is no longer imported).
- PDF OCR stops extracting preview text after 2000 characters instead of
  reading every page of the result.
- The TTS voice list runs in a thread instead of blocking the event loop
  (offline, piper's CLI could stall a worker for ~90 s), fetches Piper's
  catalogue with a timeout, also lists downloaded voices, groups voices by
  language, and is cached in DATA_DIR for all workers: a day, or 5 minutes
  when the listing was incomplete.
- Batch ZIP downloads store already-compressed files instead of deflating
  them again, and accept a form post so the browser can stream them.
- Single-chunk uploads are moved into place instead of copied.
- gzip for pages, scripts, styles and JSON (not for /download). Static
  URLs carry a content hash, so they are cached for good and an upgrade
  can't leave browsers running old scripts.
- huey workers check the queue at least once a second when idle (the
  default backoff let a job wait up to 10 s before it started).
- Dropped per-conversion gc.collect(), a duplicate status write after image
  OCR and an unused schema validation per created job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
- Jobs that were running when the background worker stopped (container
  restart, crash, out of memory) stayed "processing" forever, and the page
  kept polling them: huey does not re-run a task it had already dequeued.
  On startup the worker now marks such jobs failed (only jobs last updated
  before it started, so it never touches its own), removes their inputs,
  sends their webhooks and settles the ZIP batches they belonged to.
  Batches cut off while unpacking are failed too; queued jobs are kept.
- Webhook callbacks are retried on connection errors, 5xx and 429
  (3 attempts, 2 s and 4 s apart); the DB session is closed before the
  request instead of staying open through it.
- The periodic cleanup also removes partial files ("*.tmp-<uuid>") left
  in the upload and processed folders by interrupted uploads/conversions.
- A Whisper model is no longer unloaded while it is still transcribing
  (the next job loaded a second copy). The two copy-pasted transcription
  loops are one, and progress stays below 100 until the job completes.
- "Clear history" cancels running jobs first, like "Delete selected".
- Cancelling returns the updated job, so the page can show it at once.
- Job endpoints that only do blocking DB and file work are plain `def`
  now, so FastAPI runs them in its thread pool instead of on the event
  loop, where a write waiting for the SQLite lock stalled every request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
Job history
- Polls only the jobs changed since the newest update it has seen
  (GET /jobs?since=...), backs off from 1.5 s to 6 s while nothing changes,
  pauses in background tabs and catches up as soon as the tab is visible.
  Polling stops when no job is pending or processing.
- Only rows whose job changed are touched, and only the cells that changed:
  selections, open details and the progress bar's animation survive a poll.
  Fixes the File Size column never showing the output size until a reload,
  and sub-jobs added to an expanded batch staying hidden.
- Cancelling shows the cancelled state immediately.
- The bottom status shows the real state (Live updates / Up to date /
  Connection lost – retrying) instead of a fixed "Polling active".

Uploads
- Files go through a queue with at most 3 uploads at a time, so the first
  files finish and start processing sooner; the file input is cleared at
  once and more files can be queued right away.
- Chunks are retried with backoff after network errors, 5xx and 429.
- Files that are too large, not allowed, or not an image/PDF for OCR are
  rejected before uploading (the server refused them only after the full
  upload). Queued and running uploads can be cancelled, failed ones
  dismissed, and closing the tab mid-upload asks for confirmation.

Other
- Output formats are computed in the browser from the embedded tool list
  (no request per selection); with several files, only formats that work
  for all of them are offered, and unsupported types say so.
- Selectors are created once; submitting no longer rebuilds them all and
  re-downloads the TTS voice list (which also reset the model choice).
- The TTS list no longer shows a voice named "error" when no engine is
  installed.
- "Download Selected as ZIP" posts a form into a hidden frame, so the
  browser streams the archive to disk instead of holding it in memory.
- Escape closes the drop dialog; a failed history load is retried.
- The huey worker skips job recovery when the database does not exist yet
  (fresh install: the web server creates it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
Also guards the cancel response against a job deleted while it was
being cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
The test step always pushed test, test-small and test-cuda, so a nightly
build replaced the images waiting to be promoted. TEST_TAG=nightly pushes
nightly, nightly-small and nightly-cuda instead (promote reads the same
tag); the default stays "test".

Checked with a stand-in docker command: the test and promote steps with
TEST_TAG=nightly only touch the nightly tags, the default still pushes
test-*. Invalid tag names are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LReNFoeaXg1g9e3H4E98S
@LoredCast
LoredCast merged commit 3c22328 into main Sep 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants