Publishing a second "Hola mundo" through the CMS broke the first one's page: it rendered the article, then a second copy of the entire site. Three silent failures lined up. Decap could not write hola-mundo.es.md twice, so it wrote hola-mundo.es-1.md. That suffix is not a language, so Hugo stopped treating the file as a Spanish sibling and translate.py's split_lang() skipped it — the post went live in Spanish alone, and no German or Portuguese was ever generated. Meanwhile permalinks used "/:slug/", and :slug falls back to the title, so both files claimed /hola-mundo/; Hugo wrote both documents into that one index.html. Nothing failed. The pipeline was green throughout. Each layer now refuses its part: post permalinks carry the year and month, the CMS prefixes new filenames with the date, and translate.py aborts on a name ending in a clash counter rather than quietly declining to translate it. The build also runs with --printPathWarnings --panicOnWarning, so any future pair of pages targeting one path fails the build instead of corrupting the output. Existing post URLs change shape (/hola-mundo/ becomes /2026/09/hola-mundo/). That costs nothing today, with one real post and no inbound links, and gets expensive to change later. Verified: 16/16 pipeline checks and 15/15 markdown checks still pass, the clash-counter name aborts with the rename instruction, and an ordinary filename still parses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
60 lines
2.4 KiB
YAML
60 lines
2.4 KiB
YAML
# Woodpecker CI: translate → build → deploy
|
|
#
|
|
# Triggered by the Gitea push webhook. The translate step replaces the old
|
|
# synchronous WordPress save_post hook: publishing in Decap is one git commit,
|
|
# translation happens here, asynchronously, and every generated sibling lands
|
|
# as a reviewable bot commit.
|
|
#
|
|
# Translation is self-hosted — OPUS-MT runs on CPU inside the image, so there
|
|
# is no API key and no third-party request. Before the first run, on the server:
|
|
# bash scripts/fetch-models.sh # -> /srv/mt-models
|
|
# docker build -t vienalatina/translate:2 docker/translate
|
|
#
|
|
# The models are mounted rather than baked in, so swapping them later does not
|
|
# mean rebuilding the image. The repo must be marked "trusted" in Woodpecker
|
|
# for this mount, which it already is for the deploy step.
|
|
#
|
|
# Secrets to configure in Woodpecker (repo settings → secrets):
|
|
# gitea_push_token — Gitea token for the translations bot user
|
|
# (translations@vienalatina.com) with repo write access
|
|
#
|
|
# The deploy step writes to the Caddy web root via a host mount, which
|
|
# requires the repo to be marked "trusted" in Woodpecker.
|
|
|
|
when:
|
|
- event: push
|
|
branch: main
|
|
|
|
steps:
|
|
translate:
|
|
image: vienalatina/translate:2
|
|
pull: false # built locally on the server, never fetched from a registry
|
|
volumes:
|
|
- /srv/mt-models:/opt/mt/models:ro
|
|
environment:
|
|
GITEA_PUSH_TOKEN:
|
|
from_secret: gitea_push_token
|
|
commands:
|
|
- python scripts/translate.py
|
|
|
|
build:
|
|
image: hugomods/hugo:0.135.0
|
|
commands:
|
|
# Bundle Decap locally so the /admin page makes zero third-party requests.
|
|
- wget -q -O static/admin/decap-cms.js https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js
|
|
# --printPathWarnings makes Hugo report two pages resolving to one output
|
|
# path; --panicOnWarning turns that report into a failed build. Without
|
|
# both, Hugo writes both documents into the same index.html and the site
|
|
# ships a page that renders as the article followed by a second copy of
|
|
# the whole site — with nothing in the CMS, the pipeline or the logs
|
|
# saying a word about it. Better a red build than a corrupted page.
|
|
- hugo --minify --printPathWarnings --panicOnWarning
|
|
|
|
deploy:
|
|
image: alpine:3.20
|
|
volumes:
|
|
- /var/www/vienalatina.com:/deploy-target
|
|
commands:
|
|
- apk add --no-cache rsync
|
|
- rsync -a --delete public/ /deploy-target/
|