fix(ci): gate docker publish on the offline test suite

docker.yml pushed the image for tags and master without waiting for the
test job, so a red commit could still be published. ci.yml is now also a
workflow_call target; docker depends on its result. The gate run has its
own concurrency group and never cancels a superseded run, so a docs-only
follow-up cannot drop the image of the code commit before it. The live
job is skipped for the gate call.
This commit is contained in:
2026-10-14 22:10:35 +08:00
parent a7d4d1af8c
commit 7fbe144b57
3 changed files with 28 additions and 6 deletions
+12 -3
View File
@@ -34,6 +34,15 @@ on:
# Weekly probe of the live endpoints, so external API changes surface. # Weekly probe of the live endpoints, so external API changes surface.
- cron: '0 3 * * 1' - cron: '0 3 * * 1'
workflow_dispatch: workflow_dispatch:
# Called by docker.yml as the publish gate. `gate` marks that call: its
# concurrency group is separate from the standalone run on the same ref, and
# it never cancels a superseded run, because each gate run carries its own
# commit's image (a docs-only follow-up must not drop the code commit's).
workflow_call:
inputs:
gate:
type: boolean
default: false
permissions: permissions:
contents: read contents: read
@@ -41,8 +50,8 @@ permissions:
# A newer push to the same ref supersedes the older run; without this every # A newer push to the same ref supersedes the older run; without this every
# intermediate commit of a PR branch keeps a runner busy to completion. # intermediate commit of a PR branch keeps a runner busy to completion.
concurrency: concurrency:
group: ci-${{ github.ref }} group: ci-${{ inputs.gate && 'gate-' || '' }}${{ github.ref }}
cancel-in-progress: true cancel-in-progress: ${{ !inputs.gate }}
env: env:
# Panicking tests print their backtrace; free when nothing fails. # Panicking tests print their backtrace; free when nothing fails.
@@ -134,7 +143,7 @@ jobs:
live: live:
needs: test needs: test
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') if: ${{ !inputs.gate && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')) }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
continue-on-error: true continue-on-error: true
+15 -2
View File
@@ -111,14 +111,27 @@ jobs:
fi fi
echo "build=true" >> "$GITHUB_OUTPUT" echo "build=true" >> "$GITHUB_OUTPUT"
# The publish gate: the offline suite CI runs (fmt, clippy, tests, release
# build, audit), reused rather than copied, so no image is pushed for a
# commit whose tests are red. Pull requests skip it: their own CI run covers
# the code, and the docker job there only proves the image still builds.
ci:
needs: should-build
if: needs.should-build.outputs.build == 'true' && github.event_name != 'pull_request'
uses: ./.github/workflows/ci.yml
with:
gate: true
# No `actions/checkout` here on purpose: `docker/build-push-action` defaults # No `actions/checkout` here on purpose: `docker/build-push-action` defaults
# to the Git context (`https://github.com/<owner>/<repo>.git#<ref>`), so # to the Git context (`https://github.com/<owner>/<repo>.git#<ref>`), so
# BuildKit clones the repo itself and authenticates with the automatic # BuildKit clones the repo itself and authenticates with the automatic
# github.token. Adding `context: .` below without a checkout step would hand # github.token. Adding `context: .` below without a checkout step would hand
# BuildKit an empty workspace. # BuildKit an empty workspace.
docker: docker:
needs: should-build needs: [should-build, ci]
if: needs.should-build.outputs.build == 'true' # `always()`: a skipped `ci` (pull requests) must not skip this job; only a
# failed or cancelled gate does.
if: always() && needs.should-build.outputs.build == 'true' && (github.event_name == 'pull_request' || needs.ci.result == 'success')
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
+1 -1
View File
@@ -89,7 +89,7 @@ Docker: `docker build -t tgxmb .` then `docker run --rm -d --name tgxmb --env-fi
| `Dockerfile` | Multi-stage: cached dep layer via stub sources + `touch *.rs` mtime bump (cargo's freshness is mtime-based and `cargo clean -p` removes 0 files — the touch is what forces the real sources to rebuild while deps stay cached), static ffmpeg from ffmpeg.martin-riedl.de (`FFMPEG_URL` arg, optional `FFMPEG_SHA256` checksum, `unzip -t` integrity check), `debian:bookworm-slim` runtime, entrypoint. Runtime ships **no libssl/libcrypto/CA bundle** — rustls webpki-roots handles all TLS, and the static ffmpeg only processes local files (downloads go through reqwest) | | `Dockerfile` | Multi-stage: cached dep layer via stub sources + `touch *.rs` mtime bump (cargo's freshness is mtime-based and `cargo clean -p` removes 0 files — the touch is what forces the real sources to rebuild while deps stay cached), static ffmpeg from ffmpeg.martin-riedl.de (`FFMPEG_URL` arg, optional `FFMPEG_SHA256` checksum, `unzip -t` integrity check), `debian:bookworm-slim` runtime, entrypoint. Runtime ships **no libssl/libcrypto/CA bundle** — rustls webpki-roots handles all TLS, and the static ffmpeg only processes local files (downloads go through reqwest) |
|`docker-entrypoint.sh` | Privilege drop: `useradd` with `LOCAL_USER_ID` (default 9001) + `setpriv` (no gosu on bookworm-slim); rejects a non-numeric or `0` `LOCAL_USER_ID`, which would otherwise survive the drop and run the bot root | |`docker-entrypoint.sh` | Privilege drop: `useradd` with `LOCAL_USER_ID` (default 9001) + `setpriv` (no gosu on bookworm-slim); rejects a non-numeric or `0` `LOCAL_USER_ID`, which would otherwise survive the drop and run the bot root |
| `docker-compose.yml` | The deployment composition, committed as-is: every instance value (token, admins, site credentials, domain) is a `${VAR}` substitution read from the gitignored `.env` beside it, so the file needs no per-deployment edit — and a variable not listed in a service's `environment:` never reaches that container. Ships nginx-proxy + acme-companion: webhook mode needs TLS termination in front (teloxide's axum listener is HTTP-only; `WEBHOOK_CERT` only feeds `set_webhook`), bot exposes `VIRTUAL_HOST`/`VIRTUAL_PORT` on the shared `proxy` network, no host port; container names `nginx-proxy`/`acme-companion`/`tgxmb`, start order via `depends_on` (proxy → acme → bot) | | `docker-compose.yml` | The deployment composition, committed as-is: every instance value (token, admins, site credentials, domain) is a `${VAR}` substitution read from the gitignored `.env` beside it, so the file needs no per-deployment edit — and a variable not listed in a service's `environment:` never reaches that container. Ships nginx-proxy + acme-companion: webhook mode needs TLS termination in front (teloxide's axum listener is HTTP-only; `WEBHOOK_CERT` only feeds `set_webhook`), bot exposes `VIRTUAL_HOST`/`VIRTUAL_PORT` on the shared `proxy` network, no host port; container names `nginx-proxy`/`acme-companion`/`tgxmb`, start order via `depends_on` (proxy → acme → bot) |
| `.github/workflows/docker.yml` | CI: build+push to Docker Hub on tag `v*`/master, plus a build-only check on PRs touching the build inputs; **no test step**; verifies a release tag matches both crate versions; buildx gha cache (`cache-from` always, `cache-to` except on PRs, scope `tgxmb-build`, `mode=max`) so cargo deps + ffmpeg layers are restored across runs; `FFMPEG_URL`/`FFMPEG_SHA256` come from repo variables when set | | `.github/workflows/docker.yml` | CI: build+push to Docker Hub on tag `v*`/master, plus a build-only check on PRs touching the build inputs; the push is gated on `ci.yml`'s offline suite (reused via `workflow_call` with `gate: true`, so a red suite blocks the image); verifies a release tag matches both crate versions; buildx gha cache (`cache-from` always, `cache-to` except on PRs, scope `tgxmb-build`, `mode=max`) so cargo deps + ffmpeg layers are restored across runs; `FFMPEG_URL`/`FFMPEG_SHA256` come from repo variables when set |
| `README.md` | Feature docs + command table (Chinese) | | `README.md` | Feature docs + command table (Chinese) |
## Runtime/Tooling Preferences ## Runtime/Tooling Preferences