Container image packaging with periodic fetch worker #46

Manually merged
marvin8 merged 10 commits from feat/issue-40-container-packaging into main 2026-09-25 20:54:01 +00:00
Collaborator

Implements #40 per the findings/plan comment on the issue.

What

  • Containerfile (multi-stage, cang-style): uv sync --frozen --no-dev --no-install-project builds the locked dependency set into /app/.venv at its final path; the source tree, alembic.ini, and alembic/ ship at /app because init_db resolves alembic.ini against the source root (parents[3] of the package file) — a plain wheel install would break that lookup. Non-root system user, OCI labels, /data volume holding both the SQLite db (/data/yunjin.db) and the media directory (/data/media, derived by the app from the db path).
  • One image, two entry points (container-entrypoint.sh):
    • web (default): waitress serves yunjin.web.wsgi:app on $PORT (default 8080).
    • worker: runs yunjin init (with bounded retry) once, then loops yunjin fetch → yunjin aggregate → yunjin extract-tags → yunjin extract-media every $YUNJIN_WORKER_INTERVAL seconds (default 3600). Task failures are logged, not fatal; SIGTERM exits promptly (current child is killed).
  • Dependency: adds waitress~=3.0.2 (pure-Python production WSGI server — the Flask counterpart to cang's single-process uvicorn). uv.lock and pylock.toml regenerated in the same commit. waitress is ignored for deptry DEP002 (invoked by the entrypoint, never imported), mirroring cang's per-rule-ignores pattern.
  • CI: container job build-only buildah build on PRs/pushes/dispatch; publish-container job on (non-prerelease) release publication pushes registry.marvin8.zone/marvin8/yunjin:{latest,<version>} after gitleaks + checks, mirroring cang. Adds the release: types: [published] trigger (existing triggers unchanged).
  • Podman-generic naming: container-entrypoint.sh and .containerignore (buildah/podman read .containerignore natively); no docker-specific references anywhere.

Verification

  • Entrypoint smoke-tested locally with the repo venv over a temp data dir: web served the index with HTTP 200 on port 8080; worker ran full init → fetch → aggregate → extract-tags → extract-media cycles and repeated on the interval; invalid YUNJIN_WORKER_INTERVAL values (0, 00, 000, 1h, abc, -5) fail fast; SIGTERM during a running task or the sleep phase exits immediately.
  • uv run nox: all 9 sessions green (ty, ruff, complexipy, pysentry, pytest on 3.12/3.13/3.14, deptry, zaojun).
  • The image build itself is validated by the CI container job (green on zhuiri via buildah).

Notes for review

  • One-off commands against a running install: podman exec yunjin python -m yunjin extract-media --article-id 42 (or podman run --rm -v yunjin-data:/data <image> python -m yunjin ... — the entrypoint's default case execs arbitrary commands).
  • zaojun logs a "waitress: license could not be determined" warning (Apache-2.0 in reality); session passes.

Closes #40

Implements #40 per the findings/plan comment on the issue. ## What - **Containerfile** (multi-stage, cang-style): `uv sync --frozen --no-dev --no-install-project` builds the locked dependency set into `/app/.venv` at its final path; the source tree, `alembic.ini`, and `alembic/` ship at `/app` because `init_db` resolves `alembic.ini` against the source root (`parents[3]` of the package file) — a plain wheel install would break that lookup. Non-root system user, OCI labels, `/data` volume holding both the SQLite db (`/data/yunjin.db`) and the media directory (`/data/media`, derived by the app from the db path). - **One image, two entry points** (`container-entrypoint.sh`): - `web` (default): waitress serves `yunjin.web.wsgi:app` on `$PORT` (default 8080). - `worker`: runs `yunjin init` (with bounded retry) once, then loops `yunjin fetch` → `yunjin aggregate` → `yunjin extract-tags` → `yunjin extract-media` every `$YUNJIN_WORKER_INTERVAL` seconds (default 3600). Task failures are logged, not fatal; SIGTERM exits promptly (current child is killed). - **Dependency**: adds `waitress~=3.0.2` (pure-Python production WSGI server — the Flask counterpart to cang's single-process uvicorn). `uv.lock` and `pylock.toml` regenerated in the same commit. waitress is ignored for deptry DEP002 (invoked by the entrypoint, never imported), mirroring cang's per-rule-ignores pattern. - **CI**: `container` job build-only buildah build on PRs/pushes/dispatch; `publish-container` job on (non-prerelease) release publication pushes `registry.marvin8.zone/marvin8/yunjin:{latest,<version>}` after gitleaks + checks, mirroring cang. Adds the `release: types: [published]` trigger (existing triggers unchanged). - **Podman-generic naming**: `container-entrypoint.sh` and `.containerignore` (buildah/podman read `.containerignore` natively); no docker-specific references anywhere. ## Verification - Entrypoint smoke-tested locally with the repo venv over a temp data dir: `web` served the index with HTTP 200 on port 8080; `worker` ran full init → fetch → aggregate → extract-tags → extract-media cycles and repeated on the interval; invalid `YUNJIN_WORKER_INTERVAL` values (`0`, `00`, `000`, `1h`, `abc`, `-5`) fail fast; SIGTERM during a running task or the sleep phase exits immediately. - `uv run nox`: all 9 sessions green (ty, ruff, complexipy, pysentry, pytest on 3.12/3.13/3.14, deptry, zaojun). - The image build itself is validated by the CI `container` job (green on zhuiri via buildah). ## Notes for review - One-off commands against a running install: `podman exec yunjin python -m yunjin extract-media --article-id 42` (or `podman run --rm -v yunjin-data:/data <image> python -m yunjin ...` — the entrypoint's default case execs arbitrary commands). - zaojun logs a "waitress: license could not be determined" warning (Apache-2.0 in reality); session passes. Closes #40
Multi-stage Containerfile modeled on cang: locked deps via
uv sync --frozen --no-dev --no-install-project into /app/.venv,
source tree + alembic migrations at /app (init_db resolves
alembic.ini against the source root), non-root system user,
/data volume for the SQLite db and media directory.

One image, two entry points via docker-entrypoint.sh:
- web: waitress (new runtime dep, ~=3.0.2) on $PORT (default 8080)
- worker: init once, then fetch -> aggregate -> extract-tags every
  $YUNJIN_WORKER_INTERVAL seconds; task failures are logged, not fatal

waitress is excluded from deptry DEP002: invoked by the entrypoint,
never imported.
👷 Build container image in CI
All checks were successful
/ gitleaks (pull_request) Successful in 27s
/ container (pull_request) Successful in 2m10s
/ checks (pull_request) Successful in 2m23s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m31s
cd31e063dc
- container job: build-only buildah build on PRs, pushes, and manual
  dispatch (skipped on release events so publish is the only build)
- publish-container job: on release publication, build and push
  registry.marvin8.zone/marvin8/yunjin:{latest,<version>} after
  gitleaks and checks pass, mirroring cang's release pipeline
- add release: types: [published] trigger so the publish job can run
forgejo-actions left a comment

WuMing

Found 12 issue(s). See inline comments below.

## WuMing Found **12** issue(s). See inline comments below.
@ -58,2 +60,4 @@
uv run nox
uv build
container:

config [LOW]

The new container build job has no needs entry, so it runs in parallel with (and independently of) gitleaks and checks. Unlike publish-container, which gates on needs: [gitleaks, checks], this job will happily build an image from a revision that fails linting/tests. If the image build is meant to be part of CI validation, add needs: [gitleaks, checks] (or otherwise constrain it) for consistency with the publish job.

**config** [LOW] The new `container` build job has no `needs` entry, so it runs in parallel with (and independently of) `gitleaks` and `checks`. Unlike `publish-container`, which gates on `needs: [gitleaks, checks]`, this job will happily build an image from a revision that fails linting/tests. If the image build is meant to be part of CI validation, add `needs: [gitleaks, checks]` (or otherwise constrain it) for consistency with the publish job. <!-- wuming:sha256:c0386a9ec9512e243925805d376d32f2a7f02eb48826e1c20d39992d893ac7e3 -->
@ -60,0 +65,4 @@
timeout-minutes: 30
if: github.event_name != 'release'
steps:
- uses: actions/checkout@v4

security [LOW]

A08: Third-party CI action is referenced by the mutable tag v4 instead of a full commit SHA, which can be retargeted. Pin actions/checkout to a verified commit SHA.

**security** [LOW] A08: Third-party CI action is referenced by the mutable tag `v4` instead of a full commit SHA, which can be retargeted. Pin `actions/checkout` to a verified commit SHA. <!-- wuming:sha256:013cea22d885ed2672bfc60898bfa9bf9f196d85f411e7fe465243374bdc8f5c -->

security [LOW]

A08: Third-party CI action is referenced by the mutable tag v4 instead of a full commit SHA, which can be retargeted. Pin actions/checkout to a verified commit SHA.

**security** [LOW] A08: Third-party CI action is referenced by the mutable tag `v4` instead of a full commit SHA, which can be retargeted. Pin `actions/checkout` to a verified commit SHA. <!-- wuming:sha256:792a6858224827a7b5162ea2f494aa1a3b5d718a60cbb8b4bc0817b619c41ae1 -->
@ -60,0 +97,4 @@
- name: Install buildah
run: apt-get update -qq && apt-get install -y -qq podman buildah
- name: Login to registry
run: buildah login --username "${{ secrets.registry_user }}" --password "${{ secrets.registry_password }}" registry.marvin8.zone

config [MEDIUM]

The registry password is passed directly as a command-line argument (--password "${{ secrets.registry_password }}"). Command lines are visible to other processes on the runner (and can end up in shell traces/logs). Use stdin instead, e.g. printf '%s' "${{ secrets.registry_password }}" | buildah login --username "${{ secrets.registry_user }}" --password-stdin registry.marvin8.zone, or pass the credentials via environment variables.

**config** [MEDIUM] The registry password is passed directly as a command-line argument (`--password "${{ secrets.registry_password }}"`). Command lines are visible to other processes on the runner (and can end up in shell traces/logs). Use stdin instead, e.g. `printf '%s' "${{ secrets.registry_password }}" | buildah login --username "${{ secrets.registry_user }}" --password-stdin registry.marvin8.zone`, or pass the credentials via environment variables. <!-- wuming:sha256:fd2891ae61949da4e32d8b194ed3bce501f6a4491a6ed1e4974e2282bdfacabb -->

security [MEDIUM]

A02: The registry password is passed on the command line, which can expose it via process listings or CI logs. Use buildah login --password-stdin or a credentials file instead.

**security** [MEDIUM] A02: The registry password is passed on the command line, which can expose it via process listings or CI logs. Use `buildah login --password-stdin` or a credentials file instead. <!-- wuming:sha256:fd2891ae61949da4e32d8b194ed3bce501f6a4491a6ed1e4974e2282bdfacabb -->
@ -60,0 +118,4 @@
--annotation "org.opencontainers.image.url=https://forge.marvin8.zone/marvin8/yunjin" \
--annotation "org.opencontainers.image.revision=${REVISION}" \
--annotation "org.opencontainers.image.version=${VERSION}" \
-t registry.marvin8.zone/marvin8/yunjin:latest \

config [MEDIUM]

release: types: [published] (line 7) fires for pre-releases as well, and this job unconditionally pushes the moving latest tag. Publishing a pre-release would therefore overwrite the latest image with a pre-release build. Guard the job/tag with the pre-release flag, e.g. if: github.event_name == 'release' && !github.event.release.prerelease, and only apply the latest tag on non-prerelease releases.

**config** [MEDIUM] `release: types: [published]` (line 7) fires for pre-releases as well, and this job unconditionally pushes the moving `latest` tag. Publishing a pre-release would therefore overwrite the `latest` image with a pre-release build. Guard the job/tag with the pre-release flag, e.g. `if: github.event_name == 'release' && !github.event.release.prerelease`, and only apply the `latest` tag on non-prerelease releases. <!-- wuming:sha256:7ec8fd8308e42149f007095e74347c25d09853a7809425f35e830b14720a3f31 -->
Containerfile Outdated
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder

security [LOW]

A08: The builder base image is referenced by a mutable tag rather than an immutable digest. A retagged or compromised image could be pulled; pin python:3.12-slim-bookworm with a @sha256:... digest.

**security** [LOW] A08: The builder base image is referenced by a mutable tag rather than an immutable digest. A retagged or compromised image could be pulled; pin `python:3.12-slim-bookworm` with a `@sha256:...` digest. <!-- wuming:sha256:a048f89c733e34223c2150ad00f9b78e6ceda2716e5ec049c8439a440e2445fd -->
Containerfile Outdated
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION

shell [MEDIUM]

ARG UV_VERSION is declared with no default value and is interpolated directly into an image reference on line 4 (COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION}). Any build that does not explicitly pass --build-arg UV_VERSION=... (local docker build, a new CI job, a contributor reproducing the build) collapses the reference to ghcr.io/astral-sh/uv: and fails with an invalid-reference error, or silently resolves to whatever floating tag is supplied. Supply a pinned default so the Containerfile is self-contained and the tool version is recorded in the repo, e.g. ARG UV_VERSION=0.x.y@sha256:<digest>, and keep passing it explicitly from CI.

**shell** [MEDIUM] `ARG UV_VERSION` is declared with no default value and is interpolated directly into an image reference on line 4 (`COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION}`). Any build that does not explicitly pass `--build-arg UV_VERSION=...` (local `docker build`, a new CI job, a contributor reproducing the build) collapses the reference to `ghcr.io/astral-sh/uv:` and fails with an invalid-reference error, or silently resolves to whatever floating tag is supplied. Supply a pinned default so the Containerfile is self-contained and the tool version is recorded in the repo, e.g. `ARG UV_VERSION=0.x.y@sha256:<digest>`, and keep passing it explicitly from CI. <!-- wuming:sha256:68451ce3a7ccd677712db0b26b4db7b29f6e2b94bb4ab57707c640c5804f63e2 -->
Containerfile Outdated
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv

security [LOW]

A08: The uv tool image is downloaded by a mutable version tag without digest pinning, allowing tag substitution in the supply chain. Pin ghcr.io/astral-sh/uv to a digest.

**security** [LOW] A08: The uv tool image is downloaded by a mutable version tag without digest pinning, allowing tag substitution in the supply chain. Pin `ghcr.io/astral-sh/uv` to a digest. <!-- wuming:sha256:681620052f13728ea8734782394358ec950b5d333cba15df5b8e61fe9b886a20 -->
Containerfile Outdated
@ -0,0 +15,4 @@
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim-bookworm

security [LOW]

A08: The runtime base image is referenced by a mutable tag rather than an immutable digest. A retagged or compromised image could be pulled; pin python:3.12-slim-bookworm with a @sha256:... digest.

**security** [LOW] A08: The runtime base image is referenced by a mutable tag rather than an immutable digest. A retagged or compromised image could be pulled; pin `python:3.12-slim-bookworm` with a `@sha256:...` digest. <!-- wuming:sha256:a91c026f94a7912ee77438b751d45a5c73ef9c253398ccdfb6196889f334bd1a -->
@ -0,0 +20,4 @@
case "${1:-web}" in
web)
: "${PORT:=8080}"
exec waitress-serve --host=0.0.0.0 --port="${PORT}" yunjin.web.wsgi:app

shell [MEDIUM]

The web entry point execs waitress without ever initializing the database, while worker runs yunjin init (line 28). The application resolves its SQLite path against cwd (/data) and ships alembic for schema creation, so on a fresh /data volume — or whenever the web container is started before the worker — it will serve against a missing/empty database and fail on the first query. alembic upgrade head is idempotent, so run python -m yunjin init before the exec here as well, or explicitly document and enforce that the worker must be started first.

**shell** [MEDIUM] The `web` entry point execs waitress without ever initializing the database, while `worker` runs `yunjin init` (line 28). The application resolves its SQLite path against cwd (/data) and ships alembic for schema creation, so on a fresh `/data` volume — or whenever the web container is started before the worker — it will serve against a missing/empty database and fail on the first query. `alembic upgrade head` is idempotent, so run `python -m yunjin init` before the exec here as well, or explicitly document and enforce that the worker must be started first. <!-- wuming:sha256:ae1f902526ca7e0b3d8f34e104bb8d4724bddef5a7323887a0d0f2562eac2a36 -->
@ -0,0 +23,4 @@
exec waitress-serve --host=0.0.0.0 --port="${PORT}" yunjin.web.wsgi:app
;;
worker)
: "${YUNJIN_WORKER_INTERVAL:=3600}"

shell [MEDIUM]

YUNJIN_WORKER_INTERVAL is taken from the environment without validation. If it is non-numeric (e.g. 1h, 60s) or 0, sleep fails immediately; because the sleep is backgrounded and its exit status is discarded by wait "$!" || : (lines 37-38), set -e never fires and the loop spins with no delay, running fetch/aggregate/extract-tags and the LLM calls back-to-back. Validate the value before entering the loop, e.g. case "$YUNJIN_WORKER_INTERVAL" in ''|*[!0-9]*) echo "invalid YUNJIN_WORKER_INTERVAL" >&2; exit 1;; esac plus a check that it is greater than 0.

**shell** [MEDIUM] `YUNJIN_WORKER_INTERVAL` is taken from the environment without validation. If it is non-numeric (e.g. `1h`, `60s`) or `0`, `sleep` fails immediately; because the sleep is backgrounded and its exit status is discarded by `wait "$!" || :` (lines 37-38), `set -e` never fires and the loop spins with no delay, running fetch/aggregate/extract-tags and the LLM calls back-to-back. Validate the value before entering the loop, e.g. `case "$YUNJIN_WORKER_INTERVAL" in ''|*[!0-9]*) echo "invalid YUNJIN_WORKER_INTERVAL" >&2; exit 1;; esac` plus a check that it is greater than 0. <!-- wuming:sha256:f5a6586b13f40c2439fc74cd2fba79deacf3aee45bf3254a4d0551dc8762a006 -->
🔧 Address WuMing review wave on PR #46
Some checks failed
/ checks (pull_request) Failing after 10s
/ gitleaks (pull_request) Successful in 14s
/ publish-container (pull_request) Has been skipped
/ container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m32s
228c7065db
- Containerfile: default ARG UV_VERSION=0.12.18 so local builds
  without --build-arg resolve a valid uv image reference
- entrypoint: validate YUNJIN_WORKER_INTERVAL as a positive integer
  before the loop (invalid values previously caused a busy spin)
- ci.yml: gate the container build job on [gitleaks, checks];
  skip publish-container for pre-releases so latest is never
  overwritten by one; registry login via --password-stdin
forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
Containerfile Outdated
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder

security [LOW]

A08: The base image is referenced by the mutable tag python:3.12-slim-bookworm without a digest pin. A compromised or updated upstream tag could inject malicious content into the final image. Pin the base image by digest (e.g. python:3.12-slim-bookworm@sha256:...) and verify integrity.

**security** [LOW] A08: The base image is referenced by the mutable tag python:3.12-slim-bookworm without a digest pin. A compromised or updated upstream tag could inject malicious content into the final image. Pin the base image by digest (e.g. python:3.12-slim-bookworm@sha256:...) and verify integrity. <!-- wuming:sha256:e23e53ebe03917ca8a22fb2d7f13bb417f095ee2d5a63c5ff72006b0506ca213 -->
Containerfile Outdated
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION=0.12.18
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv

security [LOW]

A08: The uv build dependency is copied from a mutable image tag (ghcr.io/astral-sh/uv:${UV_VERSION}) with no digest or integrity verification. The UV_VERSION value is derived from pyproject.toml, so changes to that file control which image is executed during the build. Pin this image by digest and restrict UV_VERSION to an approved allow-list.

**security** [LOW] A08: The uv build dependency is copied from a mutable image tag (ghcr.io/astral-sh/uv:${UV_VERSION}) with no digest or integrity verification. The UV_VERSION value is derived from pyproject.toml, so changes to that file control which image is executed during the build. Pin this image by digest and restrict UV_VERSION to an approved allow-list. <!-- wuming:sha256:955844d7d53724fea5e6ab6ed695ec45eaeec462298ace262d9bb2e3be8ff1f8 -->
Author
Collaborator

Containerfile

ARG UV_VERSION is declared with no default value and is interpolated directly into an image reference…

✅ Fixed in commit 228c706 — ARG UV_VERSION now defaults to 0.12.18 (matching the uv~=0.12.18 dev-group pin in pyproject.toml); CI still passes it explicitly via the pyproject grep, so the recorded default and the CI-resolved value stay aligned.

[`Containerfile`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3527) > `ARG UV_VERSION` is declared with no default value and is interpolated directly into an image reference… ✅ Fixed in commit `228c706` — `ARG UV_VERSION` now defaults to `0.12.18` (matching the `uv~=0.12.18` dev-group pin in pyproject.toml); CI still passes it explicitly via the pyproject grep, so the recorded default and the CI-resolved value stay aligned.
Author
Collaborator

docker-entrypoint.sh

YUNJIN_WORKER_INTERVAL is taken from the environment without validation…

✅ Fixed in commit 228c706 — a POSIX case guard now rejects empty, non-numeric, or zero values with a clear stderr message and exit 1 before the loop starts. Smoke-tested: YUNJIN_WORKER_INTERVAL=1h and =0 both fail fast; =3 loops normally.

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3529) > `YUNJIN_WORKER_INTERVAL` is taken from the environment without validation… ✅ Fixed in commit `228c706` — a POSIX `case` guard now rejects empty, non-numeric, or zero values with a clear stderr message and `exit 1` before the loop starts. Smoke-tested: `YUNJIN_WORKER_INTERVAL=1h` and `=0` both fail fast; `=3` loops normally.
Author
Collaborator

.forgejo/workflows/ci.yml

The new container build job has no needs entry…

✅ Fixed in commit 228c706 — the container job now has needs: [gitleaks, checks], so images are only built from revisions that pass linting/tests, consistent with publish-container.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3524) > The new `container` build job has no `needs` entry… ✅ Fixed in commit `228c706` — the `container` job now has `needs: [gitleaks, checks]`, so images are only built from revisions that pass linting/tests, consistent with `publish-container`.
Author
Collaborator

.forgejo/workflows/ci.yml

release: types: [published] fires for pre-releases as well, and this job unconditionally pushes the moving latest tag…

✅ Fixed in commit 228c706 — publish-container now runs only on github.event_name == 'release' && !github.event.release.prerelease, so a pre-release can never overwrite latest.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3526) > `release: types: [published]` fires for pre-releases as well, and this job unconditionally pushes the moving `latest` tag… ✅ Fixed in commit `228c706` — `publish-container` now runs only on `github.event_name == 'release' && !github.event.release.prerelease`, so a pre-release can never overwrite `latest`.
Author
Collaborator

.forgejo/workflows/ci.yml

The registry password is passed directly as a command-line argument…

✅ Fixed in commit 228c706 — registry login now pipes the password via stdin: printf '%s' ... | buildah login --username ... --password-stdin registry.marvin8.zone. This also covers the duplicate of this finding posted by the security agent (identical wuming marker, comment 3530).

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3525) > The registry password is passed directly as a command-line argument… ✅ Fixed in commit `228c706` — registry login now pipes the password via stdin: `printf '%s' ... | buildah login --username ... --password-stdin registry.marvin8.zone`. This also covers the duplicate of this finding posted by the security agent (identical wuming marker, comment 3530).
Author
Collaborator

.forgejo/workflows/ci.yml

A02: The registry password is passed on the command line…

✅ Same finding as comment 3525 (identical wuming marker) — fixed in commit 228c706 via buildah login --password-stdin; see the reply there.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3530) > A02: The registry password is passed on the command line… ✅ Same finding as comment 3525 (identical wuming marker) — fixed in commit `228c706` via `buildah login --password-stdin`; see the reply there.
Author
Collaborator

docker-entrypoint.sh

The web entry point execs waitress without ever initializing the database…

🔴 Not actioned — the premise doesn't hold for this codebase. web execs waitress-serve yunjin.web.wsgi:app, and importing yunjin.web.wsgi builds the app at module level (app = create_app()), which runs init_db() — an idempotent alembic upgrade head plus default-user seed — before the server starts. Verified in the smoke test recorded in the PR description: web mode alone on a fresh empty directory created the database and schema on boot and served the index with HTTP 200 before any worker existed. There is no web/worker ordering constraint.

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3528) > The `web` entry point execs waitress without ever initializing the database… 🔴 Not actioned — the premise doesn't hold for this codebase. `web` execs `waitress-serve yunjin.web.wsgi:app`, and importing `yunjin.web.wsgi` builds the app at module level (`app = create_app()`), which runs `init_db()` — an idempotent alembic `upgrade head` plus default-user seed — before the server starts. Verified in the smoke test recorded in the PR description: web mode alone on a fresh empty directory created the database and schema on boot and served the index with HTTP 200 before any worker existed. There is no web/worker ordering constraint.
Author
Collaborator

Containerfile

A08: The builder base image is referenced by a mutable tag rather than an immutable digest…

❓ Over to Marvin8 — repo-convention call, not actioned yet (covers comments 3531, 3532, 3533: builder base, runtime base, and uv tool image digest pinning). The Containerfile deliberately mirrors cang's — the stated template for this issue — which uses the same mutable references (python:3.12-slim-bookworm, ghcr.io/astral-sh/uv:${UV_VERSION}) with no digest pinning. Digest-pinning yunjin alone would diverge from the sibling template and add manual bump discipline. If you want digest pinning, say the word and I'll add it here (and propose it for cang for parity).

[`Containerfile`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3531) > A08: The builder base image is referenced by a mutable tag rather than an immutable digest… ❓ Over to Marvin8 — repo-convention call, not actioned yet (covers comments 3531, 3532, 3533: builder base, runtime base, and uv tool image digest pinning). The Containerfile deliberately mirrors cang's — the stated template for this issue — which uses the same mutable references (`python:3.12-slim-bookworm`, `ghcr.io/astral-sh/uv:${UV_VERSION}`) with no digest pinning. Digest-pinning yunjin alone would diverge from the sibling template and add manual bump discipline. If you want digest pinning, say the word and I'll add it here (and propose it for cang for parity).
Author
Collaborator

.forgejo/workflows/ci.yml

A08: Third-party CI action is referenced by the mutable tag v4 instead of a full commit SHA…

❓ Over to Marvin8 — repo-convention call, not actioned yet (covers comments 3534 and 3535). actions/checkout@v4 (and marvin8/wuming@main) pre-exist across this workflow and sibling repos (cang uses the same); this PR only added two more instances of the established pattern. Pinning to commit SHAs is a repo-wide (arguably fleet-wide) change beyond this PR's scope — happy to do a follow-up PR that pins everything if you want it.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3534) > A08: Third-party CI action is referenced by the mutable tag `v4` instead of a full commit SHA… ❓ Over to Marvin8 — repo-convention call, not actioned yet (covers comments 3534 and 3535). `actions/checkout@v4` (and `marvin8/wuming@main`) pre-exist across this workflow and sibling repos (cang uses the same); this PR only added two more instances of the established pattern. Pinning to commit SHAs is a repo-wide (arguably fleet-wide) change beyond this PR's scope — happy to do a follow-up PR that pins everything if you want it.
Author
Collaborator

Duplicates of wave-1 findings (already escalated, awaiting Marvin8)

Containerfile base image digest · Containerfile uv image digest

Same substance as the digest-pinning group answered in the wave-1 reply (new markers only because the diff changed): Containerfile intentionally mirrors cang's mutable-tag layout; digest pinning is Marvin8's convention call. No further action.

### Duplicates of wave-1 findings (already escalated, awaiting Marvin8) [`Containerfile` base image digest](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3539) · [`Containerfile` uv image digest](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3540) Same substance as the digest-pinning group answered in [the wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3549) (new markers only because the diff changed): `Containerfile` intentionally mirrors cang's mutable-tag layout; digest pinning is Marvin8's convention call. No further action.
🔁 Re-trigger CI after zhuiri checks-job flake
All checks were successful
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Successful in 1m56s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m20s
/ container (pull_request) Successful in 1m24s
3eb227992f
forgejo-actions left a comment

WuMing

Found 4 issue(s). See inline comments below.

## WuMing Found **4** issue(s). See inline comments below.
Containerfile Outdated
@ -0,0 +15,4 @@
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim-bookworm

shell [MEDIUM]

Both stages (this runtime stage and the builder on line 2) use the floating tag python:3.12-slim-bookworm, so the resulting image is not reproducible and a silent upstream retag changes (or breaks) what you ship. Pin the base images by digest, e.g. FROM python:3.12-slim-bookworm@sha256:<digest>, and update the digest explicitly (Renovate/Dependabot) when you intend to move.

**shell** [MEDIUM] Both stages (this runtime stage and the builder on line 2) use the floating tag `python:3.12-slim-bookworm`, so the resulting image is not reproducible and a silent upstream retag changes (or breaks) what you ship. Pin the base images by digest, e.g. `FROM python:3.12-slim-bookworm@sha256:<digest>`, and update the digest explicitly (Renovate/Dependabot) when you intend to move. <!-- wuming:sha256:69309d1cfdc03d92cffb552765d97332dc223748ceb301c279cdbcba2cf0d533 -->
@ -0,0 +25,4 @@
worker)
: "${YUNJIN_WORKER_INTERVAL:=3600}"
case "$YUNJIN_WORKER_INTERVAL" in
''|*[!0-9]*|0)

shell [LOW]

The validation rejects the literal 0 and any non-digit, but not zero written with leading zeros: YUNJIN_WORKER_INTERVAL=00 (or 000) falls through, and sleep 00 returns immediately, turning the loop into a tight loop that runs fetch/aggregate/extract-tags back to back with no delay. Validate numerically instead of by pattern, e.g. run the case to reject non-digits, then [ "$YUNJIN_WORKER_INTERVAL" -ge 1 ] || { ... exit 1; } (or strip leading zeros first).

**shell** [LOW] The validation rejects the literal `0` and any non-digit, but not zero written with leading zeros: `YUNJIN_WORKER_INTERVAL=00` (or `000`) falls through, and `sleep 00` returns immediately, turning the loop into a tight loop that runs `fetch`/`aggregate`/`extract-tags` back to back with no delay. Validate numerically instead of by pattern, e.g. run the case to reject non-digits, then `[ "$YUNJIN_WORKER_INTERVAL" -ge 1 ] || { ... exit 1; }` (or strip leading zeros first). <!-- wuming:sha256:2cab8d45d62cce5a3d3fed95f5759a9068e4a448c172f3b22786cd2202f9c731 -->

shell [LOW]

Under set -eu this unguarded init is fatal: any transient failure (DB locked, volume not yet writable, brief network error fetching migrations) exits the container and sends it into an orchestrator restart/crash-loop, even though the periodic tasks in the loop below are deliberately tolerated. Consider a small retry loop or logging the failure explicitly before exiting so the cause is obvious in logs; keep it distinct from the logged-but-continuing task failures.

**shell** [LOW] Under `set -eu` this unguarded `init` is fatal: any transient failure (DB locked, volume not yet writable, brief network error fetching migrations) exits the container and sends it into an orchestrator restart/crash-loop, even though the periodic tasks in the loop below are deliberately tolerated. Consider a small retry loop or logging the failure explicitly before exiting so the cause is obvious in logs; keep it distinct from the logged-but-continuing task failures. <!-- wuming:sha256:06040c0ee751b2f1420ee06cbdb10602b610bb8ec27a9978f6069d5a0d016b47 -->
@ -0,0 +35,4 @@
while :; do
for task in fetch aggregate extract-tags; do
echo "yunjin worker: running ${task}"
if ! python -m yunjin "${task}"; then

shell [LOW]

SIGTERM/SIGINT is only honoured between tasks: POSIX defers trap execution while a foreground command is running, so a long yunjin fetch/aggregate keeps the shell alive until it finishes. In an orchestrator with a 10s default grace period the container is then SIGKILLed at PID 1, which can interrupt SQLite mid-write. Run the task in the background (python -m yunjin "${task}" & pid=$!), record the PID, and in the trap kill "$pid" before exiting, then wait for it.

**shell** [LOW] SIGTERM/SIGINT is only honoured between tasks: POSIX defers trap execution while a foreground command is running, so a long `yunjin fetch`/`aggregate` keeps the shell alive until it finishes. In an orchestrator with a 10s default grace period the container is then SIGKILLed at PID 1, which can interrupt SQLite mid-write. Run the task in the background (`python -m yunjin "${task}" & pid=$!`), record the PID, and in the trap `kill "$pid"` before exiting, then `wait` for it. <!-- wuming:sha256:c504a56a1a982fddd357dd8a7c88e663d4daf9f4506bbc820b6d99049cdbbf73 -->
🐛 Harden worker entrypoint signal and init handling
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ checks (pull_request) Successful in 1m48s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m52s
/ container (pull_request) Successful in 1m31s
8907c198b6
WuMing review wave 3:
- numeric check also rejects leading-zero intervals (00/000) that
  slipped past the digit pattern and made sleep a no-op busy loop
- init retries 3x with logged failures before giving up, so a
  transient db lock or volume hiccup does not crash-loop the worker
- loop tasks run in the background and the TERM/INT trap kills the
  current child before exiting, honouring shutdown signals promptly
  instead of deferring until the foreground task finishes
Author
Collaborator

docker-entrypoint.sh

YUNJIN_WORKER_INTERVAL=00 (or 000) falls through, and sleep 00 returns immediately…

✅ Fixed in commit 8907c19 — the digit-pattern case is now followed by a numeric check (-lt 1 rejects it), so 00, 000, and 0 all fail fast with the same message. Smoke-tested: 0, 00, 000, 1h, abc, -5 all exit 1.

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3558) > `YUNJIN_WORKER_INTERVAL=00` (or `000`) falls through, and `sleep 00` returns immediately… ✅ Fixed in commit `8907c19` — the digit-pattern case is now followed by a numeric check (`-lt 1` rejects it), so `00`, `000`, and `0` all fail fast with the same message. Smoke-tested: `0`, `00`, `000`, `1h`, `abc`, `-5` all exit 1.
Author
Collaborator

docker-entrypoint.sh

Under set -eu this unguarded init is fatal: any transient failure… exits the container and sends it into a restart/crash-loop…

✅ Fixed in commit 8907c19 — init now retries 3 times with a 5s pause, logging each failed attempt to stderr, and only exits non-zero after the final attempt, keeping persistent misconfiguration fail-fast while tolerating transient locks/hiccups.

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3560) > Under `set -eu` this unguarded `init` is fatal: any transient failure… exits the container and sends it into a restart/crash-loop… ✅ Fixed in commit `8907c19` — init now retries 3 times with a 5s pause, logging each failed attempt to stderr, and only exits non-zero after the final attempt, keeping persistent misconfiguration fail-fast while tolerating transient locks/hiccups.
Author
Collaborator

docker-entrypoint.sh

SIGTERM/SIGINT is only honoured between tasks: POSIX defers trap execution while a foreground command is running…

✅ Fixed in commit 8907c19 — loop tasks now run in the background with wait, and the TERM/INT trap kills the current child (task or sleep) before exiting, so shutdown is prompt instead of deferred to task completion. Smoke-tested with SIGTERM during both a running fetch and the sleep phase — exits immediately, exit code 0.

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3559) > SIGTERM/SIGINT is only honoured between tasks: POSIX defers trap execution while a foreground command is running… ✅ Fixed in commit `8907c19` — loop tasks now run in the background with `wait`, and the TERM/INT trap kills the current child (`task` or `sleep`) before exiting, so shutdown is prompt instead of deferred to task completion. Smoke-tested with SIGTERM during both a running `fetch` and the sleep phase — exits immediately, exit code 0.
Author
Collaborator

Duplicate of wave-1 finding (already escalated, awaiting Marvin8)

Containerfile base image digest

Same substance as the digest-pinning group answered in the wave-1 reply (new marker only because the diff changed): both stages intentionally mirror cang's mutable-tag layout; digest pinning is Marvin8's convention call. No further action.

### Duplicate of wave-1 finding (already escalated, awaiting Marvin8) [`Containerfile` base image digest](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3557) Same substance as the digest-pinning group answered in [the wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3549) (new marker only because the diff changed): both stages intentionally mirror cang's mutable-tag layout; digest pinning is Marvin8's convention call. No further action.
agent-pi force-pushed feat/issue-40-container-packaging from 8907c198b6
All checks were successful
/ gitleaks (pull_request) Successful in 23s
/ checks (pull_request) Successful in 1m48s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m52s
/ container (pull_request) Successful in 1m31s
to 410373153e
All checks were successful
/ gitleaks (pull_request) Successful in 19s
/ checks (pull_request) Successful in 1m54s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m21s
/ container (pull_request) Successful in 1m25s
2026-09-25 08:40:16 +00:00
Compare
forgejo-actions left a comment

WuMing

Found 9 issue(s). See inline comments below.

## WuMing Found **9** issue(s). See inline comments below.
@ -60,0 +66,4 @@
needs: [gitleaks, checks]
if: github.event_name != 'release'
steps:
- uses: actions/checkout@v4

security [MEDIUM]

A08: actions/checkout@v4 is pinned only to a mutable major tag, not a full commit SHA. This job runs with registry credentials, so a compromised or malicious update to the v4 tag could exfiltrate those secrets. Pin the action to a reviewed commit SHA and use Action pinning.

**security** [MEDIUM] A08: actions/checkout@v4 is pinned only to a mutable major tag, not a full commit SHA. This job runs with registry credentials, so a compromised or malicious update to the v4 tag could exfiltrate those secrets. Pin the action to a reviewed commit SHA and use Action pinning. <!-- wuming:sha256:fc9c6ce9837e58ce45b70f3bdf23d193134ad2e837d30a8f73db3ce4a0924413 -->
marvin8 marked this conversation as resolved
@ -60,0 +73,4 @@
env:
REVISION: ${{ github.sha }}
run: |
UV_VERSION=$(grep -oP '"uv~=\K[0-9]+\.[0-9]+\.[0-9]+' pyproject.toml)

config [LOW]

UV_VERSION is scraped with grep -oP '"uv~=\K...'. If the pin in pyproject.toml is ever written differently (uv==0.12.19, single quotes, spacing), grep exits non-zero and, because run steps execute under bash -e, the step aborts with a confusing error; if the failure is tolerated the build silently receives an empty --build-arg UV_VERSION. Add a fallback/validation (UV_VERSION=$(grep -oP ... || true) plus an explicit non-empty check) and reuse one helper instead of duplicating the same grep in the publish job at line 108.

**config** [LOW] `UV_VERSION` is scraped with `grep -oP '"uv~=\K...'`. If the pin in pyproject.toml is ever written differently (`uv==0.12.19`, single quotes, spacing), grep exits non-zero and, because run steps execute under `bash -e`, the step aborts with a confusing error; if the failure is tolerated the build silently receives an empty `--build-arg UV_VERSION`. Add a fallback/validation (`UV_VERSION=$(grep -oP ... || true)` plus an explicit non-empty check) and reuse one helper instead of duplicating the same grep in the publish job at line 108. <!-- wuming:sha256:c750d29ff357a37db6008be004c68c1fc293323ced566efbfc4a3e2e85fa3106 -->
marvin8 marked this conversation as resolved
@ -60,0 +98,4 @@
- name: Install buildah
run: apt-get update -qq && apt-get install -y -qq podman buildah
- name: Login to registry
run: printf '%s' "${{ secrets.registry_password }}" | buildah login --username "${{ secrets.registry_user }}" --password-stdin registry.marvin8.zone

config [MEDIUM]

Registry credentials are interpolated directly into the shell script with ${{ secrets.* }}. Any quote, backtick or $(...) in the password will break the command or allow shell injection on the self-hosted runner. Pass the values through env: and reference "$REGISTRY_USER" / "$REGISTRY_PASSWORD" instead. Also, buildah login writes the credentials into the runner's containers auth file, which persists after the job on a self-hosted runner such as zhuiri; finish the job with buildah logout registry.marvin8.zone or use a throwaway --authfile under $RUNNER_TEMP.

**config** [MEDIUM] Registry credentials are interpolated directly into the shell script with `${{ secrets.* }}`. Any quote, backtick or `$(...)` in the password will break the command or allow shell injection on the self-hosted runner. Pass the values through `env:` and reference `"$REGISTRY_USER"` / `"$REGISTRY_PASSWORD"` instead. Also, `buildah login` writes the credentials into the runner's containers auth file, which persists after the job on a self-hosted runner such as `zhuiri`; finish the job with `buildah logout registry.marvin8.zone` or use a throwaway `--authfile` under `$RUNNER_TEMP`. <!-- wuming:sha256:f4b9a0d7d0c511a33c9723939cf8567605a1dfe0ec4126048ec98008048ec280 -->
marvin8 marked this conversation as resolved
@ -60,0 +111,4 @@
--build-arg REVISION="${REVISION}" \
--build-arg VERSION="${VERSION}" \
--build-arg UV_VERSION="${UV_VERSION}" \
--annotation "org.opencontainers.image.source=https://forge.marvin8.zone/marvin8/yunjin" \

config [LOW]

This is the second verbatim copy of the seven --annotation flags already present in the container job (lines 81-87); only org.opencontainers.image.version differs. The two lists will drift the next time metadata changes. Factor the shared annotations into one variable/script used by both build steps, or set them as LABELs in the Containerfile and pass only the per-build values (REVISION, VERSION) as build args.

**config** [LOW] This is the second verbatim copy of the seven `--annotation` flags already present in the `container` job (lines 81-87); only `org.opencontainers.image.version` differs. The two lists will drift the next time metadata changes. Factor the shared annotations into one variable/script used by both build steps, or set them as `LABEL`s in the Containerfile and pass only the per-build values (`REVISION`, `VERSION`) as build args. <!-- wuming:sha256:c177147292e48c34ddca7ba6abd3b5268e487991ae6493318b626cceb434ab77 -->
marvin8 marked this conversation as resolved
@ -0,0 +16,4 @@
# Stop promptly on SIGTERM/SIGINT: kill the current background child (task
# or sleep), since POSIX defers traps until the foreground wait returns.
trap 'test -n "${child_pid:-}" && kill "$child_pid" 2>/dev/null; exit 0' TERM INT

shell [MEDIUM]

The SIGTERM/SIGINT handler kills the current child and then calls exit 0 immediately without waiting for the child to exit, so in-flight work (e.g. fetch partway through writing SQLite, aggregate mid-transaction) is interrupted and the container reports a clean shutdown, hiding the truncation from the supervisor and return-code based alerting. It also only signals the direct child, not grandchildren. Reap the child after signalling, e.g. trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null; wait "$child_pid" 2>/dev/null; fi; exit 0' TERM INT, and consider forwarding the signal to the whole process group (kill -TERM 0) so spawned subprocesses also stop.

**shell** [MEDIUM] The SIGTERM/SIGINT handler kills the current child and then calls `exit 0` immediately without waiting for the child to exit, so in-flight work (e.g. `fetch` partway through writing SQLite, `aggregate` mid-transaction) is interrupted and the container reports a clean shutdown, hiding the truncation from the supervisor and return-code based alerting. It also only signals the direct child, not grandchildren. Reap the child after signalling, e.g. `trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null; wait "$child_pid" 2>/dev/null; fi; exit 0' TERM INT`, and consider forwarding the signal to the whole process group (`kill -TERM 0`) so spawned subprocesses also stop. <!-- wuming:sha256:6b56bec6178fc530c243fd875dcbfbbfe51550d8b3355f45d579f81c924c530e -->
@ -0,0 +21,4 @@
case "${1:-web}" in
web)
: "${PORT:=8080}"
exec waitress-serve --host=0.0.0.0 --port="${PORT}" yunjin.web.wsgi:app

shell [LOW]

$PORT is interpolated into the waitress command line without any validation, unlike YUNJIN_WORKER_INTERVAL a few lines below. A non-numeric or out-of-range value makes the container fail at startup with an opaque waitress usage error. Validate it before exec (reject anything not matching [1-9][0-9]* and not within 1-65535) so misconfiguration fails fast with a clear message.

**shell** [LOW] `$PORT` is interpolated into the waitress command line without any validation, unlike `YUNJIN_WORKER_INTERVAL` a few lines below. A non-numeric or out-of-range value makes the container fail at startup with an opaque waitress usage error. Validate it before `exec` (reject anything not matching `[1-9][0-9]*` and not within 1-65535) so misconfiguration fails fast with a clear message. <!-- wuming:sha256:20b0ad3294b466696287d165ad5a32a367d63d12508fa770c2b6ddaee1400502 -->
marvin8 marked this conversation as resolved
@ -0,0 +62,4 @@
child_pid=
python -m yunjin "${task}" &
child_pid=$!
if ! wait "$child_pid"; then

shell [LOW]

Every task failure is swallowed with only a log line and the loop continues forever, so a permanently failing worker (expired credentials, unreachable feeds, schema drift) still looks healthy to Docker/orchestrators and never triggers a restart. Consider tracking consecutive failures and exiting non-zero after a threshold, or writing a heartbeat/health file that a HEALTHCHECK can inspect.

**shell** [LOW] Every task failure is swallowed with only a log line and the loop continues forever, so a permanently failing worker (expired credentials, unreachable feeds, schema drift) still looks healthy to Docker/orchestrators and never triggers a restart. Consider tracking consecutive failures and exiting non-zero after a threshold, or writing a heartbeat/health file that a HEALTHCHECK can inspect. <!-- wuming:sha256:335fa58a2238c7aa74c4d444f7c9aa41cdefab7ffb0b1d94cb95cfaa3633d9d0 -->
marvin8 marked this conversation as resolved
@ -447,3 +447,3 @@
[[packages]]
name = "filelock"
version = "3.32.6"
version = "4.0.3"

config [LOW]

filelock jumps from 3.32.6 to 4.0.3, a major-version change of a transitive dependency (virtualenv/nox toolchain) pulled in by the same wholesale lock regeneration. Verify that no dependent package constrains filelock<4 (otherwise the lock is unbuildable for other environments) and consider splitting the unrelated upgrade out of this container PR.

**config** [LOW] `filelock` jumps from 3.32.6 to 4.0.3, a major-version change of a transitive dependency (virtualenv/nox toolchain) pulled in by the same wholesale lock regeneration. Verify that no dependent package constrains `filelock<4` (otherwise the lock is unbuildable for other environments) and consider splitting the unrelated upgrade out of this container PR. <!-- wuming:sha256:2ecdc463bb30e0b20b279df03707d028731cb3fca6c44446ffe02b4ef82c4497 -->
@ -1403,3 +1347,3 @@
[[packages]]
name = "sqlalchemy"
version = "2.0.52"
version = "2.1.0"

config [MEDIUM]

SQLAlchemy is bumped 2.0.52 -> 2.1.0, a major runtime-dependency upgrade bundled into a container-packaging change (the same regeneration also drops the conditional greenlet pin). Major runtime bumps deserve their own PR/verification: confirm the application's ORM usage and migrations are compatible with 2.1 and that removing greenlet is intentional, rather than shipping it silently inside a lock refresh.

**config** [MEDIUM] SQLAlchemy is bumped 2.0.52 -> 2.1.0, a major runtime-dependency upgrade bundled into a container-packaging change (the same regeneration also drops the conditional `greenlet` pin). Major runtime bumps deserve their own PR/verification: confirm the application's ORM usage and migrations are compatible with 2.1 and that removing `greenlet` is intentional, rather than shipping it silently inside a lock refresh. <!-- wuming:sha256:75bdb510ba983e189366332e93eb58338c5f3507bbfda45fc2b2a58fec993e2f -->
docker-entrypoint.sh -> container-entrypoint.sh,
.dockerignore -> .containerignore (buildah/podman read it natively);
in-image path /usr/local/bin/container-entrypoint.sh.
✨ Include extract-media in the worker loop
All checks were successful
/ gitleaks (pull_request) Successful in 20s
/ checks (pull_request) Successful in 1m54s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m35s
/ container (pull_request) Successful in 1m28s
fd7f836833
Re-runs are incremental (media_url_exists skips already-stored
media per article), so periodic passes mostly re-scan and fetch
only new images. Completes the issue #40 goal of keeping articles,
images, and metadata up to date.
forgejo-actions left a comment

WuMing

Found 7 issue(s). See inline comments below.

## WuMing Found **7** issue(s). See inline comments below.
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder

shell [MEDIUM]

Both stages reference base images by mutable tag (python:3.12-slim-bookworm here and on line 18, ghcr.io/astral-sh/uv:${UV_VERSION} on line 4). Tags can be re-pointed, so a rebuild may silently pick up different (or compromised) content and the image is not reproducible. Pin each to an immutable digest (FROM python:3.12-slim-bookworm@sha256:<digest> AS builder) and bump the digest deliberately, e.g. via a Renovate/Dependabot rule.

**shell** [MEDIUM] Both stages reference base images by mutable tag (`python:3.12-slim-bookworm` here and on line 18, `ghcr.io/astral-sh/uv:${UV_VERSION}` on line 4). Tags can be re-pointed, so a rebuild may silently pick up different (or compromised) content and the image is not reproducible. Pin each to an immutable digest (`FROM python:3.12-slim-bookworm@sha256:<digest> AS builder`) and bump the digest deliberately, e.g. via a Renovate/Dependabot rule. <!-- wuming:sha256:4115a945ccc60c785ba0454e2a20a9735949444c1d1a228d93a186a384b6e014 -->
marvin8 marked this conversation as resolved
@ -0,0 +1,58 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION=0.12.18
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv

security [MEDIUM]

A08: The uv binary is copied from a mutable image tag without an immutable digest or checksum verification. If the tag is republished, an attacker can control the build tool and poison the resulting image. Pin this reference by digest, e.g. ghcr.io/astral-sh/uv@sha256:...

**security** [MEDIUM] A08: The uv binary is copied from a mutable image tag without an immutable digest or checksum verification. If the tag is republished, an attacker can control the build tool and poison the resulting image. Pin this reference by digest, e.g. ghcr.io/astral-sh/uv@sha256:... <!-- wuming:sha256:c907f55beec313e2cd4a56d7090f7e49b21b89a7a9d850b4bc3ff90addbb0ac7 -->
marvin8 marked this conversation as resolved
@ -0,0 +15,4 @@
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim-bookworm

security [MEDIUM]

A08: The runtime base image is referenced by the floating tag python:3.12-slim-bookworm rather than an immutable digest. This permits the tag to be republished with malicious content without detection. Pin the base image by digest (also applies to the builder stage at line 2).

**security** [MEDIUM] A08: The runtime base image is referenced by the floating tag python:3.12-slim-bookworm rather than an immutable digest. This permits the tag to be republished with malicious content without detection. Pin the base image by digest (also applies to the builder stage at line 2). <!-- wuming:sha256:830fa1cbeb500f3fdffcd4d6093ac2ddfdb0a5a6b583383dff0fa847c43f04ed -->
marvin8 marked this conversation as resolved
Containerfile Outdated
@ -0,0 +17,4 @@
FROM python:3.12-slim-bookworm
RUN useradd --system --no-create-home --shell /usr/sbin/nologin yunjin

shell [LOW]

useradd --system without --uid/--gid allocates a UID/GID from the system range that can change between base-image rebuilds, so the UID baked into /data is not predictable. Operators who bind-mount a host directory over /data then cannot chown it correctly and the worker/web will fail to write the SQLite db. Use an explicit, high, non-overlapping pair, e.g. RUN useradd --system --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin yunjin, and keep it documented.

**shell** [LOW] `useradd --system` without `--uid`/`--gid` allocates a UID/GID from the system range that can change between base-image rebuilds, so the UID baked into /data is not predictable. Operators who bind-mount a host directory over /data then cannot chown it correctly and the worker/web will fail to write the SQLite db. Use an explicit, high, non-overlapping pair, e.g. `RUN useradd --system --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin yunjin`, and keep it documented. <!-- wuming:sha256:f66909d225eea5a5b8f9c28f0e3c0609d04dae6f229e378e7cdaefd089aaa23e -->
@ -0,0 +17,4 @@
# Stop promptly on SIGTERM/SIGINT: kill the current background child (task
# or sleep), since POSIX defers traps until the foreground wait returns.
trap 'test -n "${child_pid:-}" && kill "$child_pid" 2>/dev/null; exit 0' TERM INT

shell [LOW]

The handler kills the current child but never waits for it, so the script exits (status 0) while a yunjin fetch/aggregate task may still be running/aborting. In addition, under set -eu the test ... && kill ... AND-list is the last command of that list; if kill fails (child already gone) the list returns non-zero and set -e aborts the trap before exit 0 is reached. Use an explicit block, e.g. trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null || true; wait "$child_pid" 2>/dev/null || true; fi; exit 0' TERM INT.

**shell** [LOW] The handler kills the current child but never `wait`s for it, so the script exits (status 0) while a `yunjin fetch`/`aggregate` task may still be running/aborting. In addition, under `set -eu` the `test ... && kill ...` AND-list is the last command of that list; if `kill` fails (child already gone) the list returns non-zero and `set -e` aborts the trap before `exit 0` is reached. Use an explicit block, e.g. `trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null || true; wait "$child_pid" 2>/dev/null || true; fi; exit 0' TERM INT`. <!-- wuming:sha256:4d2df7253d974e7032f5c6faa195dfa3e7abe2de2e9a4bb1808fe9fecb71882d -->
marvin8 marked this conversation as resolved
@ -0,0 +22,4 @@
case "${1:-web}" in
web)
: "${PORT:=8080}"
exec waitress-serve --host=0.0.0.0 --port="${PORT}" yunjin.web.wsgi:app

shell [LOW]

Only the worker branch runs yunjin init; the default web branch goes straight to waitress. With a fresh /data volume (e.g. docker run <image>, which uses the default web CMD, or a web-only deployment) the app will start against an unmigrated/empty SQLite database and fail on the first query, with no indication that a worker container must run first. Consider running the same retrying init before serving in the web branch, or document/enforce the start order (and add a compose/healthcheck dependency).

**shell** [LOW] Only the `worker` branch runs `yunjin init`; the default `web` branch goes straight to waitress. With a fresh `/data` volume (e.g. `docker run <image>`, which uses the default `web` CMD, or a web-only deployment) the app will start against an unmigrated/empty SQLite database and fail on the first query, with no indication that a worker container must run first. Consider running the same retrying `init` before serving in the `web` branch, or document/enforce the start order (and add a compose/healthcheck dependency). <!-- wuming:sha256:6b5ca936a13f854477d6602c48ca0d968beab037cf5f08d875b4297ef53a2e67 -->
marvin8 marked this conversation as resolved
@ -0,0 +58,4 @@
exit 1
fi
while :; do
for task in fetch aggregate extract-tags extract-media; do

shell [MEDIUM]

The loop runs extract-media every interval, which contradicts the PR description ("extract-media is deliberately not in the loop (heavy network use each interval)") and the stated verification run (init -> fetch -> aggregate -> extract-tags). As written, every container will hit the origin sites for media downloads on each tick, which is exactly the behaviour the PR says was excluded. Drop extract-media from this list (and fix the header comment on lines 7-8) or make it an opt-in via an env var such as ${YUNJIN_WORKER_TASKS:-fetch aggregate extract-tags}.

**shell** [MEDIUM] The loop runs `extract-media` every interval, which contradicts the PR description ("extract-media is deliberately not in the loop (heavy network use each interval)") and the stated verification run (init -> fetch -> aggregate -> extract-tags). As written, every container will hit the origin sites for media downloads on each tick, which is exactly the behaviour the PR says was excluded. Drop `extract-media` from this list (and fix the header comment on lines 7-8) or make it an opt-in via an env var such as `${YUNJIN_WORKER_TASKS:-fetch aggregate extract-tags}`. <!-- wuming:sha256:3ce1564d54e475ed7f455de98df86a2b92100f137ae6ea71b12d253f8909431f -->
marvin8 marked this conversation as resolved
🐛 Address WuMing review waves 4/5 on PR #46
All checks were successful
/ gitleaks (pull_request) Successful in 15s
/ checks (pull_request) Successful in 1m57s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m25s
/ container (pull_request) Successful in 1m29s
74d4a27a50
- registry credentials passed via env vars instead of being
  interpolated into the run script (injection/trace safety)
- UV_VERSION grep guarded: empty result fails the step with a
  clear message instead of a silent empty --build-arg
- drop the --annotation lists from both build jobs: the
  Containerfile LABELs already carry all OCI metadata
- TERM/INT trap: guard kill failure against set -e and wait for
  the killed child before exiting
- PORT validated (integer 1-65535) before exec waitress-serve
- yunjin user gets fixed uid/gid 1000 for predictable /data
  ownership on bind mounts
Author
Collaborator

.forgejo/workflows/ci.yml

Registry credentials are interpolated directly into the shell script…

✅ Fixed in commit 74d4a27 — login step now passes REGISTRY_USER/REGISTRY_PASSWORD through env: and references them as quoted shell variables, so special characters in the password can't break or inject into the command.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3581) > Registry credentials are interpolated directly into the shell script… ✅ Fixed in commit `74d4a27` — login step now passes `REGISTRY_USER`/`REGISTRY_PASSWORD` through `env:` and references them as quoted shell variables, so special characters in the password can't break or inject into the command.
Author
Collaborator

.forgejo/workflows/ci.yml

UV_VERSION is scraped with grep -oP …; if the pin in pyproject.toml is ever written differently…

✅ Fixed in commit 74d4a27 — both build jobs now tolerate the grep failure and abort with an explicit unable to determine uv version from pyproject.toml message instead of passing a silent empty --build-arg. (Note the default ARG UV_VERSION=0.12.18 in the Containerfile still covers plain local builds.)

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3582) > `UV_VERSION` is scraped with `grep -oP …`; if the pin in pyproject.toml is ever written differently… ✅ Fixed in commit `74d4a27` — both build jobs now tolerate the grep failure and abort with an explicit `unable to determine uv version from pyproject.toml` message instead of passing a silent empty `--build-arg`. (Note the default `ARG UV_VERSION=0.12.18` in the Containerfile still covers plain local builds.)
Author
Collaborator

.forgejo/workflows/ci.yml

This is the second verbatim copy of the seven --annotation flags…

✅ Fixed in commit 74d4a27 by taking your second option: the redundant --annotation lists are dropped from both jobs — the Containerfile LABELs already carry all OCI metadata including revision/version via build args, so the jobs now pass only their per-build --build-args.

[`.forgejo/workflows/ci.yml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3583) > This is the second verbatim copy of the seven `--annotation` flags… ✅ Fixed in commit `74d4a27` by taking your second option: the redundant `--annotation` lists are dropped from both jobs — the Containerfile `LABEL`s already carry all OCI metadata including `revision`/`version` via build args, so the jobs now pass only their per-build `--build-arg`s.
Author
Collaborator

docker-entrypoint.sh

The SIGTERM/SIGINT handler kills the current child and then calls exit 0 immediately without waiting…

✅ Fixed in commit 74d4a27 — the trap now (a) guards the kill with || : so a vanished child can't abort the trap through set -e before exit 0, and (b) waits for the killed child before exiting. (Path renamed to container-entrypoint.sh in the meantime.)

[`docker-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3586) > The SIGTERM/SIGINT handler kills the current child and then calls `exit 0` immediately without waiting… ✅ Fixed in commit `74d4a27` — the trap now (a) guards the `kill` with `|| :` so a vanished child can't abort the trap through `set -e` before `exit 0`, and (b) `wait`s for the killed child before exiting. (Path renamed to `container-entrypoint.sh` in the meantime.)
Author
Collaborator

container-entrypoint.sh

The handler kills the current child but never waits for it… under set -eu the test … && kill … AND-list…

✅ Same finding as comment 3586 (both the missing reap and the set -e AND-list hazard) — fixed in commit 74d4a27; see the reply there.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3595) > The handler kills the current child but never `wait`s for it… under `set -eu` the `test … && kill …` AND-list… ✅ Same finding as comment 3586 (both the missing reap and the `set -e` AND-list hazard) — fixed in commit `74d4a27`; see the reply there.
Author
Collaborator

container-entrypoint.sh

$PORT is interpolated into the waitress command line without any validation…

✅ Fixed in commit 74d4a27 — PORT is validated before exec (digits only, then range 1–65535) with the same fail-fast message style as YUNJIN_WORKER_INTERVAL. Smoke-tested: abc, 0, 70000, -1 all exit 1; a valid port boots and serves.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3587) > `$PORT` is interpolated into the waitress command line without any validation… ✅ Fixed in commit `74d4a27` — `PORT` is validated before `exec` (digits only, then range 1–65535) with the same fail-fast message style as `YUNJIN_WORKER_INTERVAL`. Smoke-tested: `abc`, `0`, `70000`, `-1` all exit 1; a valid port boots and serves.
Author
Collaborator

Containerfile

useradd --system without --uid/--gid allocates a UID/GID from the system range…

✅ Fixed in commit 74d4a27 — yunjin now gets an explicit uid/gid 1000 (groupadd --system --gid 1000 + useradd --system --uid 1000), so /data ownership is stable across base-image rebuilds and predictable for bind mounts.

[`Containerfile`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3594) > `useradd --system` without `--uid`/`--gid` allocates a UID/GID from the system range… ✅ Fixed in commit `74d4a27` — `yunjin` now gets an explicit uid/gid 1000 (`groupadd --system --gid 1000` + `useradd --system --uid 1000`), so `/data` ownership is stable across base-image rebuilds and predictable for bind mounts.
Author
Collaborator

pylock.toml

SQLAlchemy is bumped 2.0.52 -> 2.1.0, a major runtime-dependency upgrade bundled into a container-packaging change…

🔴 Not actioned — the lock regeneration is this repo's prescribed workflow: the AGENTS tooling table mandates uv sync -U --all-groups after any dependency change, and -U re-resolves the whole lock (same for the filelock jump in comment 3585). Compatibility is verified, not assumed: the full suite — including the alembic migration path and every nox session that exercises virtualenv/filelock — passes on 3.12, 3.13, and 3.14 in CI. If you'd rather scope lock churn out of feature PRs, that's a workflow convention change to make repo-wide, not here.

[`pylock.toml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3584) > SQLAlchemy is bumped 2.0.52 -> 2.1.0, a major runtime-dependency upgrade bundled into a container-packaging change… 🔴 Not actioned — the lock regeneration is this repo's prescribed workflow: the AGENTS tooling table mandates `uv sync -U --all-groups` after any dependency change, and `-U` re-resolves the whole lock (same for the `filelock` jump in comment 3585). Compatibility is verified, not assumed: the full suite — including the alembic migration path and every nox session that exercises virtualenv/filelock — passes on 3.12, 3.13, and 3.14 in CI. If you'd rather scope lock churn out of feature PRs, that's a workflow convention change to make repo-wide, not here.
Author
Collaborator

pylock.toml

filelock jumps from 3.32.6 to 4.0.3, a major-version change of a transitive dependency…

🔴 Same answer as comment 3584: wholesale -U re-resolution is the repo's prescribed post-dependency-change workflow, and filelock 4.0.3 is exercised by every nox/virtualenv session in the green CI run. No action.

[`pylock.toml`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3585) > `filelock` jumps from 3.32.6 to 4.0.3, a major-version change of a transitive dependency… 🔴 Same answer as comment 3584: wholesale `-U` re-resolution is the repo's prescribed post-dependency-change workflow, and `filelock` 4.0.3 is exercised by every nox/virtualenv session in the green CI run. No action.
Author
Collaborator

container-entrypoint.sh

The loop runs extract-media every interval, which contradicts the PR description…

🔴 Intentional and owner-directed: after the original review rounds, Marvin8 asked for extract-media to be added to the loop (it completes the issue goal of keeping articles, images, and metadata up to date). Re-runs are incremental — the extractor skips media whose URL is already stored per article (media_url_exists) — and outbound requests remain gated by YUNJIN_HTTP_MIN_HOST_INTERVAL. The PR body was updated to match.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3596) > The loop runs `extract-media` every interval, which contradicts the PR description… 🔴 Intentional and owner-directed: after the original review rounds, Marvin8 asked for extract-media to be added to the loop (it completes the issue goal of keeping articles, images, and metadata up to date). Re-runs are incremental — the extractor skips media whose URL is already stored per article (`media_url_exists`) — and outbound requests remain gated by `YUNJIN_HTTP_MIN_HOST_INTERVAL`. The PR body was updated to match.
Author
Collaborator

container-entrypoint.sh

Every task failure is swallowed with only a log line and the loop continues forever…

❓ Over to Marvin8 — the logged-not-fatal behavior is what the issue plan specifies, and exiting on failure would reintroduce exactly the crash-looping an earlier finding asked to avoid. A consecutive-failure threshold (e.g. exit after N full cycles where every task failed) or a heartbeat file for a HEALTHCHECK are both easy to add if you want them — say which, or leave as-is.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3588) > Every task failure is swallowed with only a log line and the loop continues forever… ❓ Over to Marvin8 — the logged-not-fatal behavior is what the issue plan specifies, and exiting on failure would reintroduce exactly the crash-looping an earlier finding asked to avoid. A consecutive-failure threshold (e.g. exit after N full cycles where every task failed) or a heartbeat file for a HEALTHCHECK are both easy to add if you want them — say which, or leave as-is.
forgejo-actions left a comment

WuMing

Found 7 issue(s). See inline comments below.

## WuMing Found **7** issue(s). See inline comments below.
@ -60,0 +66,4 @@
needs: [gitleaks, checks]
if: github.event_name != 'release'
steps:
- uses: actions/checkout@v4

security [LOW]

GitHub Action is referenced by mutable version tag 'v4' rather than a commit SHA. A compromised action tag could execute malicious code in the CI pipeline. Pin actions/checkout to the full-length commit SHA (A08).

**security** [LOW] GitHub Action is referenced by mutable version tag 'v4' rather than a commit SHA. A compromised action tag could execute malicious code in the CI pipeline. Pin actions/checkout to the full-length commit SHA (A08). <!-- wuming:sha256:371deef7f3133b53d2e6b5e9922b74d54145ea4a0cc9ee05f2e05440ddc9b032 -->
marvin8 marked this conversation as resolved
@ -60,0 +91,4 @@
needs: [gitleaks, checks]
if: github.event_name == 'release' && !github.event.release.prerelease
steps:
- uses: actions/checkout@v4

security [LOW]

GitHub Action is referenced by mutable version tag 'v4' rather than a commit SHA. A compromised action tag could execute malicious code in the release publishing pipeline. Pin actions/checkout to the full-length commit SHA (A08).

**security** [LOW] GitHub Action is referenced by mutable version tag 'v4' rather than a commit SHA. A compromised action tag could execute malicious code in the release publishing pipeline. Pin actions/checkout to the full-length commit SHA (A08). <!-- wuming:sha256:85d49dc200fab27964399c7ca9b1799a6824a01c0885963de4b65221df2dfa22 -->
marvin8 marked this conversation as resolved
@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder

shell [LOW]

Base images are pinned only by mutable tag, not digest. python:3.12-slim-bookworm (here and again at line 18) and ghcr.io/astral-sh/uv:${UV_VERSION} (line 4) can be re-tagged upstream, so builds are not reproducible and a compromised upstream tag would be pulled silently. Pin to a digest (e.g. python:3.12-slim-bookworm@sha256:...) or, at minimum, pin the uv image by digest and document the update process.

**shell** [LOW] Base images are pinned only by mutable tag, not digest. `python:3.12-slim-bookworm` (here and again at line 18) and `ghcr.io/astral-sh/uv:${UV_VERSION}` (line 4) can be re-tagged upstream, so builds are not reproducible and a compromised upstream tag would be pulled silently. Pin to a digest (e.g. `python:3.12-slim-bookworm@sha256:...`) or, at minimum, pin the uv image by digest and document the update process. <!-- wuming:sha256:b4c573107b86c2d24eedb403e850ebffb080e1342a37a50fd40ec6bcf46de948 -->

security [MEDIUM]

Base image is referenced by a mutable tag without a content digest (also repeated for the runtime stage at line 18). A compromised registry or upstream tag update could silently inject malicious code into the image. Pin the image to a digest, e.g. python:3.12-slim-bookworm@sha256:... (A08).

**security** [MEDIUM] Base image is referenced by a mutable tag without a content digest (also repeated for the runtime stage at line 18). A compromised registry or upstream tag update could silently inject malicious code into the image. Pin the image to a digest, e.g. python:3.12-slim-bookworm@sha256:... (A08). <!-- wuming:sha256:b4c573107b86c2d24eedb403e850ebffb080e1342a37a50fd40ec6bcf46de948 -->
marvin8 marked this conversation as resolved
@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION=0.12.18
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv

security [MEDIUM]

The uv binary is copied from a mutable container tag without a digest. This provides no integrity guarantee that the fetched binary is the expected, untampered artifact. Pin to a digest, e.g. ghcr.io/astral-sh/uv@sha256: (A08).

**security** [MEDIUM] The uv binary is copied from a mutable container tag without a digest. This provides no integrity guarantee that the fetched binary is the expected, untampered artifact. Pin to a digest, e.g. ghcr.io/astral-sh/uv@sha256:<digest> (A08). <!-- wuming:sha256:ccac9c0f8cd5637289f6a0e86b6878b7af1c1c85b10581fae1b3d114b20d5525 -->
marvin8 marked this conversation as resolved
@ -0,0 +18,4 @@
# Stop promptly on SIGTERM/SIGINT: kill the current background child (task
# or sleep), since POSIX defers traps until the foreground wait returns, then
# reap it before exiting so shutdown status reflects the child's termination.
trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null || :; wait "$child_pid" 2>/dev/null || :; fi; exit 0' TERM INT

shell [LOW]

The TERM/INT trap always exit 0, so a container stopped while a fetch/aggregate task was killed mid-run still reports a successful exit status; the comment above it ('shutdown status reflects the child's termination') is not what the code does. Capture the child's status from wait and propagate it (e.g. exit $? or [ $st -eq 0 ] || exit 143) if you want the runtime/podman stop to surface a killed task. Also note only the direct child is killed, not any grandchildren it may spawn.

**shell** [LOW] The TERM/INT trap always `exit 0`, so a container stopped while a fetch/aggregate task was killed mid-run still reports a successful exit status; the comment above it ('shutdown status reflects the child's termination') is not what the code does. Capture the child's status from `wait` and propagate it (e.g. `exit $?` or `[ $st -eq 0 ] || exit 143`) if you want the runtime/`podman stop` to surface a killed task. Also note only the direct child is killed, not any grandchildren it may spawn. <!-- wuming:sha256:82cce59984c7823062c61c9ec50422a886183dca7cfe8905b92a9faa7f25c7c6 -->
marvin8 marked this conversation as resolved
@ -0,0 +55,4 @@
# worker; retry briefly, then fail loudly on persistent errors.
init_ok=0
for attempt in 1 2 3; do
if python -m yunjin init; then

shell [MEDIUM]

This init runs as a foreground command, and POSIX shells defer trap handlers until the currently executing foreground command returns. A slow/hung yunjin init (or the foreground sleep 5 on line 64) therefore blocks SIGTERM/SIGINT handling entirely, so the container will be SIGKILLed at the stop grace period instead of shutting down promptly, contradicting the 'SIGTERM exits promptly' design claim and risking a partially migrated SQLite db. Run init in the background and wait on it (like the task loop does) or wrap it with a bounded timeout so the trap can fire.

**shell** [MEDIUM] This init runs as a foreground command, and POSIX shells defer trap handlers until the currently executing foreground command returns. A slow/hung `yunjin init` (or the foreground `sleep 5` on line 64) therefore blocks SIGTERM/SIGINT handling entirely, so the container will be SIGKILLed at the stop grace period instead of shutting down promptly, contradicting the 'SIGTERM exits promptly' design claim and risking a partially migrated SQLite db. Run init in the background and `wait` on it (like the task loop does) or wrap it with a bounded timeout so the trap can fire. <!-- wuming:sha256:73167fefa619ae3901dd23616c7fbf8ebcbbb88d040a43812786dde49d9987d2 -->
marvin8 marked this conversation as resolved
Author
Collaborator

Duplicates of already-answered findings (waves 4/5)

Pinning: ci.yml checkout · pinning: Containerfile builder · pinning: uv image · pinning: runtime base · web-init (container-entrypoint.sh)

  • The digest/SHA-pinning group was escalated in wave 1 and Marvin8 has decided to defer pinning to a later effort — no action until then.
  • The web-init finding is the same one declined with evidence in the wave-1 reply: create_app() runs the idempotent init_db() at import time, before waitress accepts a request; verified on a fresh volume. No worker-first ordering requirement exists.

No further action.

### Duplicates of already-answered findings (waves 4/5) [Pinning: `ci.yml` checkout](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3589) · [pinning: `Containerfile` builder](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3593) · [pinning: uv image](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3598) · [pinning: runtime base](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3599) · [web-init (`container-entrypoint.sh`)](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3597) - The digest/SHA-pinning group was escalated in wave 1 and Marvin8 has decided to **defer pinning to a later effort** — no action until then. - The web-init finding is the same one declined with evidence in [the wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3548): `create_app()` runs the idempotent `init_db()` at import time, before waitress accepts a request; verified on a fresh volume. No worker-first ordering requirement exists. No further action.
🐛 Run worker init in the background so it stays signal-killable
All checks were successful
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Successful in 1m57s
/ publish-container (pull_request) Has been skipped
/ container (pull_request) Successful in 1m25s
/ pr-review (pull_request) Successful in 3m43s
8778a04a95
WuMing wave 6: init ran as a foreground command, and POSIX defers
trap handlers until the foreground command returns, so a slow or
hung init (or its retry sleep) blocked SIGTERM handling entirely.
init_database now runs as a background child under the same
trap as worker tasks. Also fix the trap comment: signal-driven
shutdown deliberately reports success (container stop convention);
interrupted work re-runs on next start.
Author
Collaborator

container-entrypoint.sh

This init runs as a foreground command, and POSIX shells defer trap handlers until the currently executing foreground command returns…

✅ Fixed in commit 8778a04 — the init retry loop moved into an init_database function run as a background child under the same child_pid mechanism as worker tasks, so a slow/hung init (or its retry sleep) is killed promptly on SIGTERM/SIGINT instead of deferring the trap. Verified against a read-only /data: SIGTERM during the retry sleep exits immediately; without a signal, three attempts still end in a logged give-up with exit 1.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3615) > This init runs as a foreground command, and POSIX shells defer trap handlers until the currently executing foreground command returns… ✅ Fixed in commit `8778a04` — the init retry loop moved into an `init_database` function run as a background child under the same `child_pid` mechanism as worker tasks, so a slow/hung init (or its retry `sleep`) is killed promptly on SIGTERM/SIGINT instead of deferring the trap. Verified against a read-only `/data`: SIGTERM during the retry sleep exits immediately; without a signal, three attempts still end in a logged give-up with exit 1.
Author
Collaborator

container-entrypoint.sh

The TERM/INT trap always exit 0, so a container stopped while a fetch/aggregate task was killed mid-run still reports a successful exit status; the comment above it is not what the code does…

✅ Fixed the comment in commit 8778a04 — you're right that the code doesn't propagate the child's status. That's deliberate: exiting non-zero on a signal-driven stop is conventionally treated by orchestrators as a crash, and the worker's semantics are at-least-once (an interrupted task simply re-runs on the next start). The comment now states this explicitly instead of implying propagation. If you want status propagation instead, it's a one-line change — say so.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3616) > The TERM/INT trap always `exit 0`, so a container stopped while a fetch/aggregate task was killed mid-run still reports a successful exit status; the comment above it is not what the code does… ✅ Fixed the comment in commit `8778a04` — you're right that the code doesn't propagate the child's status. That's deliberate: exiting non-zero on a signal-driven stop is conventionally treated by orchestrators as a crash, and the worker's semantics are at-least-once (an interrupted task simply re-runs on the next start). The comment now states this explicitly instead of implying propagation. If you want status propagation instead, it's a one-line change — say so.
Author
Collaborator

container-entrypoint.sh

Every task failure is swallowed with only a log line and the loop continues forever…

🔴 Resolved by the repo owner: a permanently failing worker should just log and keep running — no consecutive-failure threshold and no stop. The logged-not-fatal behavior is the intended design; closing this.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3588) > Every task failure is swallowed with only a log line and the loop continues forever… 🔴 Resolved by the repo owner: a permanently failing worker should just log and keep running — no consecutive-failure threshold and no stop. The logged-not-fatal behavior is the intended design; closing this.
Author
Collaborator

Duplicates of the deferred pinning findings (waves 4–6)

Containerfile base digests · Containerfile runtime base · Containerfile uv image · ci.yml checkout (build job) · ci.yml checkout (publish job)

Same substance as the digest/SHA-pinning findings escalated in wave 1 and answered in the wave-1 reply: the repo owner has decided to defer all image-digest and action-SHA pinning to a later effort. No action in this PR; these markers are tracked so repeats are recognized as duplicates.

### Duplicates of the deferred pinning findings (waves 4–6) [`Containerfile` base digests](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3614) · [`Containerfile` runtime base](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3617) · [`Containerfile` uv image](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3618) · [`ci.yml` checkout (build job)](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3619) · [`ci.yml` checkout (publish job)](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3620) Same substance as the digest/SHA-pinning findings escalated in wave 1 and answered in [the wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3549): the repo owner has decided to **defer all image-digest and action-SHA pinning to a later effort**. No action in this PR; these markers are tracked so repeats are recognized as duplicates.
forgejo-actions left a comment

WuMing

Found 5 issue(s). See inline comments below.

## WuMing Found **5** issue(s). See inline comments below.
@ -0,0 +15,4 @@
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim-bookworm

shell [LOW]

Neither the runtime base image (line 18) nor the builder base (line 2) nor the uv image (line 4) is pinned to an immutable digest, only to mutable tags (python:3.12-slim-bookworm, ghcr.io/astral-sh/uv:${UV_VERSION}). Tags can be re-pointed upstream, so the same commit can build different images and a compromised/retagged upstream would be pulled silently. Pin by digest, e.g. FROM python:3.12-slim-bookworm@sha256:<digest> and COPY --from=ghcr.io/astral-sh/uv@sha256:<digest> /uv ..., and update them deliberately (Dependabot/Renovate).

**shell** [LOW] Neither the runtime base image (line 18) nor the builder base (line 2) nor the uv image (line 4) is pinned to an immutable digest, only to mutable tags (`python:3.12-slim-bookworm`, `ghcr.io/astral-sh/uv:${UV_VERSION}`). Tags can be re-pointed upstream, so the same commit can build different images and a compromised/retagged upstream would be pulled silently. Pin by digest, e.g. `FROM python:3.12-slim-bookworm@sha256:<digest>` and `COPY --from=ghcr.io/astral-sh/uv@sha256:<digest> /uv ...`, and update them deliberately (Dependabot/Renovate). <!-- wuming:sha256:2c6f3f39660d5015b5b5f8ee783ef67fa601af5c263d3f740b95e5da87516dc0 -->
marvin8 marked this conversation as resolved
@ -0,0 +43,4 @@
LABEL org.opencontainers.image.title="yunjin"
LABEL org.opencontainers.image.url="https://forge.marvin8.zone/marvin8/yunjin"
ARG REVISION

shell [LOW]

ARG REVISION / ARG VERSION have no defaults, so labels built into the image via lines 48-49 (org.opencontainers.image.revision=$REVISION, ...version=$VERSION) are empty strings whenever the build is invoked without --build-arg — which is the normal path for local builds and for the CI container job if it does not pass them. Give them sentinel defaults (e.g. ARG REVISION=unknown, ARG VERSION=0.0.0) or fail the build when they are unset, so the OCI metadata is never blank.

**shell** [LOW] `ARG REVISION` / `ARG VERSION` have no defaults, so labels built into the image via lines 48-49 (`org.opencontainers.image.revision=$REVISION`, `...version=$VERSION`) are empty strings whenever the build is invoked without `--build-arg` — which is the normal path for local builds and for the CI `container` job if it does not pass them. Give them sentinel defaults (e.g. `ARG REVISION=unknown`, `ARG VERSION=0.0.0`) or fail the build when they are unset, so the OCI metadata is never blank. <!-- wuming:sha256:60507a6510d5ec3112b043029da06155b217fe4aefc0a513abd9b14b3f4b2b27 -->
@ -0,0 +20,4 @@
# then reap it. Signal-driven shutdown deliberately reports success (the
# conventional container stop status); interrupted work simply re-runs on
# the next start.
trap 'if [ -n "${child_pid:-}" ]; then kill "$child_pid" 2>/dev/null || :; wait "$child_pid" 2>/dev/null || :; fi; exit 0' TERM INT

shell [MEDIUM]

The TERM/INT trap only sends SIGTERM to the direct child and then blocks in wait with no timeout or escalation to SIGKILL. If a long-running task (e.g. yunjin fetch mid-network-call) ignores or is slow to handle SIGTERM, PID 1 never exits and the container hangs until the orchestrator's grace period expires and it is SIGKILLed — losing the graceful-shutdown behaviour the comment promises. Add a bounded wait plus kill -KILL fallback (e.g. kill the child's process group, sleep N seconds, then kill -KILL), or use an init such as tini.

**shell** [MEDIUM] The TERM/INT trap only sends SIGTERM to the direct child and then blocks in `wait` with no timeout or escalation to SIGKILL. If a long-running task (e.g. `yunjin fetch` mid-network-call) ignores or is slow to handle SIGTERM, PID 1 never exits and the container hangs until the orchestrator's grace period expires and it is SIGKILLed — losing the graceful-shutdown behaviour the comment promises. Add a bounded wait plus `kill -KILL` fallback (e.g. kill the child's process group, sleep N seconds, then `kill -KILL`), or use an init such as `tini`. <!-- wuming:sha256:2861f8ad05c2bfba484031e96913909a17419bebd093fbee8b3d9e46320c4c5a -->
marvin8 marked this conversation as resolved
@ -0,0 +52,4 @@
echo "yunjin: invalid PORT: '${PORT}' (integer 1-65535 required)" >&2
exit 1
fi
exec waitress-serve --host=0.0.0.0 --port="${PORT}" yunjin.web.wsgi:app

shell [MEDIUM]

Only the worker branch runs yunjin init (the alembic upgrade that creates/migrates the schema); the web branch execs waitress immediately. Starting the image with the default web entry point against a fresh /data volume (or on a first rollout where the worker has not run yet) serves the app against a missing/out-of-date database, producing 500s until a worker happens to run init. Run the same bounded init before exec waitress-serve (it is idempotent), or gate the web start on the schema and document that a worker must run first.

**shell** [MEDIUM] Only the `worker` branch runs `yunjin init` (the alembic upgrade that creates/migrates the schema); the `web` branch execs waitress immediately. Starting the image with the default `web` entry point against a fresh `/data` volume (or on a first rollout where the worker has not run yet) serves the app against a missing/out-of-date database, producing 500s until a worker happens to run init. Run the same bounded init before `exec waitress-serve` (it is idempotent), or gate the web start on the schema and document that a worker must run first. <!-- wuming:sha256:946657b1d4c6c92b70e634692f7bbc432e1a1158b5a514587550681796906a16 -->
@ -0,0 +90,4 @@
echo "yunjin worker: sleeping ${YUNJIN_WORKER_INTERVAL}s"
sleep "${YUNJIN_WORKER_INTERVAL}" &
child_pid=$!
wait "$child_pid" || :

shell [MEDIUM]

The result of the sleep wait is discarded (|| :), so any failure of sleep (e.g. a value test -gt/-lt accepted but coreutils rejects as out of range, or a test that mis-parses a leading-zero value like 08 as invalid octal and therefore skips the -lt 1 guard) is turned into a zero-length interval. The outer while : then re-runs fetch → aggregate → extract-tags → extract-media back-to-back in a tight loop, hammering the feeds and the SQLite database with no delay and no log. Check the sleep status and exit/log on failure (or validate the value with an explicit range/leading-zero check) instead of swallowing it.

**shell** [MEDIUM] The result of the sleep wait is discarded (`|| :`), so any failure of `sleep` (e.g. a value `test -gt/-lt` accepted but coreutils rejects as out of range, or a `test` that mis-parses a leading-zero value like `08` as invalid octal and therefore skips the `-lt 1` guard) is turned into a zero-length interval. The outer `while :` then re-runs fetch → aggregate → extract-tags → extract-media back-to-back in a tight loop, hammering the feeds and the SQLite database with no delay and no log. Check the sleep status and exit/log on failure (or validate the value with an explicit range/leading-zero check) instead of swallowing it. <!-- wuming:sha256:8c6de7743b9f5fa97d9f88a14ea26a7aa7b4a57f83d950ba336339ab2d50b39a -->
marvin8 marked this conversation as resolved
🔧 Cap worker interval digits and default dev labels
All checks were successful
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Successful in 1m56s
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 2m26s
/ container (pull_request) Successful in 1m27s
516500b213
WuMing wave 7:
- reject intervals longer than 9 digits: values that overflow the
  shell's integer comparison would bypass the range guard and make
  sleep fail into a busy spin; log sleep failures instead of
  discarding them
- ARG REVISION/VERSION default to 'dev' so local builds get
  honest OCI labels instead of empty strings
Author
Collaborator

container-entrypoint.sh

The result of the sleep wait is discarded… any failure of sleep… is turned [into a busy loop]…

✅ Fixed in commit 516500b on both fronts: intervals longer than 9 digits are now rejected at validation (values that would overflow the shell's integer comparison could otherwise bypass the range guard), and the sleep wait logs a sleep failed or was interrupted; resuming line instead of discarding the status. Note the octal half of the finding doesn't apply: POSIX test parses decimal, so [ "08" -lt 1 ] compares 8 — smoke-tested, 08 is accepted and sleeps 8 seconds.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3630) > The result of the sleep wait is discarded… any failure of `sleep`… is turned [into a busy loop]… ✅ Fixed in commit `516500b` on both fronts: intervals longer than 9 digits are now rejected at validation (values that would overflow the shell's integer comparison could otherwise bypass the range guard), and the sleep wait logs a `sleep failed or was interrupted; resuming` line instead of discarding the status. Note the octal half of the finding doesn't apply: POSIX `test` parses decimal, so `[ "08" -lt 1 ]` compares 8 — smoke-tested, `08` is accepted and sleeps 8 seconds.
Author
Collaborator

Containerfile

ARG REVISION / ARG VERSION have no defaults, so labels built into the image… are empty strings…

✅ Fixed in commit 516500b — both ARGs now default to dev, so local builds carry honest labels instead of empty strings; CI overrides both with the real revision/version.

[`Containerfile`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3632) > `ARG REVISION` / `ARG VERSION` have no defaults, so labels built into the image… are empty strings… ✅ Fixed in commit `516500b` — both ARGs now default to `dev`, so local builds carry honest labels instead of empty strings; CI overrides both with the real revision/version.
Author
Collaborator

container-entrypoint.sh

The TERM/INT trap only sends SIGTERM to the direct child and then blocks in wait with no timeout or escalation to SIGKILL…

🔴 Not actioned — the tasks are CPython processes, and Python installs no SIGTERM handler by default: the signal takes the OS default disposition, which terminates the process immediately (even mid-syscall); there is no Python-level path that could ignore or defer it, so a kill escalation would be dead code. (SIGINT differs — Python does default-handle it — but we never send SIGINT for container stop.)

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3629) > The TERM/INT trap only sends SIGTERM to the direct child and then blocks in `wait` with no timeout or escalation to SIGKILL… 🔴 Not actioned — the tasks are CPython processes, and Python installs no SIGTERM handler by default: the signal takes the OS default disposition, which terminates the process immediately (even mid-syscall); there is no Python-level path that could ignore or defer it, so a kill escalation would be dead code. (SIGINT differs — Python does default-handle it — but we never send SIGINT for container stop.)
Author
Collaborator

container-entrypoint.sh

Only the worker branch runs yunjin init… Starting the image with the default web entry point against a fresh /data volume…

🔴 Standing declined finding, third re-raise — see the wave-1 reply with evidence: waitress-serve yunjin.web.wsgi:app imports yunjin.web.wsgi, which builds the app at module level via create_app(); that call runs the idempotent init_db() (alembic upgrade to head + default-user seed) before the server accepts a connection. Verified on a fresh empty volume: web alone migrates the schema and serves HTTP 200. No worker-first ordering exists.

[`container-entrypoint.sh`](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3631) > Only the `worker` branch runs `yunjin init`… Starting the image with the default `web` entry point against a fresh `/data` volume… 🔴 Standing declined finding, third re-raise — see [the wave-1 reply with evidence](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3548): `waitress-serve yunjin.web.wsgi:app` imports `yunjin.web.wsgi`, which builds the app at module level via `create_app()`; that call runs the idempotent `init_db()` (alembic upgrade to head + default-user seed) before the server accepts a connection. Verified on a fresh empty volume: web alone migrates the schema and serves HTTP 200. No worker-first ordering exists.
Author
Collaborator

Duplicate of the deferred pinning finding (wave 7)

Containerfile image digests

Same substance as the digest/SHA-pinning group escalated in wave 1 (wave-1 reply, deferral consolidated here): the repo owner has deferred image-digest and action-SHA pinning to a later effort. No action in this PR.

### Duplicate of the deferred pinning finding (wave 7) [`Containerfile` image digests](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3628) Same substance as the digest/SHA-pinning group escalated in wave 1 ([wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3549), [deferral consolidated here](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3622)): the repo owner has deferred image-digest and action-SHA pinning to a later effort. No action in this PR.
forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder

security [LOW]

A08: The base Python image is referenced by a mutable tag rather than pinning to a digest. An attacker who can influence the tag or registry could cause the build to use a tampered base image. Pin to a specific digest, e.g. FROM python:3.12-slim-bookworm@sha256:...

**security** [LOW] A08: The base Python image is referenced by a mutable tag rather than pinning to a digest. An attacker who can influence the tag or registry could cause the build to use a tampered base image. Pin to a specific digest, e.g. FROM python:3.12-slim-bookworm@sha256:... <!-- wuming:sha256:7b27bb655fea3e9cb120416a803961300e30304d5b70c4363a469fee4001442b -->
marvin8 marked this conversation as resolved
@ -0,0 +1,59 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM python:3.12-slim-bookworm AS builder
ARG UV_VERSION=0.12.18
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv

security [LOW]

A08: The uv binary is copied from a remote container image referenced only by version tag, with no digest or checksum verification. This is a downloaded build artefact without an integrity check. Pin to an immutable digest or verify the binary checksum after copying.

**security** [LOW] A08: The uv binary is copied from a remote container image referenced only by version tag, with no digest or checksum verification. This is a downloaded build artefact without an integrity check. Pin to an immutable digest or verify the binary checksum after copying. <!-- wuming:sha256:3edeaa49ac3a0c12e2c18a3f78d7fdee526e4e02ef05f635cdd182bb5c804033 -->
marvin8 marked this conversation as resolved
Author
Collaborator

Duplicates of the deferred pinning finding (wave 8)

Containerfile base image · Containerfile uv image

Same substance as the digest/SHA-pinning group answered in the wave-1 reply and the deferral consolidation: the repo owner has deferred image-digest and action-SHA pinning to a later effort. No action in this PR.

### Duplicates of the deferred pinning finding (wave 8) [`Containerfile` base image](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3640) · [`Containerfile` uv image](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3641) Same substance as the digest/SHA-pinning group answered in [the wave-1 reply](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3549) and [the deferral consolidation](https://forge.marvin8.zone/marvin8/yunjin/pulls/46#issuecomment-3622): the repo owner has deferred image-digest and action-SHA pinning to a later effort. No action in this PR.
marvin8 approved these changes 2026-09-25 20:52:51 +00:00
marvin8 manually merged commit 7bcb138e2d into main 2026-09-25 20:54:01 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/yunjin!46
No description provided.