Featured Post

Speaking CV

Bio Tony Wilhelm has amassed over two decades of experience working with databases and computers. His expertise in SQL server dates back t...

Thursday, July 30, 2026

Automating Blogger Publishing with GitHub Actions

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:

post.html The blog post content — plain HTML fragment, no <html> wrapper. Written and styled to paste directly into Blogger's HTML editor.
post.json Metadata: the post title, Blogger labels, and the name of its HTML file. The publish script reads this to know what to send.
published.json A shared index mapping each post's slug to its Blogger post ID. If the slug is already here, the script does a 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:

Secret name Where to get it
BLOGGER_BLOG_ID Blogger dashboard URL
GOOGLE_CLIENT_ID Printed by get_token.py
GOOGLE_CLIENT_SECRET Printed by get_token.py
GOOGLE_REFRESH_TOKEN Printed by get_token.py

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:

  1. Exchanges the stored refresh token for a short-lived access token using the OAuth2 token endpoint.
  2. Reads the JSON metadata file to get the title, labels, and path to the HTML content.
  3. Checks published.json for the post slug. If found, it sends a PUT to update. If not, it sends a POST to create and records the new Blogger post ID.
  4. 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:

1 Write your post as an HTML fragment in blogposts/my-post.html.
2 Create blogposts/my-post.json with the title, labels, and the HTML filename.
3 Commit and push to 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.
Edit the HTML and push again anytime — the same post on Blogger gets updated in place.

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.

V-RodDBA · July 2026 · github.com/TonyWSQL/Presentations

Hitting the Road to Albany

V-RodDBA on the Road

Hitting the Road to Albany

August 6 – 9, 2026  ·  Day of Data Albany #1144

IND MDW ALB and back again ALB MDW IND

BEGIN TRAN Albany calls, and V-RodDBA answers. Four days, two Southwest legs each way, one community data conference, and — allegedly — the world's largest Walmart. Time to SELECT * FROM adventure WHERE city = 'Albany'. Let's go.

Thursday · Aug 6
Wheels Up from Indy
Travel Day
WN IND → MDW → ALB
Southwest out of Indianapolis, connect through Midway and land in Albany. No stored procedures required — just a boarding pass and a carry-on. Bags in hand, let the adventure begin.
🍽 #SQLFamily Dinner with Jason & Sheila Romans, Kristyna Ferris, Chris Hyde, Andy Yun, and Deborah Melkin — venue TBD. Fair warning: index talk may occur between courses.
Their blogs: The DAX Shepherd (Jason & Sheila)  ·  Data on Wheels (Kristyna)  ·  Hydrate Consulting (Chris)  ·  SQLBek (Andy)  ·  Deb the DBA (Deborah)
Friday · Aug 7
Exploring the Capital Region
Pre-Con Day & Shenanigans
Coffee & Bagels at Uncommon Grounds — a Capital Region institution. Fueling up before the day's adventures.
🛒 World's Largest Walmart?? Apparently this is a thing in the Albany area. And… what on earth is a Cartalator? A cart elevator? A shopping cart escalator? Either way, it's a full table scan of the grocery section. Investigation required. Photographic evidence will be obtained.
🍽 Lunch with the Romans' — venue TBD. Always good eats, always great company.
📊 Speaker Dinner — Reconnecting with #SQLFamily before the big day. Kristyna Ferris and Chris Hyde are wrapping up their full-day pre-con Modern Data Warehousing with Microsoft Fabric, so expect them to arrive with strong opinions about Lakehouses and at least one hot take on medallion architecture. Joining the table: Taiob Ali, Steve Jones, Kevin Feasel, and the rest of the Day of Data Albany speaker crew — the kind of dinner where the conversation never quite leaves the data world, and nobody minds.
Saturday · Aug 8
Day of Data Albany 2026
The Main Event — #1144
Coffee & Bagels at Uncommon Grounds — the perfect pre-conference ritual. Coffee, bagels, and the calm before the #SQLFamily storm.
Community Conference
Day of Data Albany 2026 (#1144)

A one-day, community-driven technical conference for anyone working in data — SQL Server, relational databases, analytics, machine learning, AI, and beyond. Sessions from beginner through expert level.

📍 Massry School of Business, UAlbany 🗓 August 8, 2026 👥 Capacity: 300 🎟 In-Person 🎤 My session: Intro to PowerShell with dbatools — because your DBA toolkit deserves a throttle, not just a mouse

Hosted by the Capital Area SQL Server User Group (CASSUG), a 501(c)(3) non-profit. Attending alongside speakers including Jason Romans and Kristyna Ferris.

→ Event page & registration

Sunday · Aug 9
Wheels Down in Indy
Return Travel Day
WN ALB → MDW → IND
🍳 Brunch with the Romans' — venue TBD. If you're in the Albany area Sunday morning, come join us! Always good eats, always great company. #SQLFamily welcome!
The first leg out of Albany is shared with Jason & Sheila Romans. Southwest passengers on this flight: you have been warned. This crew may be loud, caffeinated, running hot from 48 hours of data conference opinions, and still debating whether NULL equals NULL.
🏠 Connecting through Midway and back home to Indy. Another great trip in the books. COMMIT TRAN
V-RodDBA · August 2026 · Albany, NY  |  Day of Data Albany #1144