Competitive Update #120
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: 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 |