
Bruno BorgesEvery Monday at 10 AM Eastern, @javaevolved now tweets a modern Java pattern — automatically. No...
Every Monday at 10 AM Eastern, @javaevolved now tweets a modern Java pattern — automatically. No manual steps, no third-party services, no cron servers. Just a GitHub Actions workflow, a couple of JBang scripts, and the Twitter API.
Here's how it works, and how you can do the same for your own project.
Java Evolved is a static site with 113 code patterns showing the old way vs. the modern way to write Java. Each pattern has a title, summary, old/modern approach labels, JDK version, and a link to its detail page.
I wanted to promote each pattern on Twitter — one per week, in random order, cycling forever. The requirements:
The system has three components:
content/*.yaml → [Queue Generator] → social/queue.txt
→ social/tweets.yaml
→ social/state.yaml
social/* → [Post Script] → Twitter API v2 → updated state
GitHub Actions cron → runs Post Script every Monday
Everything lives in the repository. State is tracked via committed files, not external databases.
File: html-generators/generatesocialqueue.java
A JBang script that scans all content YAML files, shuffles them randomly, and produces three files:
social/queue.txt — the posting order, one category/slug per linesocial/tweets.yaml — pre-drafted tweet text for each patternsocial/state.yaml — a pointer tracking where we are in the queueThe tweet template looks like this:
☕ {title}
{summary}
{oldApproach} → {modernApproach} (JDK {jdkVersion}+)
🔗 https://javaevolved.github.io/{category}/{slug}.html
#Java #JavaEvolved
The generator also validates that every tweet fits within Twitter's 280-character limit. If a summary is too long, it's automatically truncated with an ellipsis. Of the 113 patterns, 12 needed truncation.
When you re-run the generator after adding new content files, it detects new patterns and appends them to the end of the existing queue — preserving the current order and any manual tweet edits. Deleted or renamed patterns are automatically pruned.
Use --reshuffle to force a full reshuffle when the cycle is exhausted.
File: html-generators/socialpost.java
Another JBang script that:
social/state.yaml
social/queue.txt
social/tweets.yaml
I initially planned to use a shell script with curl and openssl for OAuth signing. That turned out to be a bad idea — percent-encoding, signature base strings, and nonce generation are error-prone in Bash.
Instead, the post script uses Java's built-in java.net.http.HttpClient and javax.crypto.Mac for HMAC-SHA1 signing. Here's the core of the OAuth signature:
// Build signature base string
var paramString = oauthParams.entrySet().stream()
.map(e -> percentEncode(e.getKey()) + "=" + percentEncode(e.getValue()))
.collect(Collectors.joining("&"));
var baseString = method + "&" + percentEncode(url) + "&" + percentEncode(paramString);
var signingKey = percentEncode(consumerSecret) + "&" + percentEncode(tokenSecret);
// HMAC-SHA1
var mac = javax.crypto.Mac.getInstance("HmacSHA1");
mac.init(new javax.crypto.spec.SecretKeySpec(
signingKey.getBytes(UTF_8), "HmacSHA1"));
var signature = Base64.getEncoder().encodeToString(
mac.doFinal(baseString.getBytes(UTF_8)));
The script also supports --dry-run to preview the next tweet without posting:
$ jbang html-generators/socialpost.java --dry-run
Queue has 113 entries, current index: 1
Pattern: language/guarded-patterns
Tweet (200 chars):
---
☕ Guarded patterns with when
Add conditions to pattern cases using when guards.
Nested if → when Clause (JDK 21+)
🔗 https://javaevolved.github.io/language/guarded-patterns.html
#Java #JavaEvolved
---
DRY RUN — not posting.
File: .github/workflows/social-post.yml
name: Weekly Social Post
on:
schedule:
- cron: '0 14 * * 1' # Every Monday at 14:00 UTC (10 AM ET)
workflow_dispatch: # Manual trigger
concurrency:
group: social-post
cancel-in-progress: false
jobs:
post:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- uses: jbangdev/setup-jbang@main
- name: Post to Twitter
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_APP_CONSUMER_KEY }}
# ... other secrets
run: jbang html-generators/socialpost.java
- name: Commit updated state
run: |
git add social/state.yaml
git commit -m "chore: update social post state [skip ci]"
git pull --rebase
git push
A few details worth noting:
concurrency group prevents double-posts if a manual dispatch overlaps with the cron[skip ci] in the commit message prevents the state update from triggering other workflowssocial/, not content/ — the deploy workflow watches content/**, so keeping state separate avoids unnecessary site rebuildsgit pull --rebase before push handles the rare case where another commit lands between checkout and pushTwitter's API pricing means each tweet costs about $0.01. With 113 patterns posted weekly:
That's essentially free for a perpetual social media presence.
This entire feature — the queue generator, the post script, the GitHub Actions workflow, the tweet drafts, the documentation updates — was built in a single interactive session with GitHub Copilot CLI. From planning to the first live tweet, everything happened in the terminal.
The session included planning the architecture, getting a rubber-duck critique (which caught several issues — like using shell for OAuth signing and putting state files where they'd trigger deploys), implementing all three components, testing locally with --dry-run, committing, pushing, and triggering the first real tweet.
You can read the full session transcript here: gist.github.com/brunoborges/40ef1b5e9b05de279dab64e443b96a11
The entire implementation is open source at github.com/javaevolved/javaevolved.github.io. You'll need:
title, summary, oldApproach, modernApproach, jdkVersion, category, and slug fieldsGenerate the queue, review the drafts, push, and let GitHub Actions handle the rest.
Follow @javaevolved for a new modern Java pattern every Monday.