V-RodDBA · Developer Notes
Automating Blogger Publishing with GitHub Actions
How I wired up a Python script and four GitHub secrets so that pushing an HTML file to my repo is all it takes to publish — or update — a blog post.
I already keep my presentation slides, session notes, and blog post drafts in a GitHub repo. It made sense — version control, easy sharing, one place for everything. What didn't make sense was the publishing ritual: open Blogger, click the HTML tab, paste the whole file, pray I got the right version, click Publish. Every. Single. Time. And heaven forbid I caught a typo afterward and had to do it again.
A DBA who manually repeats the same process over and over without automating it should lose their keyboard privileges. So I fixed it. Push to main, the post goes live. Push again, the existing post updates — no duplicates, no paste-and-pray. Here's exactly how it works.
The three files per post
Each blog post lives in the blogposts/ folder as a pair of files, with one shared index:
<html> wrapper. Written and styled to paste directly into Blogger's HTML editor.
PUT (update) instead of a POST (create). This is what prevents duplicates.
A minimal metadata file looks like this:
{
"title": "Hitting the Road to Albany",
"labels": ["DayOfData", "PowerShell", "SQLFamily"],
"htmlFile": "albany-august-2026.html"
}
One-time setup
Step 1 — Google Cloud Console
Create a project, enable the Blogger API v3, and add an OAuth 2.0 credential (Desktop app type). Download the client_secret.json file — you'll need it locally in the next step. Also grab your Blog ID from your Blogger dashboard URL.
Important: add yourself as a test user under the OAuth consent screen while the app is in "Testing" mode, or the token request will be blocked.
Step 2 — Get a refresh token (run once, locally)
The GitHub Action needs a long-lived credential to call the Blogger API without interactive login. Run get_token.py once on your machine — it opens a browser, you log in with your Google account, and it prints three values you'll store as secrets:
pip install google-auth-oauthlib python blogposts/get_token.py blogposts/client_secret.json
Never commit client_secret.json or the printed token values. They live only in GitHub Secrets.
Step 3 — Add four GitHub Secrets
In your repo go to Settings → Secrets and variables → Actions and add:
The GitHub Actions workflow
The workflow lives at .github/workflows/publish-blogpost.yml. It fires on any push to main that touches a file in blogposts/:
name: Publish Blog Post to Blogger on: push: branches: [main] paths: - "blogposts/**.json" - "blogposts/**.html" jobs: publish: runs-on: ubuntu-latest permissions: contents: write # needed to commit published.json back steps: - uses: actions/checkout@v4 - name: Publish all posts env: BLOGGER_BLOG_ID: ${{ secrets.BLOGGER_BLOG_ID }} GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} GOOGLE_REFRESH_TOKEN: ${{ secrets.GOOGLE_REFRESH_TOKEN }} run: | for f in blogposts/*.json; do [ "$f" = "blogposts/published.json" ] && continue python blogposts/publish.py "$f" done - name: Commit updated published.json run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -f blogposts/published.json git diff --staged --quiet || git commit -m "chore: update published post index" git push
The loop skips published.json itself — that file is managed by the script, not treated as a post. After every successful run the bot commits the updated index back to the repo so future runs know which posts already exist on Blogger.
What publish.py actually does
The script is pure stdlib — no dependencies beyond the one-time google-auth-oauthlib install for getting the initial token. On each run it:
- Exchanges the stored refresh token for a short-lived access token using the OAuth2 token endpoint.
- Reads the JSON metadata file to get the title, labels, and path to the HTML content.
- Checks
published.jsonfor the post slug. If found, it sends aPUTto update. If not, it sends aPOSTto create and records the new Blogger post ID. - Writes the updated index back to disk so the workflow step can commit it.
The idempotency guarantee — update vs. create — comes entirely from that slug lookup. As long as the JSON filename stays the same, re-publishing the same post will always hit update, not create.
Writing and publishing a new post
Day-to-day the workflow is simple:
blogposts/my-post.html.
blogposts/my-post.json with the title, labels, and the HTML filename.
main. Stage light goes green, the Action launches, and your post is off the line before you can close the terminal. The bot commits the updated published.json back on its own.
The whole thing is about 100 lines of Python and a 40-line YAML file. No frameworks, no npm, no containers — just the Blogger API, four secrets, and a push hook. If you already keep your content in git, this is about as lean as a publishing pipeline gets. I built it with a little help from Claude, which made the back-and-forth on the OAuth flow considerably less painful.
The full source — workflow, publish script, and token helper — is in the blogposts/ folder of my Presentations repo on GitHub. Steal it freely. The only thing I ask is that you actually use it — a half-automated workflow sitting in a branch nobody pushed is just digital clutter, and tempdb has enough of that already.