Skip to content

Tech Post

Tech Post #92

Workflow file for this run

name: Tech Post
on:
# Fires daily at 9 AM UTC — actual posting interval is controlled by POST_INTERVAL_DAYS secret (default: 3)
schedule:
- cron: '0 9 * * *'
# Manual trigger from GitHub Actions tab
workflow_dispatch:
inputs:
debug:
description: 'Enable verbose debug logging'
required: false
default: 'false'
type: choice
options: ['false', 'true']
force:
description: 'Force post even if interval has not elapsed'
required: false
default: 'false'
type: choice
options: ['false', 'true']
jobs:
post:
name: Research & Post
runs-on: ubuntu-latest
timeout-minutes: 370
permissions:
contents: write
env:
ACTIONS_STEP_DEBUG: ${{ github.event.inputs.debug == 'true' }}
ACTIONS_RUNNER_DEBUG: ${{ github.event.inputs.debug == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check posting interval
id: interval
run: |
INTERVAL="${{ secrets.POST_INTERVAL_DAYS }}"
INTERVAL="${INTERVAL:-3}"
FORCE="${{ github.event.inputs.force }}"
echo "POST_INTERVAL_DAYS = $INTERVAL"
if [ "$FORCE" = "true" ]; then
echo "Force flag set — skipping interval check"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ ! -f .last-post-date ]; then
echo "No .last-post-date found — first run, proceeding"
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
LAST=$(cat .last-post-date | tr -d '[:space:]')
LAST_TS=$(date -d "$LAST" +%s 2>/dev/null || date -j -f "%Y-%m-%d" "$LAST" +%s 2>/dev/null || echo 0)
NOW_TS=$(date +%s)
ELAPSED_DAYS=$(( (NOW_TS - LAST_TS) / 86400 ))
echo "Last post: $LAST ($ELAPSED_DAYS days ago, interval: $INTERVAL)"
if [ "$ELAPSED_DAYS" -ge "$INTERVAL" ]; then
echo "Interval elapsed — proceeding"
echo "should_run=true" >> "$GITHUB_OUTPUT"
else
echo "Interval not yet elapsed — skipping"
echo "should_run=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Node.js
if: steps.interval.outputs.should_run == 'true'
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Print environment info
if: steps.interval.outputs.should_run == 'true'
run: |
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
echo "Runner OS: ${{ runner.os }}"
echo "Triggered by: ${{ github.event_name }}"
echo "Debug mode: ${{ github.event.inputs.debug || 'false' }}"
echo "---"
echo "Secrets configured:"
echo " ANTHROPIC_API_KEY → ${{ secrets.ANTHROPIC_API_KEY != '' && '✓ set' || '✗ MISSING' }}"
echo " NEWSAPI_KEY → ${{ secrets.NEWSAPI_KEY != '' && '✓ set' || '✗ not set (optional)' }}"
echo " LINKEDIN tokens → ${{ secrets.LINKEDIN_ACCESS_TOKEN != '' && '✓ set' || '✗ MISSING' }}"
echo " TWITTER tokens → ${{ secrets.TWITTER_APP_KEY != '' && '✓ set' || '✗ not set (optional)' }}"
echo " POST_INTERVAL_DAYS → ${{ secrets.POST_INTERVAL_DAYS != '' && '✓ set' || '✗ not set (default: 3)' }}"
echo " NEWS_NICHE → ${{ secrets.NEWS_NICHE != '' && '✓ set' || '✗ not set (default: business-architect)' }}"
echo " POSTING_STYLE → ${{ secrets.POSTING_STYLE != '' && '✓ set' || '✗ not set (default: niche-driven)' }}"
echo " DEFAULT_TONE → ${{ secrets.DEFAULT_TONE != '' && '✓ set' || '✗ not set (optional)' }}"
echo " POST_AUTHOR_NAME → ${{ secrets.POST_AUTHOR_NAME != '' && '✓ set' || '✗ not set (default: PostAgent)' }}"
echo " NEWS_RESEARCH_WINDOW_DAYS → ${{ secrets.NEWS_RESEARCH_WINDOW_DAYS != '' && '✓ set' || '✗ not set (default: 30)' }}"
echo " ALLOW_EVERGREEN_FALLBACK → ${{ secrets.ALLOW_EVERGREEN_FALLBACK != '' && '✓ set' || '✗ not set (default: true)' }}"
echo " PUBLISH_RETRY_ATTEMPTS → ${{ secrets.PUBLISH_RETRY_ATTEMPTS != '' && '✓ set' || '✗ not set (default: 3)' }}"
echo " FAIL_ON_PUBLISH_FAILURE → ${{ secrets.FAIL_ON_PUBLISH_FAILURE != '' && '✓ set' || '✗ not set (default: false)' }}"
echo " CONTENT_SIMILARITY_THRESHOLD → ${{ secrets.CONTENT_SIMILARITY_THRESHOLD != '' && '✓ set' || '✗ not set (default: 0.80)' }}"
echo " ENABLE_CLAUDE_BOT → ${{ secrets.ENABLE_CLAUDE_BOT == 'false' && '✗ disabled' || '✓ enabled (default)' }}"
echo " ENABLE_PR_REVIEW → ${{ secrets.ENABLE_PR_REVIEW == 'false' && '✗ disabled' || '✓ enabled (default)' }}"
echo " SLACK_BOT_TOKEN → ${{ secrets.SLACK_BOT_TOKEN != '' && '✓ set' || '✗ not set (no approval gate)' }}"
echo " SLACK_CHANNEL_ID → ${{ secrets.SLACK_CHANNEL_ID != '' && '✓ set' || '✗ not set' }}"
- name: Install dependencies
if: steps.interval.outputs.should_run == 'true'
run: npm ci
- name: Run post pipeline
if: steps.interval.outputs.should_run == 'true'
id: post_run
run: |
set -o pipefail
npm run trigger | tee post-run.log
if grep -q "PUBLISHED_ANY=true" post-run.log; then
echo "published_any=true" >> "$GITHUB_OUTPUT"
else
echo "published_any=false" >> "$GITHUB_OUTPUT"
fi
env:
NODE_ENV: production
NODE_OPTIONS: '--enable-source-maps'
NEWSAPI_KEY: ${{ secrets.NEWSAPI_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_MODEL: ${{ secrets.CLAUDE_MODEL }}
LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }}
LINKEDIN_PERSON_URN: ${{ secrets.LINKEDIN_PERSON_URN }}
TWITTER_APP_KEY: ${{ secrets.TWITTER_APP_KEY }}
TWITTER_APP_SECRET: ${{ secrets.TWITTER_APP_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_SECRET: ${{ secrets.TWITTER_ACCESS_SECRET }}
POST_VARIATION_SEED: ${{ github.run_id }}-${{ github.run_attempt }}
CONTENT_SIMILARITY_THRESHOLD: ${{ secrets.CONTENT_SIMILARITY_THRESHOLD }}
POSTING_STYLE: ${{ secrets.POSTING_STYLE }}
DEFAULT_TONE: ${{ secrets.DEFAULT_TONE }}
NEWS_NICHE: ${{ secrets.NEWS_NICHE }}
NEWS_STORY_COUNT: ${{ secrets.NEWS_STORY_COUNT }}
NEWS_RESEARCH_WINDOW_DAYS: ${{ secrets.NEWS_RESEARCH_WINDOW_DAYS }}
ALLOW_EVERGREEN_FALLBACK: ${{ secrets.ALLOW_EVERGREEN_FALLBACK }}
PUBLISH_RETRY_ATTEMPTS: ${{ secrets.PUBLISH_RETRY_ATTEMPTS }}
FAIL_ON_PUBLISH_FAILURE: ${{ secrets.FAIL_ON_PUBLISH_FAILURE }}
POST_AUTHOR_NAME: ${{ secrets.POST_AUTHOR_NAME }}
POST_AGENT_NAME: ${{ secrets.POST_AGENT_NAME }}
LINKEDIN_POST_LENGTH: ${{ secrets.LINKEDIN_POST_LENGTH }}
TWITTER_POST_LENGTH: ${{ secrets.TWITTER_POST_LENGTH }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
SLACK_APPROVAL_TIMEOUT_MINUTES: ${{ secrets.SLACK_APPROVAL_TIMEOUT_MINUTES }}
SLACK_POST_VARIANTS: ${{ secrets.SLACK_POST_VARIANTS }}
- name: Persist run state
if: steps.interval.outputs.should_run == 'true' && steps.post_run.outcome == 'success' && steps.post_run.outputs.published_any == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
echo "$(date -u +%Y-%m-%d)" > .last-post-date
git add .last-post-date
if [ -f .post-history.json ] && ! git diff --quiet -- .post-history.json; then
git add .post-history.json
fi
git commit -m "chore: update post state [skip ci]" || echo "Nothing to commit"
git push origin HEAD:${{ github.ref_name }}