Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions .github/workflows/digest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# =============================================================================
# Follow Builders — Daily Digest Workflow
# =============================================================================
# Orchestrates the full digest pipeline: prepare → LLM remix → deliver.
# Runs daily after feed generation, or on-demand via workflow_dispatch.
#
# ── Required GitHub Secrets ──────────────────────────────────────────────────
#
# LLM API (at least one required):
# ANTHROPIC_API_KEY — Anthropic Claude API key (priority)
# OPENAI_API_KEY — OpenAI API key (fallback)
#
# Telegram delivery (when DELIVERY_METHOD = telegram):
# TELEGRAM_BOT_TOKEN — Bot token from @BotFather
# TELEGRAM_CHAT_ID — Target chat/group ID
#
# Email delivery (when DELIVERY_METHOD = email):
# RESEND_API_KEY — Resend.com API key
# DIGEST_EMAIL — Destination email address
#
# ── Required GitHub Variables ────────────────────────────────────────────────
#
# DIGEST_FEATURE_FLAG — Set to 'true' to enable this workflow.
# Not set in upstream repo (workflow never runs).
#
# ── Optional GitHub Variables ────────────────────────────────────────────────
#
# DIGEST_LANGUAGE — 'en' (default), 'zh', or 'bilingual'
# DELIVERY_METHOD — 'telegram' (default), 'email', or 'stdout'
# LLM_MODEL — Override default LLM model name
#
# =============================================================================

name: Daily Digest

on:
schedule:
# Run daily at 7:00 UTC — one hour after feed generation (6:00 UTC)
- cron: '0 7 * * *'
workflow_dispatch:
inputs:
language:
description: 'Digest language'
required: false
default: 'en'
type: choice
options:
- en
- zh
- bilingual
delivery_method:
description: 'Delivery method'
required: false
default: 'telegram'
type: choice
options:
- telegram
- email
- stdout
llm_model:
description: 'LLM model override (optional)'
required: false
default: ''
type: string

jobs:
digest:
# Feature flag — skips entire job when not 'true'.
# Upstream repo leaves this unset so the workflow never executes there.
if: vars.DIGEST_FEATURE_FLAG == 'true'
runs-on: ubuntu-latest

env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
DIGEST_EMAIL: ${{ secrets.DIGEST_EMAIL }}
DIGEST_LANGUAGE: ${{ inputs.language || vars.DIGEST_LANGUAGE || 'en' }}
DELIVERY_METHOD: ${{ inputs.delivery_method || vars.DELIVERY_METHOD || 'telegram' }}
LLM_MODEL: ${{ inputs.llm_model || vars.LLM_MODEL || '' }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: cd scripts && npm install

# ── Task 3.2: Validate secrets ────────────────────────────────────────
- name: Validate secrets
run: |
MISSING=""

# At least one LLM API key is required
if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
MISSING="${MISSING} - ANTHROPIC_API_KEY or OPENAI_API_KEY (at least one LLM API key required)\n"
fi

# Delivery-method-specific secrets
if [ "$DELIVERY_METHOD" = "telegram" ]; then
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
MISSING="${MISSING} - TELEGRAM_BOT_TOKEN (required for Telegram delivery)\n"
fi
if [ -z "$TELEGRAM_CHAT_ID" ]; then
MISSING="${MISSING} - TELEGRAM_CHAT_ID (required for Telegram delivery)\n"
fi
elif [ "$DELIVERY_METHOD" = "email" ]; then
if [ -z "$RESEND_API_KEY" ]; then
MISSING="${MISSING} - RESEND_API_KEY (required for Email delivery)\n"
fi
if [ -z "$DIGEST_EMAIL" ]; then
MISSING="${MISSING} - DIGEST_EMAIL (required for Email delivery)\n"
fi
fi
# stdout needs no delivery secrets

if [ -n "$MISSING" ]; then
echo "::error::Missing required secrets:"
printf "$MISSING" | while IFS= read -r line; do echo "::error::$line"; done
echo ""
echo "❌ Missing required secrets:"
printf "$MISSING"
echo ""
echo "Please configure the missing secrets in your repository settings."
exit 1
fi

echo "✅ All required secrets are configured."

# ── Task 3.3: Prepare → check-content → remix ────────────────────────
- name: Prepare digest data
id: prepare
run: cd scripts && node prepare-digest.js > /tmp/prepare-output.json

- name: Check content availability
id: check-content
run: |
HAS_CONTENT=$(node -e "
const data = JSON.parse(require('fs').readFileSync('/tmp/prepare-output.json', 'utf-8'));
const s = data.stats || {};
const has = (s.podcastEpisodes || 0) > 0 || (s.xBuilders || 0) > 0 || (s.blogPosts || 0) > 0;
console.log(has ? 'true' : 'false');
")
echo "has_content=$HAS_CONTENT" >> "$GITHUB_OUTPUT"
if [ "$HAS_CONTENT" = "false" ]; then
echo "ℹ️ No new content found in feeds. Skipping remix and delivery."
else
echo "✅ Content found — proceeding with remix."
fi

- name: Remix digest with LLM
id: remix
if: steps.check-content.outputs.has_content == 'true'
run: |
MODEL_FLAG=""
if [ -n "$LLM_MODEL" ]; then
MODEL_FLAG="--model $LLM_MODEL"
fi
cd scripts && node remix-digest.js \
--file /tmp/prepare-output.json \
--language "$DIGEST_LANGUAGE" \
$MODEL_FLAG \
> /tmp/digest-output.txt

# ── Task 3.4: Deliver with artifact fallback ─────────────────────────
- name: Setup delivery config
id: setup-deliver-config
if: steps.check-content.outputs.has_content == 'true'
run: |
mkdir -p ~/.follow-builders

# Build config.json and .env using node for clean JSON output
node -e "
const fs = require('fs');
const method = process.env.DELIVERY_METHOD;
const lang = process.env.DIGEST_LANGUAGE;
const config = { language: lang, delivery: { method } };

if (method === 'telegram') {
config.delivery.chatId = process.env.TELEGRAM_CHAT_ID;
} else if (method === 'email') {
config.delivery.email = process.env.DIGEST_EMAIL;
}

const dir = require('os').homedir() + '/.follow-builders';
fs.writeFileSync(dir + '/config.json', JSON.stringify(config, null, 2));

let envContent = '';
if (method === 'telegram') {
envContent = 'TELEGRAM_BOT_TOKEN=' + process.env.TELEGRAM_BOT_TOKEN;
} else if (method === 'email') {
envContent = 'RESEND_API_KEY=' + process.env.RESEND_API_KEY;
}
fs.writeFileSync(dir + '/.env', envContent);
"

- name: Deliver digest
id: deliver
if: steps.check-content.outputs.has_content == 'true'
run: cd scripts && node deliver.js --file /tmp/digest-output.txt

- name: Upload digest artifact (fallback)
if: failure() && steps.remix.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: digest-output
path: /tmp/digest-output.txt
retention-days: 7

- name: Output run summary
if: always() && steps.check-content.outputs.has_content == 'true'
run: |
# Determine LLM provider used
if [ -n "$ANTHROPIC_API_KEY" ]; then
LLM_PROVIDER="Anthropic"
elif [ -n "$OPENAI_API_KEY" ]; then
LLM_PROVIDER="OpenAI"
else
LLM_PROVIDER="N/A"
fi

# Parse stats and write summary
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('/tmp/prepare-output.json', 'utf-8'));
const s = data.stats || {};
const summary = [
'## 📰 AI Builders Digest — Run Summary',
'',
'| Metric | Count |',
'|--------|-------|',
'| 🐦 Tweets | ' + (s.totalTweets || 0) + ' |',
'| 🎙️ Podcasts | ' + (s.podcastEpisodes || 0) + ' |',
'| 📝 Blog posts | ' + (s.blogPosts || 0) + ' |',
'| 🤖 LLM Provider | ${LLM_PROVIDER} |',
'| 🌐 Language | ${DIGEST_LANGUAGE} |',
'| 📬 Delivery | ${DELIVERY_METHOD} |',
].join('\n');
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary + '\n');
"
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,43 @@ git clone https://github.com/zarazhangrui/follow-builders.git ~/.claude/skills/f
cd ~/.claude/skills/follow-builders/scripts && npm install
```

## Deploy via GitHub Actions (No Computer Needed)

If you don't want to keep a local agent running, you can use GitHub Actions to automatically generate and deliver your digest in the cloud.

### Setup

1. **Fork** this repository
2. Go to **Settings → Secrets and variables → Actions**
3. Under the **Variables** tab, create `DIGEST_FEATURE_FLAG` and set it to `true`
4. Under the **Secrets** tab, add at least one LLM API key and your delivery secrets (see table below)
5. Go to the **Actions** tab and enable workflows for your fork
6. (Optional) Trigger manually via **Actions → Daily Digest → Run workflow** to test

### Secrets & Variables Reference

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `DIGEST_FEATURE_FLAG` | Variable | Yes | Set to `true` to enable the digest workflow |
| `ANTHROPIC_API_KEY` | Secret | At least one | Anthropic Claude API key (used by default if both are set) |
| `OPENAI_API_KEY` | Secret | At least one | OpenAI API key (fallback if Anthropic key is not set) |
| `TELEGRAM_BOT_TOKEN` | Secret | For Telegram | Bot token from @BotFather |
| `TELEGRAM_CHAT_ID` | Secret | For Telegram | Target chat or group ID |
| `RESEND_API_KEY` | Secret | For Email | Resend.com API key |
| `DIGEST_EMAIL` | Secret | For Email | Destination email address |
| `DIGEST_LANGUAGE` | Variable | No | `en` (default), `zh`, or `bilingual` |
| `DELIVERY_METHOD` | Variable | No | `telegram` (default), `email`, or `stdout` |
| `LLM_MODEL` | Variable | No | Override the default LLM model name |

### Deployment Methods Comparison

| | Local Agent (Kiro / Claude Code) | OpenClaw | GitHub Actions |
|---|---|---|---|
| Quality | Best (full LLM remix) | Best (persistent agent) | Good (API-based remix) |
| Always-on | No — requires computer to be on | Yes — automatic delivery | Yes — runs in cloud |
| Setup | Install skill, configure via chat | Install agent, configure | Fork repo, add secrets |
| Best for | Hands-on users who want full control | Always-on, set-and-forget | Easy cloud setup, no computer needed |

## Requirements

- An AI agent (OpenClaw, Claude Code, or similar)
Expand Down
37 changes: 37 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,43 @@ git clone https://github.com/zarazhangrui/follow-builders.git ~/.claude/skills/f
cd ~/.claude/skills/follow-builders/scripts && npm install
```

## 通过 GitHub Actions 部署(无需本地电脑)

如果你不想一直开着本地 agent,可以使用 GitHub Actions 在云端自动生成并推送摘要。

### 设置步骤

1. **Fork** 本仓库
2. 进入 **Settings → Secrets and variables → Actions**
3. 在 **Variables** 标签页中,创建 `DIGEST_FEATURE_FLAG` 并设置为 `true`
4. 在 **Secrets** 标签页中,添加至少一个 LLM API key 和你的推送凭证(见下表)
5. 进入 **Actions** 标签页,为你的 fork 启用工作流
6. (可选)通过 **Actions → Daily Digest → Run workflow** 手动触发测试

### Secrets 与 Variables 参考

| 名称 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `DIGEST_FEATURE_FLAG` | Variable | 是 | 设为 `true` 以启用摘要工作流 |
| `ANTHROPIC_API_KEY` | Secret | 至少一个 | Anthropic Claude API key(两者都设置时优先使用) |
| `OPENAI_API_KEY` | Secret | 至少一个 | OpenAI API key(Anthropic key 未设置时使用) |
| `TELEGRAM_BOT_TOKEN` | Secret | Telegram 推送时 | 从 @BotFather 获取的 Bot token |
| `TELEGRAM_CHAT_ID` | Secret | Telegram 推送时 | 目标聊天或群组 ID |
| `RESEND_API_KEY` | Secret | 邮件推送时 | Resend.com API key |
| `DIGEST_EMAIL` | Secret | 邮件推送时 | 目标邮箱地址 |
| `DIGEST_LANGUAGE` | Variable | 否 | `en`(默认)、`zh` 或 `bilingual` |
| `DELIVERY_METHOD` | Variable | 否 | `telegram`(默认)、`email` 或 `stdout` |
| `LLM_MODEL` | Variable | 否 | 覆盖默认 LLM 模型名称 |

### 部署方式对比

| | 本地 Agent(Kiro / Claude Code) | OpenClaw | GitHub Actions |
|---|---|---|---|
| 质量 | 最佳(完整 LLM 混编) | 最佳(持久化 agent) | 良好(基于 API 混编) |
| 持续运行 | 否——需要电脑保持开机 | 是——自动推送 | 是——在云端运行 |
| 设置 | 安装 skill,通过对话配置 | 安装 agent,配置 | Fork 仓库,添加 secrets |
| 适合 | 喜欢完全掌控的动手型用户 | 一劳永逸的自动化设置 | 简单云端部署,无需电脑 |

## 系统要求

- 一个 AI agent(OpenClaw、Claude Code 或类似工具)
Expand Down
Loading