Build & Deploy (Nix) / build-and-deploy (push) Successful in 56s
Chunk parts > 19MB are stored to Telegram but getFile cannot resolve files over 20MB ('Bad Request: file is too big'), making every part undownloadable (prod bug 2026-08-01: 48MB chunk -> download 500).
- src/env.ts: reject TELEGRAM_CHUNK_SIZE_BYTES > 19922944 at startup (log error + throw), default changed 20MB -> 19MB
- src/shared/utils/validation.ts: TELEGRAM_CHUNK_SIZE_MAX_BYTES constant; asSafeChunkSize now enforces the max at runtime (covers S3 multipart parts too)
- test/env.test.ts: unit tests + subprocess fail-fast tests (48MB rejected, 19MB accepted)
- test/helpers/setup-env.ts: pin safe chunk size so a stale .env can't break the suite
- .env.example + CLAUDE.md: document the 20MB getFile limit
Build & Deploy (Nix) / build-and-deploy (push) Successful in 15s
The CI runner container can't access the VPS host's systemd and Nix
directly. Instead of installing Nix in the container and trying to
access the host, SSH directly to the VPS to build and deploy.
This approach:
1. SSHs to the VPS using the VPS_SSH_KEY_VALUE secret
2. Pulls the latest code on the VPS
3. Builds with Nix directly on the VPS
4. Updates nix-env profile and restarts systemd service
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 53s
Docker socket /var/run/docker.sock is available in the runner
container. Use docker run --pid=host --privileged to access
the VPS host filesystem via chroot to execute nix-env and systemctl.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 46s
Diagnosing deploy failure: systemctl unavailable inside runner container.
Adding inspect step to understand available mounts and access mechanisms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 47s
The deploy step runs with 'sudo' which resets PATH, so nix-env is
not found. Use explicit path to the nix-env binary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 52s
Re-instate Determinate Systems installer with correct flags:
- install --no-confirm (not --no-daemon which it doesn't support)
- Nix installs to /nix/var/nix/profiles/default/bin
- Source daemon profile in build step
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 11s
The Determinate Systems installer doesn't support --no-daemon.
Switch to the official Nix installer which has a well-documented
--no-daemon flag suitable for container/CI use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build & Deploy (Nix) / build-and-deploy (push) Failing after 7s
The Gitea Actions runner image (docker.gitea.com/runner-images:ubuntu-latest)
does not have Nix pre-installed, causing 'nix: command not found' on the
build step.
Add an 'Install Nix' step using the Determinate Systems installer
(--no-daemon mode since the runner runs in a container).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- flake.nix with Bun build from nixpkgs (1.3.x)
- flake.lock pinned inputs
- .gitignore result symlink from nix build
Build: nix build .#teleuploader --impure --option sandbox false
Until CI/CD with binary cache is set up
- Replaces createReadStream / bare ReadableStream with
Readable.from(Bun.file(path).stream()) — works in both
test (Bun.write + Bun.file) and production (Node Readable)
- Reverts writeBufferToTemp back to Bun.write
- getFileType: only classify image/jpeg as 'photo'; png/gif/webp → 'document'
(Telegram Bot API rejects non-JPEG for sendPhoto)
- Dockerfile: copy home.html to dist/ instead of root (import.meta.dir = dist/)
- Update test assertion for getFileType(image/png) → 'document'
- Removed UploadBatcher (src/infrastructure/telegram/upload-batcher.ts + DI):
pending uploads no longer lost on crash, files sent directly to Telegram
- Changed upload-controller to use Bun.file().stream() instead of createReadStream
- Made PER_BOT_CONCURRENCY configurable via TELEGRAM_BOT_CONCURRENCY env
- Fixed 18 test files with updated import paths and mock shapes
- Updated package.json test script: telegramQueue.test.ts → bot-pool.test.ts
- Build, lint, and test suite all pass
- Now 6 total bots: 1 main + 5 additional
- Removed UPLOAD_CONCURRENCY from PRODUCTION_ENV
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Each bot has its own PQueue with concurrency=1
- selectBot() assigns uploads to least-loaded available bot
- 429 rate limits are tracked per-bot with cooldown timers
- Failed uploads retry on next available bot
- Removed global upload-queue.ts and uploadConcurrency config
- Updated ITelegramService interface
Effective concurrency derived from bot pool size. Chunked-storage
backpressure now uses botPool.getEffectiveConcurrency().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add MAX_OUTER_RETRIES constant and transientAttempts counter for outer-loop retry
- Restore getFileInfo transient retry logging with bot identity and fileId
- Create test/bot-pool.test.ts with 4 tests for core BotPool behavior
- Add empty-bots guard in selectBot() returning null
- Add safety net comment and improved logging for outer 429 catch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each bot has its own PQueue (concurrency=1). Uploads are assigned to
the least-loaded available bot. On 429, the bot is marked rate-limited
and the upload retries on the next available bot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a setTimeout(0) microtask yield after Promise.race to ensure
the .finally() handler that removes promises from the inFlight
set has executed before the next backpressure check.
Also ensure parts array is sorted by partNumber after concurrent
uploads complete, since promises resolve in arbitrary order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Previously uploadFileInTelegramChunks awaited each chunk's upload
before reading the next, making all chunks sequential within a file.
Now chunks are uploaded concurrently using a managed Set of in-flight
promises with backpressure limiting (2x uploadConcurrency).
This means a single 1GB Docker layer split into 48MB chunks will
have up to 32 chunks uploading simultaneously, not one at a time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tested actual Telegram Bot API limit:
- 49MB ✅
- 50MB ❌ (413 Request Entity Too Large)
Set TELEGRAM_CHUNK_SIZE_BYTES=50331648 (48MB) for safety
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The createMultipartUpload function inserts content_type but the
database column was missing, causing 500 errors on every Gitea
Docker registry push (which uses multipart uploads for blob storage).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploy FileDrop / deploy (push) Failing after 14m18s
AWS SDK requires ETag header in 304 responses. Without it, the SDK
throws UnknownError despite receiving a valid 304 status code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AWS SDK requires ETag header in 304 responses. Without it, the SDK
throws UnknownError despite receiving a valid 304 status code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The empty-segment skip in normalizeUri (introduced in round 1 fix)
was stripping trailing slashes from canonical URIs, e.g. /bucket/
became /bucket. The AWS SDK signs with the trailing slash intact, so
signatures never matched for any S3 operation with a body.
The fix: only skip '.' segments (dot-segment removal per RFC 3986),
preserve all other segments including empty ones from trailing
slashes and double slashes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AWS SDK includes trailing slash in the canonical URI for bucket
operations (e.g. PUT /bucket-name/). My earlier 'fix' that stripped
trailing slashes broke SigV4 signature verification. The trailing
slash is intentional per AWS SigV4 — only dot-segments are removed,
not trailing slashes.
Re-verified with @smithy/signature-v4: path /bucket-name/ produces
the client signature, while /bucket-name does not match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AWS SigV4 canonical URI must not have trailing slash (except root '/').
Bun can receive paths with trailing slash from SDK, causing signature
mismatch for all bucket operations (CreateBucket, HeadBucket, etc.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>