Yes, it’s true: I haven’t updated this blog all summer.
Starting in mid-June, my life was consumed by an overwhelming deluge: a Google internship, three conference paper deadlines, an industry-academia collaboration, and prepping for graduate school job hunting. I basically went into a four-month hibernation (or rather, estivation) from content creation.
Over the recent National Day holiday, I finally resolved to revive the blog, kicking off a brand-new hands-on project series (Day 1 & Day 2 of my Spark on K8s portfolio). Once I finished drafting the Traditional Chinese post and happily ran git push, I eagerly anticipated the fruits of my proud automated bilingual pipeline from May to auto-translate it into fluent English.
Then I flipped over to the English version of the site.
Nothing was there. The English version looked completely frozen in time back in July.
Even more bizarrely, the GitHub Actions runs were proudly sporting a pristine row of innocent green checkmarks (✓). I had assumed the October 10th post was just “waiting in queue to be translated.” In reality, my automated pipeline had suffered a silent, catastrophic flatline.
Late into the night, I ran a comprehensive health check on the entire website. What I unearthed was a textbook cascade of CI/CD edge cases, LLM lifecycle shifts, and token permission pitfalls. Here is the full post-mortem of that debugging journey.
Pitfall 1: The Swallowed 402 Error (False Greens in CI)
Back in May, I wrote an auto_translate.py script. On every push, GitHub Actions would run this script, calling the Gemini API to translate any .zh-tw.md post missing an English .md counterpart.
When I checked why recent articles weren’t translating, I pulled open the raw GitHub Actions run logs and immediately spotted this:
Translating: content/posts/261010_2318.zh-tw.md
-> Error translating: 402 RESOURCE_EXHAUSTED.
Your prepayment credits are depleted. Please go to AI Studio...
Translating: content/posts/260612_1625.zh-tw.md
-> Error translating: 402 RESOURCE_EXHAUSTED...
Done. Translated 0 files.
My Gemini API prepaid credits had completely run dry sometime over the summer!
So why did GitHub Actions show a green checkmark? The culprit was right in my script:
try:
t_title, t_subtitle, t_desc, t_body = translate_content(...)
except Exception as e:
print(f" -> Error translating: {e}")
return False
When the API raised an exception, the script silently caught it, printed a log line, and exited cleanly with exit code 0. GitHub Actions saw the process exit with 0 and cheerfully marked the run as a success.
The Takeaway: In automated pipelines, failures in critical external dependencies (especially billing-dependent APIs) must never be swallowed silently. Either accumulate failures and fail loudly with sys.exit(1), or set up explicit alerts. A “false green” only creates a dangerous illusion that everything is fine.
Pitfall 2: Models Retire Fast in the Real World (404 NOT_FOUND)
Once the problem was identified as credit exhaustion, the initial fix seemed straightforward: generate and swap in a fresh API key.
Yet when I tested the script locally, instead of a successful translation, I was greeted with a brand-new error:
404 NOT_FOUND.
{'error': {'code': 404, 'message': 'This model models/gemini-2.5-flash is no longer available to new users. Please update your code to use models/gemini-3.8-flash for the latest features and improvements.'}}
I had to pause for a second: in just a few short months, the hardcoded gemini-2.5-flash in my code had already been phased out for new projects in favor of gemini-3.8-flash!
The speed of AI iteration is ruthless, and this was a vivid reminder of real-world LLMOps:
- Prompts degrade over time.
- Model versions get deprecated and retired.
- Hardcoded model identifiers quietly turn into technical debt.
I quickly upgraded all scripts across the repository—including the weekly trend-curating weekly_github_ai.py—to gemini-3.8-flash. Testing it out, the translation speed was noticeably snappier, and phrasing felt even more natural.
Pitfall 3: The Root Cause — GitHub Actions Recursive Trigger Guard
With the model upgraded and API credentials refreshed, the translations were finally generating locally. But that brought me back to the core mystery:
“Even back when the API was functioning properly, why did new Chinese posts never immediately show their English versions on the live site, requiring a subsequent manual commit to appear?”
This was the exact source of my confusion. Inspecting .github/workflows/ revealed the true architectural race condition.
I originally maintained two independent workflows:
hugo.yml: Triggered on push tomain, building static assets and deploying to GitHub Pages.auto_translate.yml: Triggered on push tomain, invoking Gemini for translation and committing the resulting English.mdfiles back tomainviastefanzweifel/git-auto-commit-action.
This created two critical flaws:
1. The Race Condition
When I pushed a Traditional Chinese post, both workflows triggered simultaneously. Hugo is blazingly fast, finishing checkout and site compilation in ~35 seconds. Meanwhile, the translation workflow was still establishing API connections and generating text. Consequently, Hugo deployed a build that did not yet contain the English version.
2. GITHUB_TOKEN Security Guardrails (The Real Culprit)
Once the translation workflow finished and pushed the new English markdown files back to main, shouldn’t Hugo have automatically triggered again?
The answer: Nope, not at all.
According to GitHub Actions’ foundational security specifications:
To prevent accidental infinite recursion (a workflow pushing a commit that triggers another workflow infinitely), GitHub mandates that any push event initiated via the standard
GITHUB_TOKENwill never trigger another workflow.
In other words:
- You push a Chinese post.
- Hugo immediately deploys the Chinese-only build.
- The translation action commits the English file to GitHub.
- Hugo receives no event and has no clue a new commit was pushed, so it never rebuilds.
- The English file sits quietly in the repository—invisible to the live web—until you happen to push something else days or weeks later.
The Solution: Workflow Chaining
The fix is clean and declarative. Since auto_translate.yml controls the auto-commit step, all we need to do is explicitly dispatch the Hugo workflow whenever new translations are detected and committed.
We granted the job actions: write permission and added a follow-up step using the GitHub CLI:
- name: Commit and push changes
id: auto_commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore(i18n): auto-translate new Chinese content to English"
branch: main
file_pattern: 'content/**/*.md'
- name: Trigger Hugo Deploy
if: steps.auto_commit.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run hugo.yml --ref main
Leveraging the changes_detected output from git-auto-commit-action, the Hugo deployment workflow (hugo.yml) is dispatched via gh workflow run only when new translation files were actually committed.
Now the lifecycle runs sequentially and reliably:
Push Chinese post ➔ Translation workflow runs ➔ English markdown committed ➔ Hugo deployment dispatched ➔ Bilingual site updated simultaneously!
Bonus: Housekeeping & Multilingual Polish
While overhauling the pipeline, I took the opportunity to clean up several long-standing quirks across the site:
- Eliminated Ghost Duplicate Posts: Discovered an old file named
250727_1908.md, where 2026 was mistyped as 2025. This caused two identical “On a Temporary Break” notices to sit side-by-side on the English feed. The rogue duplicate was purged. - Untracked
public/Artifacts: In the past, generated files inpublic/(such asfavicon.icoandsitemap.xml) had accidentally been tracked in git history. Runninggit rm -r --cached public/returned them to.gitignore, keeping local git state clean. - Bilingual Search Input: The global search placeholder had hardcoded Chinese (
🔍 搜尋你想要找尋的關鍵字...). I updated the template with language conditionals so English pages now display🔍 Search articles...andNo results found. - Friendly Language Switcher: Enabled
displayFullLangName: trueinhugo.yaml, transforming the genericZh-TwandEnbuttons into welcoming中文andEnglishlabels. - Context-Aware Language Switching (No More Being Kicked to Home): By default, PaperMod’s header template wires language switching to
site.Home.Translations. This meant clicking the language switcher from within an article would abruptly dump readers back onto that language’s homepage. I patchedlayouts/partials/header.htmlto inspect$currentPage.Translationsfirst, redirecting readers directly to the corresponding translation of the current post whenever available, while gracefully falling back to the homepage only when no counterpart exists.
Reflections
Automated pipelines are like houseplants: leave them untended for a few months, and the soil dries out, pipes clog, and the foundation models you depended on get deprecated.
Yet working through this post-mortem was immensely rewarding. It was a great reminder of how much nuanced engineering lives behind even a personal static blog—from exception handling and LLM lifecycle deprecations to CI/CD state machines and token security boundaries.
The pipes are clear, the bilingual feeds are synced, and Day 1 and Day 2 are live for the world to see.
What’s being rekindled isn’t just my project portfolio—it’s this little automated corner of the internet.