Tech Post #92
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |