-
-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,8 +31,8 @@ | |
| from email.utils import parsedate_to_datetime | ||
| from typing import Any | ||
| from xml.etree import ElementTree | ||
|
|
||
|
Check warning on line 34 in .agents/skills/track-framework-updates/scripts/fetch_rss.py
|
||
|
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. RSS item URL field reaches Claude agent unsanitized, enabling prompt injection The Evidence
Identified by Warden security-review · AYS-H4N |
||
| 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 | ||
|
|
@@ -163,7 +163,7 @@ | |
| continue | ||
| entry["items"].append( | ||
| { | ||
| "title": item["title"], | ||
| "title": sanitize_untrusted_text(item["title"]), | ||
| "url": item["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,87 @@ | ||
| 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 | ||
|
|
||
| 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 }} | ||
|
|
||
| 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. | ||
| After writing the digest, append the Markdown digest to $GITHUB_STEP_SUMMARY. | ||
| claude_args: | | ||
| --model claude-opus-4-8 | ||
| --max-turns 80 | ||
| --allowedTools "Read,Write,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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The release body is truncated before sanitization, which can allow a prompt injection payload to bypass security filters if it's split by the truncation.
Severity: HIGH
Suggested Fix
Reverse the order of operations. First, sanitize the entire release body using
sanitize_untrusted_text, and then truncate the sanitized output toMAX_BODY_CHARS.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.