Skip to content

Competitive Update #120

Competitive Update

Competitive Update #120

name: Competitive Update
# ---------------------------------------------------------------
# daily competitive intelligence pipeline.
# merges competitor release checking (from collect_competitive.py)
# with pricing page comparison into a single workflow.
#
# sources:
# - competitor releases: openai/codex, cursor changelog, gemini-cli
# - pricing pages: cursor, antigravity, copilot
#
# daily competitor pricing and feature comparison
#
# cost: ~$0.02-0.05 per run (haiku, stripped text input)
# tested with: claude code v2.1.122
# ---------------------------------------------------------------
on:
schedule:
- cron: '0 12 * * *' # daily at 12:00 UTC
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: "competitive-update"
cancel-in-progress: false
env:
CLAUDE_MODEL: claude-haiku-4-5-20251001
jobs:
competitive-check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Check API key
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::warning::ANTHROPIC_API_KEY not available -- skipping"
exit 1
fi
- uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install anthropic==0.49.0 requests==2.32.3
npm install -g @anthropic-ai/claude-code
# ---- phase 1: check competitor releases ----
- name: Check competitor releases
id: releases
run: python .github/scripts/collect_competitive.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# ---- phase 2: fetch competitor pricing pages ----
- name: Fetch competitor pricing pages
id: pricing
run: |
mkdir -p /tmp/competitor-data
PRICING_CHANGED=false
fetch_with_retry() {
local URL="$1"
local NAME="$2"
local OUTPUT="/tmp/competitor-data/${NAME}-pricing.txt"
for attempt in 1 2; do
CONTENT=$(curl -sL --connect-timeout 10 --max-time 30 "$URL" 2>/dev/null \
| python3 -c "import sys,re; t=sys.stdin.read(); t=re.sub(r'<script[^>]*>.*?</script>','',t,flags=re.S); t=re.sub(r'<style[^>]*>.*?</style>','',t,flags=re.S); t=re.sub(r'<[^>]+>',' ',t); t=re.sub(r'\s+',' ',t).strip(); print(t[:8000])" \
|| true)
# validate: must be >500 chars and contain price-related keywords
CHAR_COUNT=$(echo "$CONTENT" | wc -c)
if [ "$CHAR_COUNT" -gt 500 ] && echo "$CONTENT" | grep -qiE '(\$[0-9]|price|pricing|per month|/mo|free|plan|tier|pro|enterprise)'; then
echo "$CONTENT" > "$OUTPUT"
echo " fetched $NAME: ${CHAR_COUNT} chars (attempt $attempt)"
return 0
fi
if [ "$attempt" -eq 1 ]; then
echo " retrying $NAME (got ${CHAR_COUNT} chars, missing pricing keywords)..."
sleep 2
fi
done
echo " WARN: $NAME fetch failed after 2 attempts"
echo "" > "$OUTPUT"
return 0
}
for url_name in \
"https://www.cursor.com/pricing cursor" \
"https://antigravity.google/pricing antigravity" \
"https://github.com/features/copilot copilot"; do
URL=$(echo "$url_name" | cut -d' ' -f1)
NAME=$(echo "$url_name" | cut -d' ' -f2)
fetch_with_retry "$URL" "$NAME"
done
# verify at least one file has content
TOTAL=$(cat /tmp/competitor-data/*.txt 2>/dev/null | wc -c)
if [ "$TOTAL" -lt 100 ]; then
echo "no competitor pricing data fetched -- all fetches failed"
echo "pricing_fetched=false" >> "$GITHUB_OUTPUT"
else
echo "fetched $TOTAL chars of competitor pricing data"
echo "pricing_fetched=true" >> "$GITHUB_OUTPUT"
fi
# ---- phase 3: compare pricing against current docs ----
- name: Compare pricing against current docs
if: steps.pricing.outputs.pricing_fetched == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p --model "$CLAUDE_MODEL" \
"you are checking if competitor pricing/features have changed.
read the current comparison docs in docs/comparisons/.
then read the freshly fetched pricing text in /tmp/competitor-data/.
also read /tmp/competitive_changes.json for any new competitor releases.
for each competitor (cursor, antigravity, copilot):
1. extract current pricing tiers and key features from the fetched text
2. compare against what's in docs/comparisons/
3. if anything changed (prices, tier names, features, limits), update the comparison doc
4. if there are new releases in competitive_changes.json, note them in the relevant doc
rules:
- only update facts that clearly changed -- dont rewrite style
- keep the existing doc structure and voice (lowercase, no fluff)
- add a comment like '<!-- updated YYYY-MM-DD -->' near changed sections
- if a fetch failed (empty file), skip that competitor
- be conservative -- if you're not sure something changed, dont update it
write a summary of changes to /tmp/comparison-changes.txt.
if nothing changed, write 'no changes detected'." \
--allowedTools Read,Write,Edit,Glob,Grep
# ---- phase 4: persist state from release checker ----
- name: Persist state
run: |
git config user.name "competitive-update[bot]"
git config user.email "competitive-update[bot]@users.noreply.github.com"
git add .github/state/
git diff --cached --quiet && echo "no state changes" && exit 0
git commit -m "chore: update competitive watcher state"
git push origin main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ---- phase 5: create PR if changes detected ----
- name: Create PR if changes exist
id: create_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if git diff --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
echo "no competitive changes detected"
exit 0
fi
BRANCH="auto/competitive-update-$(date +%Y%m%d)"
git checkout -b "$BRANCH"
git add docs/comparisons/
git diff --cached --quiet && echo "nothing to commit" && exit 0
git commit -m "docs: competitive intelligence update $(date +%Y-%m-%d)"
git push -u origin "$BRANCH"
# build PR body -- use --body-file to avoid shell injection from LLM content
{
echo "## automated competitive update"
echo ""
echo "### sources checked"
echo "- competitor releases (openai/codex, cursor, gemini-cli)"
echo "- pricing pages (cursor, antigravity, copilot)"
echo ""
echo "### changes"
cat /tmp/comparison-changes.txt 2>/dev/null || echo "see diff"
echo ""
echo "---"
echo "automated -- verify pricing data and release info before merging."
} > /tmp/pr-body.md
if ! PR_URL=$(gh pr create \
--title "docs: competitive update $(date +%Y-%m-%d)" \
--body-file /tmp/pr-body.md \
--base main \
--draft 2>/tmp/pr-create-err.txt); then
echo "::error::PR creation failed: $(cat /tmp/pr-create-err.txt)"
exit 1
fi
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
# enable auto-merge after CI passes
gh pr merge "$PR_NUMBER" --auto --merge 2>/dev/null || echo "auto-merge not available (branch protection may not be configured)"
echo "Draft PR created with auto-merge: $PR_URL"
# ---- phase 6: close superseded PRs ----
- name: Close superseded PRs
if: steps.create_pr.outputs.pr_number
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_PR="${{ steps.create_pr.outputs.pr_number }}"
# find and close old comparison and competitive PRs
for prefix in "auto/comparison" "auto/competitive"; do
gh pr list --state open --json number,headRefName --jq '.[] | select(.headRefName | startswith("'"$prefix"'")) | .number' \
| while read -r OLD_PR; do
if [ "$OLD_PR" != "$NEW_PR" ]; then
echo "closing superseded PR #$OLD_PR"
gh pr close "$OLD_PR" --comment "superseded by #$NEW_PR" --delete-branch 2>/dev/null || true
fi
done
done