Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
chore(github): Add GitHub workflow for framework updates digest#21681
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6df88d2c7364bf83ad9e2ab79fd983791dfa1ac546File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -32,7 +32,7 @@ | ||
| from typing import Any | ||
| from xml.etree import ElementTree | ||
s1gr1d marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| from _common import cutoff, load_frameworks, parse_iso | ||
| from _common import cutoff, load_frameworks, parse_iso, sanitize_untrusted_text | ||
| USER_AGENT = "sentry-javascript-track-framework-updates/1.0" | ||
| TIMEOUT_SECONDS = 20 | ||
| @@ -160,11 +160,14 @@ | ||
| for item in parsed: | ||
| published = _parse_date(item.get("publishedAt")) | ||
| if published is None or published < since: | ||
| continue | ||
| url = item["url"] | ||
| if not url.startswith("https://"): | ||
| continue | ||
sentry[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| entry["items"].append( | ||
| { | ||
Check notice on line 168 in .agents/skills/track-framework-updates/scripts/fetch_rss.py
| ||
| "title": item["title"], | ||
| "url": item["url"], | ||
| "title": sanitize_untrusted_text(item["title"]), | ||
| "url": url, | ||
| "publishedAt": item["publishedAt"], | ||
| "feed": feed_url, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| #!/usr/bin/env python3 | ||
| """Read Claude Code execution output JSON and write run metrics as Markdown. | ||
| Intended for GitHub Actions job summary ($GITHUB_STEP_SUMMARY). Outputs a | ||
| compact metrics table followed by the digest content (if available). | ||
| Usage: | ||
| python3 write_job_summary.py <execution-output.json> [digest.md] | ||
| """ | ||
| from __future__ import annotations | ||
| import json | ||
| import sys | ||
| def main() -> int: | ||
| if len(sys.argv) < 2: | ||
| print( | ||
| "Usage: write_job_summary.py <execution-output.json> [digest.md]", | ||
| file=sys.stderr, | ||
| ) | ||
| return 1 | ||
| exec_path = sys.argv[1] | ||
| digest_path = sys.argv[2] if len(sys.argv) > 2 else None | ||
| # Parse execution output for metrics | ||
| duration_ms = None | ||
| num_turns = None | ||
| total_cost = None | ||
| subtype = "" | ||
| try: | ||
| with open(exec_path, encoding="utf-8") as f: | ||
| content = f.read() | ||
| results = [] | ||
| for line in content.strip().splitlines(): | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| try: | ||
| obj = json.loads(line) | ||
| if isinstance(obj, dict) and obj.get("type") == "result": | ||
| results.append(obj) | ||
| elif isinstance(obj, list): | ||
| for item in obj: | ||
| if isinstance(item, dict) and item.get("type") == "result": | ||
| results.append(item) | ||
| except json.JSONDecodeError: | ||
| continue | ||
| if not results: | ||
| try: | ||
| obj = json.loads(content) | ||
| if isinstance(obj, dict) and obj.get("type") == "result": | ||
| results = [obj] | ||
| elif isinstance(obj, list): | ||
| results = [ | ||
| item | ||
| for item in obj | ||
| if isinstance(item, dict) and item.get("type") == "result" | ||
| ] | ||
| except json.JSONDecodeError: | ||
| pass | ||
| if results: | ||
| last = results[-1] | ||
| duration_ms = last.get("duration_ms") | ||
| num_turns = last.get("num_turns") | ||
| total_cost = last.get("total_cost_usd") | ||
| subtype = last.get("subtype", "") | ||
| except OSError as e: | ||
| print(f"Could not read execution output: {e}", file=sys.stderr) | ||
| # Build summary: digest first, run metrics at the bottom | ||
| lines: list[str] = [] | ||
| # Digest content on top | ||
| if digest_path: | ||
| try: | ||
| with open(digest_path, encoding="utf-8") as f: | ||
| digest = f.read().strip() | ||
| if digest: | ||
| lines.append(digest) | ||
| except OSError: | ||
| lines.append("_Digest file not found._") | ||
| # Run metrics at the bottom | ||
| cost_str = ( | ||
| f"${total_cost:.4f}" if isinstance(total_cost, (int, float)) else "n/a" | ||
| ) | ||
| duration_str = ( | ||
| f"{duration_ms / 1000:.0f}s" | ||
| if isinstance(duration_ms, (int, float)) | ||
| else "n/a" | ||
| ) | ||
| lines.extend([ | ||
| "", | ||
| "---", | ||
| "", | ||
| "### Run metrics", | ||
| "", | ||
| "| Metric | Value |", | ||
| "|--------|-------|", | ||
| f"| Duration | {duration_str} |", | ||
| f"| Turns | {num_turns if num_turns is not None else 'n/a'} |", | ||
| f"| Cost (USD) | {cost_str} |", | ||
| ]) | ||
| if subtype == "error_max_turns": | ||
| lines.extend(["", "> **Run stopped:** maximum turns reached."]) | ||
| elif subtype and subtype != "success": | ||
| lines.extend(["", f"> Result: `{subtype}`"]) | ||
| print("\n".join(lines)) | ||
| return 0 | ||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| name: Track Framework Updates | ||
| on: | ||
| # For future use (first, we test it manually) | ||
| # schedule: | ||
| # # Every Monday Morning at 04:00 UTC | ||
| # - cron: '0 4 * * 1' | ||
| workflow_dispatch: | ||
| inputs: | ||
| since_days: | ||
| description: 'Number of days to look back (default: 7)' | ||
| required: false | ||
| type: number | ||
| default: 7 | ||
| concurrency: | ||
| group: track-framework-updates | ||
| cancel-in-progress: true | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: do we need to set this? MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's unlikely that it happens that this workflow is called multiple times but a small safety measure to not run it multiple times at once. | ||
| jobs: | ||
| track-updates: | ||
| runs-on: ubuntu-latest | ||
| environment: ci-triage | ||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: read | ||
| id-token: write | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: develop | ||
| - name: Determine lookback window | ||
| id: params | ||
| env: | ||
| INPUT_SINCE_DAYS: ${{ github.event.inputs.since_days }} | ||
| run: | | ||
| SINCE_DAYS="${INPUT_SINCE_DAYS:-7}" | ||
| echo "since_days=$SINCE_DAYS" >> "$GITHUB_OUTPUT" | ||
| echo "Looking back $SINCE_DAYS days" | ||
| - name: Run Claude digest | ||
| id: digest | ||
| uses: anthropics/claude-code-action@24492741e0ccfdef4c1d19da8e11e0f373d07494 # v1 | ||
| with: | ||
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| allowed_non_write_users: '*' | ||
| prompt: | | ||
| /track-framework-updates --since-days ${{ steps.params.outputs.since_days }} | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixValidate and sanitize the Prompt for AI Agent | ||
| IMPORTANT: Do NOT wait for approval. | ||
| Do NOT write files outside the workspace. | ||
| Do NOT use Bash redirection (> file) — it is blocked. | ||
| Do NOT use `python3 -c` or inline Python — only the skill's scripts are allowed. | ||
| Do NOT attempt to delete (`rm`) files. | ||
| claude_args: | | ||
| --model claude-opus-4-8 | ||
| --max-turns 80 | ||
| --allowedTools "Read,Write(.agents/skills/track-framework-updates/output/*),Bash(python3 .agents/skills/track-framework-updates/scripts/collect_updates.py *),Bash(python3 .agents/skills/track-framework-updates/scripts/check_support.py)" | ||
| - name: Post job summary | ||
| if: always() | ||
| run: | | ||
| EXEC_FILE="${{ steps.digest.outputs.execution_file }}" | ||
| if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then | ||
| EXEC_FILE="${RUNNER_TEMP}/claude-execution-output.json" | ||
| fi | ||
| DIGEST=".agents/skills/track-framework-updates/output/framework-updates-digest.md" | ||
| SCRIPT=".agents/skills/track-framework-updates/scripts/write_job_summary.py" | ||
| if [ -f "$EXEC_FILE" ]; then | ||
| python3 "$SCRIPT" "$EXEC_FILE" "$DIGEST" >> "$GITHUB_STEP_SUMMARY" | ||
| elif [ -f "$DIGEST" ]; then | ||
| echo "## Framework Updates Digest" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "" >> "$GITHUB_STEP_SUMMARY" | ||
| cat "$DIGEST" >> "$GITHUB_STEP_SUMMARY" | ||
| else | ||
| echo "## Framework Updates Digest" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "No output found. The run may have failed before producing results." >> "$GITHUB_STEP_SUMMARY" | ||
| fi | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.