I Built a GitHub Action That Writes and Publishes a DEV.to Post Every Morning
I wanted to answer a simple question:
Can I build a technical blog that keeps publishing without me manually writing and publishing every post?
So I built it.
Every morning, a GitHub Actions workflow wakes up.
It checks what engineers are currently talking about on DEV.to.
It gives those signals to an LLM.
The LLM proposes an original engineering angle and writes the article.
The pipeline validates the result.
Then it publishes the article directly to DEV.to.
Finally, it records what was published so tomorrow's job doesn't write the same thing again.
No laptop.
No browser.
No copy/paste.
No "I'll publish it later."
Just a scheduled engineering pipeline.
And the interesting lesson wasn't actually the AI.
It was everything I had to build around the AI.
The Architecture
The whole system looks like this:
┌─────────────────────┐
│ GitHub Actions │
│ Daily Schedule │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ DEV.to API │
│ Rising Articles │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Topic Selection │
│ │
│ Trends + History │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Gemini API │
│ │
│ Generate Article │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Validation │
│ │
│ JSON / length / │
│ tags / duplicates │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ DEV.to API │
│ PUBLISH │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Topic History │
│ Git commit │
└─────────────────────┘
It's basically a tiny content platform.
Except the content platform is a Git repository and a scheduled CI job.
Why GitHub Actions?
My first thought was to build a small backend.
Something like:
API server
↓
Database
↓
Scheduler
↓
LLM
↓
DEV.to
But why?
The job runs once a day.
It doesn't need a server running 24/7.
It doesn't need a database cluster.
It doesn't need Kubernetes.
GitHub Actions already gives me:
- scheduled execution
- manual execution
- logs
- secrets
- a Python runtime
- Git access
- failure visibility
So the infrastructure becomes almost zero.
That's one of those cases where boring infrastructure wins.
The GitHub Actions Workflow
The workflow is intentionally simple.
name: Daily DEV.to Article
on:
schedule:
- cron: "30 2 * * *"
workflow_dispatch:
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install requests google-genai
- name: Generate and publish
env:
DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: |
python scripts/generate_and_publish.py
- name: Commit topic history
run: |
git config user.name "devto-bot"
git config user.email "devto-bot@users.noreply.github.com"
git add data/topic_history.json
git diff --cached --quiet || \
git commit -m "chore: update DEV.to topic history"
git push
The cron expression:
30 2 * * *
runs at 02:30 UTC, which is 08:00 IST.
And because workflow_dispatch is also configured, I can run the exact same workflow manually while developing it.
Step 1: Find What Engineers Are Talking About
I didn't want to maintain a spreadsheet containing:
Monday → React
Tuesday → System Design
Wednesday → TypeScript
Thursday → AI
That would become stale very quickly.
Instead, the pipeline looks at DEV.to's rising content.
response = requests.get(
"https://dev.to/api/articles",
params={
"state": "rising",
"per_page": 17
},
headers={
"api-key": DEVTO_API_KEY
},
timeout=30
)
response.raise_for_status()
articles = response.json()
Now the pipeline has current signals.
Things like:
- titles
- descriptions
- tags
- reactions
- comments
- publication time
But there's an important distinction.
Trending does not mean copy this.
Trending means:
"This is something developers currently care about."
That's all.
Step 2: Turn a Trend Into an Engineering Angle
Suppose AI coding agents are trending.
A weak automation might produce:
"What Are AI Coding Agents?"
That's generic.
A better pipeline asks:
What engineering problem exists underneath this trend?
That could become:
"Why AI Coding Agents Need Architectural Guardrails"
Or:
"Your AI Coding Agent Doesn't Need More Autonomy. It Needs Better Boundaries."
Now we're no longer simply following a trend.
We're using the trend as a signal for finding a problem worth explaining.
That's the difference between:
AI → generate article
and:
Trend
↓
Problem
↓
Engineering perspective
↓
Original article
The second one is what I wanted.
Step 3: Give the Model Memory
There's another problem.
Imagine the automation runs for 30 days.
Without memory, it could eventually produce:
Day 1:
React Rendering Explained
Day 12:
Understanding React Rendering
Day 27:
How React Rendering Works
Technically different titles.
Practically the same article.
So I created:
data/topic_history.json
It starts as:
[]
After publishing, it becomes something like:
[
{
"title": "Why AI Coding Agents Need Architectural Guardrails",
"tags": [
"ai",
"architecture",
"softwareengineering"
],
"url": "https://dev.to/example/article"
}
]
The next generation receives recent history.
Now the model knows:
"We've already covered this."
It's not perfect memory.
But for a daily publishing system, it's surprisingly useful.
Step 4: Force Structured Output
This is where many LLM automations become fragile.
You ask:
Write an article.
And get:
Sure! Here's your article...
Then maybe some Markdown.
Then a conclusion.
Then:
Hope you enjoyed it!
Good for a chat.
Terrible for an API pipeline.
So I define a contract.
{
"title": "...",
"description": "...",
"tags": [
"...",
"..."
],
"body_markdown": "..."
}
The model is instructed to return only valid JSON.
Now the pipeline can treat the model like an unreliable external service.
And that's an important mindset shift:
Don't trust an LLM response just because it looks good.
Parse it.
Validate it.
Reject it if necessary.
Step 5: Validate Before Publishing
The model can generate something syntactically valid but operationally bad.
For example:
{
"title": "",
"description": "",
"tags": [],
"body_markdown": "..."
}
So the pipeline performs deterministic validation.
For example:
def validate_article(article):
if not article.get("title"):
raise ValueError("Missing title")
if not article.get("description"):
raise ValueError("Missing description")
body = article.get("body_markdown", "")
if len(body) < 1000:
raise ValueError("Article is too short")
tags = article.get("tags", [])
if not tags:
raise ValueError("Missing tags")
return True
This is deliberately boring.
And that's exactly what I want.
AI decides what to write.
Code decides whether it is acceptable.
The Boundary I Care About
This became the central design principle of the project:
AI
│
│ creative
▼
┌───────────────┐
│ Article draft │
└───────┬───────┘
│
│ deterministic
▼
┌───────────────┐
│ Validation │
└───────┬───────┘
│
│ deterministic
▼
┌───────────────┐
│ Publish │
└───────────────┘
The LLM has freedom inside the creative boundary.
The system around it remains deterministic.
That's a much safer architecture than:
LLM
↓
Whatever it says
↓
Production
Step 6: The Part I Didn't Expect — Gemini 503
During testing, the pipeline failed.
Not because my code was wrong.
Not because DEV.to rejected the API request.
The Gemini API returned:
503 UNAVAILABLE
This model is currently experiencing high demand.
This was a useful reminder.
When you build an automated system, external APIs are dependencies, not guarantees.
A human running an AI tool might simply retry.
A GitHub Action can't.
So I added retries.
for attempt in range(3):
try:
return generate()
except TemporaryError:
delay = 10 * (2 ** attempt)
time.sleep(delay)
That gives:
Attempt 1
↓
10 seconds
↓
Attempt 2
↓
20 seconds
↓
Attempt 3
And I also added a fallback model.
The important lesson isn't "Gemini sometimes returns 503."
It's:
Every external service in an automation needs a failure strategy.
Step 7: Publish Directly to DEV.to
Once the article passes validation, publishing is just an API request.
Conceptually:
payload = {
"article": {
"title": article["title"],
"description": article["description"],
"tags": article["tags"],
"body_markdown": article["body_markdown"],
"published": True
}
}
response = requests.post(
"https://dev.to/api/articles",
headers={
"api-key": DEVTO_API_KEY,
"Content-Type": "application/json"
},
json=payload,
timeout=30
)
response.raise_for_status()
And that's the moment the pipeline changes from:
AI-generated draft
to:
published article
No human click required.
Secrets Stay Out of Git
There are two credentials the workflow needs:
DEVTO_API_KEY
GEMINI_API_KEY
They are stored as GitHub Actions secrets.
The repository contains:
scripts/
generate_and_publish.py
data/
topic_history.json
.github/
workflows/
daily-devto.yml
But never:
DEVTO_API_KEY=...
GEMINI_API_KEY=...
inside the repository.
This sounds obvious.
Until someone accidentally commits an API key at 2 AM and discovers it the next morning.
The Complete Pipeline
Putting everything together:
┌──────────────────────┐
│ GitHub Scheduler │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ DEV.to Rising API │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Topic Selection │
│ │
│ trends + history │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Gemini API │
│ │
│ Generate article │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ JSON Parse │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Validate │
│ │
│ title │
│ description │
│ tags │
│ length │
│ duplicates │
└──────────┬───────────┘
│
PASS │
▼
┌──────────────────────┐
│ DEV.to Publish │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Save Topic History │
└──────────────────────┘
It's only a few hundred lines of Python.
But the architecture is more interesting than the amount of code.
What I Would Build Next
The first version works.
But it could become much smarter.
1. Multiple Trend Sources
Instead of relying only on DEV.to:
DEV.to
Hacker News
Reddit
GitHub Trending
Google Trends
Then combine the signals.
2. Topic Scoring
Instead of:
pick something trending
score topics based on:
trend strength
+ relevance to my expertise
+ novelty
+ historical performance
Something like:
Topic Score =
30% trend
+ 25% relevance
+ 20% novelty
+ 15% historical performance
+ 10% freshness
Now topic selection becomes an actual ranking problem.
3. Research Before Writing
The current pipeline mainly uses metadata.
A stronger version could:
Trend
↓
Research
↓
Primary sources
↓
Technical verification
↓
Article
That would make the generated posts substantially more useful.
4. A Quality Gate
Before publishing, another model could review the article.
For example:
Writer
↓
Reviewer
↓
Score
↓
Publish only if score >= 8
The reviewer could check:
- technical correctness
- originality
- clarity
- usefulness
- code quality
- clickbait
- unsupported claims
This introduces an interesting pattern:
One model creates. Another model critiques. Code makes the final decision.
5. Close the Analytics Loop
Eventually the system could learn from its own results.
Article
↓
Publish
↓
Views
Reactions
Comments
↓
Analytics
↓
Topic scoring
↓
Better next article
At that point, the system isn't merely generating content.
It's becoming a feedback loop.
The Bigger Lesson
I started this project thinking:
"How can I automate writing a DEV.to article?"
But that's actually the easy part.
The hard part is everything around the generation:
- What should we write?
- How do we avoid repeating ourselves?
- How do we structure model output?
- What happens when the API fails?
- How do we validate generated content?
- How do we protect credentials?
- When should we publish?
- How do we know the article is good?
- How do we learn from previous results?
That's the real engineering problem.
And it leads to a pattern I'm increasingly interested in:
AI
│
Generate / Reason
│
▼
┌──────────────┐
│ Deterministic│
│ System │
└──────────────┘
│
Validate / Act
│
▼
External World
AI is powerful at the fuzzy parts.
Traditional software is still much better at enforcing contracts.
The strongest systems combine both.
Final Thought
I don't think the interesting future is:
"AI will write all the code."
I think it's closer to:
Software systems will increasingly contain AI components, while the surrounding architecture becomes responsible for constraining, validating, observing, and recovering from them.
This little DEV.to bot is a toy example of that idea.
The LLM writes.
The pipeline decides.
Git remembers.
DEV.to publishes.
And tomorrow morning, the whole thing starts again.
Repository Structure
The implementation is roughly:
staff-engineer-academy/
│
├── .github/
│ └── workflows/
│ └── daily-devto.yml
│
├── scripts/
│ └── generate_and_publish.py
│
└── data/
└── topic_history.json
If you're building your own AI automation, I'd strongly recommend starting with this principle:
Treat the LLM as a powerful but unreliable dependency — not as the system itself.
Top comments (0)