wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation
This commit is contained in:
233
.agent/knowledge/awesome_claude/.github/ISSUE_TEMPLATE/recommend-resource.yml
vendored
Normal file
233
.agent/knowledge/awesome_claude/.github/ISSUE_TEMPLATE/recommend-resource.yml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
name: 🚀 Recommend New Resource
|
||||
description: Recommend a new resource to be featured in Awesome Claude Code
|
||||
title: "[Resource]: WRITE THE NAME OF YOUR RESOURCE HERE"
|
||||
labels: ["resource-submission", "pending-validation"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Welcome!
|
||||
|
||||
Thank you for recommending a resource to Awesome Claude Code! This form will guide you through the recommendation process.
|
||||
Please make sure that you have already reviewed the [CONTRIBUTING](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md) document as well as the [CODE_OF_CONDUCT](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CODE_OF_CONDUCT.md), and that you agree to abide by the terms. Be really, really sure.
|
||||
|
||||
**WARNING: A strict spam-deterrent system has been put in place. Failure to comply with the simple requirements stated in the [CONTRIBUTING](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md) document will result in intreasingly severe penalties.**
|
||||
|
||||
**Resource Guidelines:**
|
||||
- Issues must be submitted by human users using the github.com UI. The system does not allow resource submissions via the `gh` CLI or other programmatic means. Doing so violates the Code of Conduct and submissions will be automatically closed.
|
||||
- Ensure that you have actually visited this repo before and reviewed the entries on the list. Recommendations must be unique from existing resources, and should be of an equally high caliber.
|
||||
- Avoid submitting resources that violate the Claude Code Usage Policy,or the licensing rights of other independent developers.
|
||||
- Recommendations will be closely scrutinized for security and potential risk.
|
||||
- Although most recommendations are submitted by the authors, you may submit any resource that you love.
|
||||
- The system does not allow resource submissions via the `gh` CLI.
|
||||
- Resources must be at least one week old.
|
||||
|
||||
**Tips and Tricks for a Speedy Review:**
|
||||
- Please provide clear installation AND uninstallation instructions for any installable resources.
|
||||
- If your resource requires me to execute a bash script, you **must** provide me with a clearly annotated/commented version in which everything is documented clearly. I _can_ read Bash, but it hurts my eyes after a while.
|
||||
- Short examples or demos are tremendously helpful in the review process. If I can see it in action before I think about running it, you're way ahead of the curve.
|
||||
- If your resource requires elevated access or "--dangerously-skip-permissions", please make sure the user is aware of this(!)
|
||||
- If your resource involves making ANY network requests except to the Anthropic API, you **must** state that here.
|
||||
- Offering an auto-update functionality for a library may be a very nice convenience for people. (Similarly, `npx @latest`). However, this is also a known threat vector and will be viewed with caution.
|
||||
- If you are claiming that a resource improves Claude's capacity to perform some particular action, these claims must be backed by evidence. It's your job to provide the evidence, not mine.
|
||||
- Try to submit _focused_ resources that differentiate your project from others, not general-purpose marketplaces.
|
||||
- Avoid submitting complex systems that require long onboarding or extensive training in a particular methodology.
|
||||
|
||||
**Ask Claude for a Candid Review:**
|
||||
When I review your recommendation, I will ask my assistant Claude Code to perform a review (this is to assist me - I do not base my judgment on this review alone.) You can find the type of prompt in `.claude/commands/evaluate-repository.md`. I recommend that you run this evaluation yourself ahead of time. Also, ask yourself: "Could Opus build this in one session?"
|
||||
|
||||
After submission, our automated system will validate whether your Issue is well-formed with respect to the requirements of the template, and post the results as a comment. (This is merely a formality and does not constitute a review.)
|
||||
|
||||
Once your recommendation has been validated, you've done your job - the project has been recommended. I do my best to review recommendations. That summarizes the extent of my obligation. If I raise any further questions about your project, it's usually because I'm interested in it, and want to understand it better. Don't make any changes solely on the basis of my feedback.
|
||||
|
||||
- type: input
|
||||
id: display_name
|
||||
attributes:
|
||||
label: Display Name
|
||||
description: The name of the resource as it will appear in the list
|
||||
placeholder: "e.g., My Awesome Tool, /my-command, claude-helper"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Category
|
||||
description: Select the primary category for your resource (note that I'm currenlty lumping most things called "plugins" under "Agent Skills" until I figure out a better classification system).
|
||||
options:
|
||||
- Agent Skills
|
||||
- Workflows & Knowledge Guides
|
||||
- Tooling
|
||||
- Status Lines
|
||||
- Hooks
|
||||
- Output Styles
|
||||
- Slash-Commands
|
||||
- CLAUDE.md Files
|
||||
- Alternative Clients
|
||||
- Official Documentation
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: subcategory
|
||||
attributes:
|
||||
label: Sub-Category
|
||||
description: Select a sub-category if applicable (based on your category choice above)
|
||||
options:
|
||||
- General
|
||||
- "Workflows & Knowledge Guides: Ralph Wiggum"
|
||||
- "Tooling: IDE Integrations"
|
||||
- "Tooling: Usage Monitors"
|
||||
- "Tooling: Orchestrators"
|
||||
- "Tooling: Config Managers"
|
||||
- "Slash-Commands: Version Control & Git"
|
||||
- "Slash-Commands: Code Analysis & Testing"
|
||||
- "Slash-Commands: Context Loading & Priming"
|
||||
- "Slash-Commands: Documentation & Changelogs"
|
||||
- "Slash-Commands: CI / Deployment"
|
||||
- "Slash-Commands: Project & Task Management"
|
||||
- "Slash-Commands: Miscellaneous"
|
||||
- "CLAUDE.md Files: Language-Specific"
|
||||
- "CLAUDE.md Files: Domain-Specific"
|
||||
- "CLAUDE.md Files: Project Scaffolding & MCP"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: primary_link
|
||||
attributes:
|
||||
label: Primary Link
|
||||
description: The main URL for your resource (must start with https://). If you have a GitHub repo and a website, _use the GitHub repo_.
|
||||
placeholder: "https://github.com/username/repository"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: author_name
|
||||
attributes:
|
||||
label: Author Name
|
||||
description: "The author's name, alias, or GitHub username. (You may submit public/open-source resources that you do not own.)"
|
||||
placeholder: "Jane Doe or janedoe"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: author_link
|
||||
attributes:
|
||||
label: Author Link
|
||||
description: "Link to author's GitHub profile or personal website"
|
||||
placeholder: "https://github.com/janedoe"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: license
|
||||
attributes:
|
||||
label: License
|
||||
description: Select the license for your resource (or choose 'Other' to specify something unlisted).
|
||||
options:
|
||||
- MIT
|
||||
- Apache-2.0
|
||||
- GPL-3.0
|
||||
- BSD-3-Clause
|
||||
- ISC
|
||||
- MPL-2.0
|
||||
- AGPL-3.0
|
||||
- Unlicense
|
||||
- CC0-1.0
|
||||
- CC-BY-4.0
|
||||
- CC-BY-SA-4.0
|
||||
- "©"
|
||||
- Other (specify below)
|
||||
- No License / Not Specified
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: license_other
|
||||
attributes:
|
||||
label: Other License
|
||||
description: If you selected "Other" above, please specify the license
|
||||
placeholder: "e.g., BSD-2-Clause, Proprietary"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: "A brief description of your resource (1-3 sentences maximum, no emojis) - follow the list's style - be descriptive, not promotional - do not address the reader"
|
||||
placeholder: "Describe what your resource does and its key features..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
The following three fields are encouraged for all users. If you are recommending a plugin, skill, collection, framework, etc., then these are **mandatory**.
|
||||
|
||||
- type: textarea
|
||||
id: validate_claims
|
||||
attributes:
|
||||
label: Validate Claims
|
||||
description: "If you are submitting a complicated resource that gives Claude Code super-powers, suggest a low-friction way for me, or anyone, to prove it to themselves that what you're claiming is true. If you are submitting a plugin, skill, framework, or similar, this field is mandatory."
|
||||
placeholder: "e.g., install this Skill and ask Claude how many times the letter 'r' appears in your codebase"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: validate_claim_part_2
|
||||
attributes:
|
||||
label: Specific Task(s)
|
||||
description: "Tell me at least one specific task I should give to Claude Code to demonstrate the value of your resource."
|
||||
placeholder: "e.g., install this Skill and give Claude a counting task."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: validate_claims_part_3
|
||||
attributes:
|
||||
label: Specific Prompt(s)
|
||||
description: "Tell me what to say to Claude Code when I give it the task above. The more I have to figure things out for myself, the more likely it is that I will miss the unique value of your resource. So you are advised to be as specific as possible."
|
||||
placeholder: "Ask Claude how many times the letter 'r' appears in your codebase"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional_comments
|
||||
attributes:
|
||||
label: Additional Comments
|
||||
description: "Optional - Any additional information you'd like to share about your resource (not processed during validation)"
|
||||
placeholder: "e.g., context about why you created this, special features, acknowledgments, etc."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Recommendation Checklist
|
||||
description: Please confirm the following
|
||||
options:
|
||||
- label: "I have checked that this resource hasn't already been submitted"
|
||||
required: true
|
||||
- label: It has been over one week since the first public commit to the repo I am recommending
|
||||
required: true
|
||||
- label: All provided links are working and publicly accessible
|
||||
required: true
|
||||
- label: I do NOT have any other open issues in this repository
|
||||
required: true
|
||||
- label: I am primarily composed of human-y stuff and not electrical circuits
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## What happens next?
|
||||
|
||||
1. **Automated Validation**: Our bot will validate the well-formed-ness of this Issue and let you know if anything needs to be fixed
|
||||
2. **Review**: If validation passes, you should go back to working on your library - your recommendation has been received. It will be reviewed at the discretion of the maintainer.
|
||||
3. **Approval**: If approved, a PR will be automatically created with your resource
|
||||
4. **Notification**: You'll be notified when your resource is added
|
||||
|
||||
Thank you for contributing to Awesome Claude Code. I have
|
||||
65
.agent/knowledge/awesome_claude/.github/ISSUE_TEMPLATE/repository-enhancement.yml
vendored
Normal file
65
.agent/knowledge/awesome_claude/.github/ISSUE_TEMPLATE/repository-enhancement.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: 💡 Repository Enhancement
|
||||
description: Suggest an improvement to the repository structure, categories, or processes
|
||||
title: "[Enhancement]: "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Repository Enhancement Suggestion
|
||||
|
||||
Use this form to suggest improvements to Awesome Claude Code itself (not for submitting resources).
|
||||
|
||||
- type: dropdown
|
||||
id: enhancement_type
|
||||
attributes:
|
||||
label: Enhancement Type
|
||||
description: What kind of improvement are you suggesting?
|
||||
options:
|
||||
- New category or subcategory
|
||||
- Repository structure
|
||||
- Submission process
|
||||
- Documentation
|
||||
- Automation/workflows
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Describe your enhancement suggestion in detail
|
||||
placeholder: "Explain what you'd like to see improved and why..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: benefit
|
||||
attributes:
|
||||
label: Expected Benefit
|
||||
description: How will this enhancement help the community?
|
||||
placeholder: "This would help users by..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: implementation
|
||||
attributes:
|
||||
label: Possible Implementation
|
||||
description: If you have ideas on how to implement this, please share
|
||||
placeholder: "One way to implement this could be..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
options:
|
||||
- label: I've checked that this enhancement hasn't already been suggested
|
||||
required: true
|
||||
- label: This enhancement would improve the repository for the community
|
||||
required: true
|
||||
73
.agent/knowledge/awesome_claude/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
73
.agent/knowledge/awesome_claude/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# Pull Request
|
||||
|
||||
If you want to submit a resource for recommendation for Awesome Claude Code, please use the [resource recommendation issue form](https://github.com/hesreallyhim/awesome-claude-code/issues/new?template=recommen-resource.yml) and don't open a PR.
|
||||
|
||||
It's fairly uncommon for anyone to open a PR to this repo, even the maintainer. However, if you've noticed a technical problem/bug or a documentation problem, then this may be appropriate. Otherwise, in general, only the bots get to make PRs.
|
||||
|
||||
## Type of Contribution
|
||||
|
||||
<!-- Select ONE by marking with an [x] -->
|
||||
|
||||
- [ ] **New Resource** - Adding a new resource to the list [ONLY THE BOT MAY DO THIS]
|
||||
- [ ] **Update Resource** - Updating existing resource information (e.g., broken link, license info)
|
||||
- [ ] **Repository Improvement** - Improving the repository itself (not adding resources) [Use [this issue template](https://github.com/hesreallyhim/awesome-claude-code/issues/new?template=repository-enhancement.yml) to suggest general improvements]
|
||||
|
||||
---
|
||||
|
||||
## For New Resources
|
||||
|
||||
### Resource Information
|
||||
|
||||
- **Display Name**: <!-- e.g., "Claude Task Manager" or "/commit" -->
|
||||
- **Category**: <!-- Select from: Workflows & Knowledge Guides, Tooling, Hooks, Slash-Commands, CLAUDE.md Files, Official Documentation -->
|
||||
- **Sub-Category** (if applicable): <!-- e.g., "Version Control & Git", "Code Analysis & Testing" -->
|
||||
- **Primary Link**: <!-- The main URL for the resource -->
|
||||
- **Author Name**: <!-- Creator/maintainer name -->
|
||||
- **Author Link**: <!-- Link to author's profile -->
|
||||
- **License** (if known): <!-- e.g., MIT, Apache-2.0, GPL-3.0 -->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- 1-2 sentences describing what the resource does and why it's valuable to Claude Code users -->
|
||||
|
||||
### Automated Notification
|
||||
|
||||
<!-- Check if applicable -->
|
||||
- [ ] This is a GitHub-hosted resource and will receive an automatic notification issue when merged
|
||||
|
||||
---
|
||||
|
||||
## For Resource Updates
|
||||
|
||||
### What Changed?
|
||||
|
||||
<!-- Describe what you're updating -->
|
||||
|
||||
- **Resource Name**:
|
||||
- **Change Type**: <!-- e.g., Fix broken link, Update license, Update description -->
|
||||
- **Details**:
|
||||
|
||||
---
|
||||
|
||||
## For Repository Improvements
|
||||
|
||||
### Description of Changes
|
||||
|
||||
<!-- Describe what you're improving and why -->
|
||||
|
||||
### Checklist for Repository Changes
|
||||
|
||||
- [ ] Changes follow existing code style
|
||||
- [ ] Updated relevant documentation
|
||||
- [ ] Tested changes locally
|
||||
- [ ] Pre-commit hooks pass
|
||||
|
||||
---
|
||||
|
||||
## Additional Notes
|
||||
|
||||
<!-- Any additional context that would help reviewers -->
|
||||
|
||||
## Questions?
|
||||
|
||||
- See [CONTRIBUTING.md](../docs/CONTRIBUTING.md) for detailed contribution guidelines
|
||||
200
.agent/knowledge/awesome_claude/.github/workflows/README.md
vendored
Normal file
200
.agent/knowledge/awesome_claude/.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
# GitHub Workflows
|
||||
|
||||
This directory contains GitHub Action workflows for repository maintenance, resource submission handling, and health monitoring.
|
||||
|
||||
---
|
||||
|
||||
## Workflow: Validate New Issue
|
||||
|
||||
**File:** `.github/workflows/validate-new-issue.yml`
|
||||
|
||||
### Purpose
|
||||
|
||||
Handles all new issues opened in the repository with two mutually exclusive jobs:
|
||||
|
||||
1. **validate-resource**: Validates properly-submitted resource recommendations (issues with `resource-submission` label)
|
||||
2. **detect-informal**: Detects informal submissions that bypassed the issue template (issues without the label)
|
||||
|
||||
### Trigger
|
||||
|
||||
- `issues.opened` - New issue created
|
||||
- `issues.reopened` - Issue reopened
|
||||
- `issues.edited` - Issue body edited
|
||||
|
||||
### Job 1: Validate Resource Submission
|
||||
|
||||
Runs when an issue has the `resource-submission` label (applied automatically by the issue template).
|
||||
|
||||
**Behavior:**
|
||||
- Parses the issue body using `scripts/resources/parse_issue_form.py`
|
||||
- Validates all required fields (display name, category, URLs, etc.)
|
||||
- Checks for duplicate resources in `THE_RESOURCES_TABLE.csv`
|
||||
- Validates URL accessibility
|
||||
- Posts validation results as a comment
|
||||
- Updates labels: `validation-passed` or `validation-failed`
|
||||
- Notifies maintainer when changes are made after `/request-changes`
|
||||
|
||||
### Job 2: Detect Informal Submission
|
||||
|
||||
Runs when a **new** issue does NOT have the `resource-submission` label.
|
||||
|
||||
**Purpose:** Catches users who try to recommend resources without using the official template.
|
||||
|
||||
**Detection Signals:**
|
||||
|
||||
| Signal Type | Examples | Weight |
|
||||
|-------------|----------|--------|
|
||||
| Template field labels | `Display Name:`, `Category:`, `Primary Link:` | Very strong (+0.7 for 3+) |
|
||||
| Submission language | "recommend", "submit", "please add" | Strong (+0.3 each) |
|
||||
| Resource mentions | "plugin", "skill", "hook", "slash command" | Medium (+0.15 each) |
|
||||
| GitHub URLs | `github.com/user/repo` | Medium (+0.15) |
|
||||
| License mentions | MIT, Apache, GPL | Medium (+0.15) |
|
||||
| Bug/question language | "bug", "error", "how do I" | Negative (-0.2 each) |
|
||||
|
||||
**Two-Tier Response:**
|
||||
|
||||
| Confidence | Action |
|
||||
|------------|--------|
|
||||
| ≥ 0.6 (High) | Add `needs-template` label, post warning, **auto-close** |
|
||||
| 0.4 - 0.6 (Medium) | Add `needs-template` label, post gentle warning, **leave open** |
|
||||
| < 0.4 (Low) | No action |
|
||||
|
||||
### Local Usage
|
||||
|
||||
```bash
|
||||
# Test informal submission detection
|
||||
ISSUE_TITLE="Check out my plugin" ISSUE_BODY="I made this tool at github.com/user/repo" \
|
||||
python -m scripts.resources.detect_informal_submission
|
||||
```
|
||||
|
||||
### Related Scripts
|
||||
|
||||
- `scripts/resources/parse_issue_form.py` - Parses and validates issue form data
|
||||
- `scripts/resources/detect_informal_submission.py` - Detects informal submissions
|
||||
|
||||
---
|
||||
|
||||
## Workflow: Handle Resource Submission Commands
|
||||
|
||||
**File:** `.github/workflows/handle-resource-submission-commands.yml`
|
||||
|
||||
### Purpose
|
||||
|
||||
Processes maintainer commands on resource submission issues.
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description | Requirements |
|
||||
|---------|-------------|--------------|
|
||||
| `/approve` | Creates PR to add resource to CSV | Issue must have `validation-passed` label |
|
||||
| `/reject [reason]` | Closes issue as rejected | Maintainer permission |
|
||||
| `/request-changes [message]` | Requests changes from submitter | Maintainer permission |
|
||||
|
||||
### Trigger
|
||||
|
||||
- `issue_comment.created` on issues with `resource-submission` label
|
||||
- Only processes comments from OWNER, MEMBER, or COLLABORATOR
|
||||
|
||||
---
|
||||
|
||||
## Workflow: Update GitHub Release Data
|
||||
|
||||
**File:** `.github/workflows/update-github-release-data.yml`
|
||||
|
||||
### Purpose
|
||||
|
||||
Updates `THE_RESOURCES_TABLE.csv` with:
|
||||
- Latest commit date on the default branch (Last Modified)
|
||||
- Latest GitHub Release date (Latest Release)
|
||||
- Latest GitHub Release version (Release Version)
|
||||
|
||||
### Schedule
|
||||
|
||||
- Runs automatically every day at **3:00 AM UTC**
|
||||
- Can be triggered manually via the GitHub Actions UI
|
||||
|
||||
### Local Usage
|
||||
|
||||
```bash
|
||||
python -m scripts.maintenance.update_github_release_data
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
```bash
|
||||
python -m scripts.maintenance.update_github_release_data --help
|
||||
```
|
||||
|
||||
- `--csv-file`: Path to CSV file (default: THE_RESOURCES_TABLE.csv)
|
||||
- `--max`: Process at most N resources
|
||||
- `--dry-run`: Print updates without writing changes
|
||||
|
||||
## Workflow: Check Repository Health
|
||||
|
||||
**File:** `.github/workflows/check-repo-health.yml`
|
||||
|
||||
### Purpose
|
||||
|
||||
Ensures that active GitHub repositories in the resource list are still maintained and responsive by checking:
|
||||
- Number of open issues
|
||||
- Date of last push or PR merge (last updated)
|
||||
|
||||
### Behavior
|
||||
|
||||
The workflow will **fail** if any repository:
|
||||
- Has not been updated in over **6 months** AND
|
||||
- Has more than **2 open issues**
|
||||
|
||||
Deleted or private repositories are logged as warnings but do not cause the workflow to fail.
|
||||
|
||||
### Schedule
|
||||
|
||||
- Runs automatically every **Monday at 9:00 AM UTC**
|
||||
- Can be triggered manually via the GitHub Actions UI
|
||||
|
||||
### Local Usage
|
||||
|
||||
You can run the health check locally using:
|
||||
|
||||
```bash
|
||||
make check-repo-health
|
||||
```
|
||||
|
||||
Or directly with Python:
|
||||
|
||||
```bash
|
||||
python3 -m scripts.maintenance.check_repo_health
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
```bash
|
||||
python3 -m scripts.maintenance.check_repo_health --help
|
||||
```
|
||||
|
||||
- `--csv-file`: Path to CSV file (default: THE_RESOURCES_TABLE.csv)
|
||||
- `--months`: Months threshold for outdated repos (default: 6)
|
||||
- `--issues`: Open issues threshold (default: 2)
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
INFO: Reading repository list from THE_RESOURCES_TABLE.csv
|
||||
INFO: Checking owner/repo (Resource Name)
|
||||
INFO:
|
||||
============================================================
|
||||
INFO: Summary:
|
||||
INFO: Total active GitHub repositories checked: 50
|
||||
INFO: Deleted/unavailable repositories: 2
|
||||
INFO: Problematic repositories: 0
|
||||
INFO:
|
||||
============================================================
|
||||
INFO: ✅ HEALTH CHECK PASSED
|
||||
INFO: All active repositories are healthy!
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `GITHUB_TOKEN`: GitHub personal access token or Actions token (recommended to avoid rate limiting)
|
||||
|
||||
The GitHub Actions workflow automatically uses the `GITHUB_TOKEN` secret provided by GitHub Actions.
|
||||
47
.agent/knowledge/awesome_claude/.github/workflows/check-repo-health.yml
vendored
Normal file
47
.agent/knowledge/awesome_claude/.github/workflows/check-repo-health.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Check Repository Health
|
||||
|
||||
# This workflow checks the health of active GitHub repositories listed in THE_RESOURCES_TABLE.csv.
|
||||
# It verifies that repositories are still active and maintained by checking:
|
||||
# - Number of open issues
|
||||
# - Date of last push or PR merge
|
||||
#
|
||||
# The workflow will fail if any repository:
|
||||
# - Has not been updated in over 6 months AND
|
||||
# - Has more than 2 open issues
|
||||
#
|
||||
# Deleted repositories are logged but do not cause the workflow to fail.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run weekly on Monday at 9:00 AM UTC
|
||||
- cron: '0 9 * * 1'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-repo-health:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -e ".[dev]"
|
||||
|
||||
- name: Run repository health check
|
||||
run: |
|
||||
python3 -m scripts.maintenance.check_repo_health
|
||||
40
.agent/knowledge/awesome_claude/.github/workflows/ci.yml
vendored
Normal file
40
.agent/knowledge/awesome_claude/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
docs_tree_check:
|
||||
description: "Fail CI if README tree is out of date"
|
||||
type: boolean
|
||||
default: true
|
||||
docs_tree_debug:
|
||||
description: "Print diff/context on mismatch"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: true
|
||||
# Defaults for push/PR
|
||||
DOCS_TREE_CHECK: "1"
|
||||
DOCS_TREE_DEBUG: "0"
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
|
||||
- name: Run CI checks
|
||||
run: make ci
|
||||
env:
|
||||
# Override defaults only for workflow_dispatch
|
||||
DOCS_TREE_CHECK: ${{ github.event_name == 'workflow_dispatch' && (inputs.docs_tree_check && '1' || '0') || '1' }}
|
||||
DOCS_TREE_DEBUG: ${{ github.event_name == 'workflow_dispatch' && (inputs.docs_tree_debug && '1' || '0') || '0' }}
|
||||
154
.agent/knowledge/awesome_claude/.github/workflows/close-resource-pr.yml
vendored
Normal file
154
.agent/knowledge/awesome_claude/.github/workflows/close-resource-pr.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
name: Close Resource Submission PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
detect-and-close:
|
||||
name: Detect Resource Submission PR
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Skip PRs from bots (GitHub Actions bot, Dependabot, etc.)
|
||||
if: github.event.pull_request.user.type != 'Bot'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check if PR is a resource submission
|
||||
id: detect
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title || '';
|
||||
const body = context.payload.pull_request.body || '';
|
||||
const combined = `${title}\n${body}`.toLowerCase();
|
||||
|
||||
// ── High-signal title patterns ──────────────────────────
|
||||
const highSignalTitlePatterns = [
|
||||
// "Add [resource]: My Tool" or "Add [resource]: My Tool to Hooks"
|
||||
/^add\s*\[resource\]\s*:/i,
|
||||
// "[Resource]: My Tool"
|
||||
/^\[resource\]\s*:/i,
|
||||
// "Add <name> to <section>" (common PR title for list additions)
|
||||
/^add\s+.+\s+to\s+(slash.?commands?|hooks?|claude\.?md|tooling|skills?|agent|mcp|plugins?|workflows?|status.?lines?)/i,
|
||||
];
|
||||
|
||||
let titleHighSignal = false;
|
||||
for (const pattern of highSignalTitlePatterns) {
|
||||
if (pattern.test(title)) {
|
||||
titleHighSignal = true;
|
||||
console.log(`High-signal title match: ${pattern}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body phrase patterns (medium signal) ────────────────
|
||||
const bodyPhrases = [
|
||||
/add(ing)?\s+(a\s+)?(new\s+)?resource/i,
|
||||
/submit(ting)?\s+(a\s+)?resource/i,
|
||||
/resource\s+(submission|recommendation)/i,
|
||||
/please\s+add\s+(this|my)/i,
|
||||
/adding\s+.+\s+to\s+the\s+(list|awesome\s+list)/i,
|
||||
/new\s+entry\s+(for|in)\s+/i,
|
||||
/recommend(ing)?\s+(this|a)\s+(tool|resource|project)/i,
|
||||
];
|
||||
|
||||
let bodyMatchCount = 0;
|
||||
for (const pattern of bodyPhrases) {
|
||||
if (pattern.test(combined)) {
|
||||
bodyMatchCount++;
|
||||
console.log(`Body phrase match: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CSV / README file changes (very high signal) ────────
|
||||
// Check if the PR touches THE_RESOURCES_TABLE.csv or README.md
|
||||
const files = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
let touchesResourceFiles = false;
|
||||
for (const file of files.data) {
|
||||
if (
|
||||
file.filename === 'THE_RESOURCES_TABLE.csv' ||
|
||||
file.filename === 'README.md' ||
|
||||
file.filename.startsWith('README_ALTERNATIVES/')
|
||||
) {
|
||||
touchesResourceFiles = true;
|
||||
console.log(`Touches resource file: ${file.filename}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Decision logic ──────────────────────────────────────
|
||||
// High-signal title alone is enough
|
||||
// Body phrases + resource file changes is enough
|
||||
// 2+ body phrase matches is enough
|
||||
const isResourceSubmission =
|
||||
titleHighSignal ||
|
||||
(bodyMatchCount >= 1 && touchesResourceFiles) ||
|
||||
bodyMatchCount >= 2;
|
||||
|
||||
console.log(`Title high signal: ${titleHighSignal}`);
|
||||
console.log(`Body match count: ${bodyMatchCount}`);
|
||||
console.log(`Touches resource files: ${touchesResourceFiles}`);
|
||||
console.log(`Is resource submission: ${isResourceSubmission}`);
|
||||
|
||||
core.setOutput('is_resource_submission', isResourceSubmission.toString());
|
||||
|
||||
- name: Post comment and close PR
|
||||
if: steps.detect.outputs.is_resource_submission == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const templateUrl = `https://github.com/${owner}/${repo}/issues/new?template=recommend-resource.yml`;
|
||||
const contributingUrl = `https://github.com/${owner}/${repo}/blob/main/docs/CONTRIBUTING.md`;
|
||||
|
||||
const body = [
|
||||
'## ⚠️ Resource submissions are not accepted via pull request',
|
||||
'',
|
||||
'Thank you for your interest in contributing to Awesome Claude Code!',
|
||||
'',
|
||||
'However, resource recommendations **must** be submitted through our issue template, not as a pull request. The entire resource pipeline — validation, review, and merging — is managed by automation. Even the maintainer does not use PRs to add entries to the list.',
|
||||
'',
|
||||
'**To submit your resource correctly:**',
|
||||
'',
|
||||
`1. 📖 Read the [CONTRIBUTING.md](${contributingUrl}) document`,
|
||||
`2. 📝 [Submit your resource using the official template](${templateUrl})`,
|
||||
'3. ✅ The bot will validate your submission automatically',
|
||||
'4. 👀 A maintainer will review it once validation passes',
|
||||
'',
|
||||
'If this PR is **not** a resource submission (e.g., it\'s a bug fix or improvement), please comment below and we\'ll reopen it.',
|
||||
'',
|
||||
'---',
|
||||
'*This PR has been automatically closed.*',
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body,
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: ['needs-template'],
|
||||
});
|
||||
142
.agent/knowledge/awesome_claude/.github/workflows/close-resource-prs.yml
vendored
Normal file
142
.agent/knowledge/awesome_claude/.github/workflows/close-resource-prs.yml
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
name: Close Resource Submission PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
detect-and-close:
|
||||
name: Classify and Close Resource PRs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: github.event.pull_request.user.type != 'Bot'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const files = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 50,
|
||||
});
|
||||
return files.data.map(f => f.filename).join('\n');
|
||||
result-encoding: string
|
||||
|
||||
- name: Classify PR with Claude
|
||||
id: classify
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_FILES: ${{ steps.files.outputs.result }}
|
||||
run: |
|
||||
# Use jq to safely construct JSON (handles all escaping)
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg body "${PR_BODY:0:2000}" \
|
||||
--arg files "$PR_FILES" \
|
||||
'{
|
||||
model: "claude-haiku-4-5-20251001",
|
||||
max_tokens: 50,
|
||||
system: "You classify GitHub pull requests for the awesome-claude-code repository (a curated awesome-list of tools, skills, hooks, and resources for the coding agent Claude Code).\n\nA \"resource submission\" is any PR that attempts to add, recommend, or promote a tool, project, library, skill, MCP server, hook, workflow, guide, or ANY resource whatsoever to the list. This includes any PR that edit THE_RESOURCES_TABLE.csv.\n\nA \"not resource submission\" is a PR that fixes bugs, improves CI/workflows, corrects typos, updates documentation about the repo itself (not adding a new external resource), refactors code, or makes other repository maintenance changes.\n\nRespond with ONLY a JSON object, no markdown fences: {\"classification\": \"resource_submission\" | \"not_resource_submission\", \"confidence\": \"high\" | \"low\"}",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: ("PR Title: " + $title + "\n\nPR Body:\n" + $body + "\n\nChanged files:\n" + $files)
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d "$PAYLOAD") || {
|
||||
echo "API call failed"
|
||||
echo "classification=error" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=none" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Extract Claude's text response, then parse the JSON within it
|
||||
TEXT=$(echo "$RESPONSE" | jq -r '.content[0].text')
|
||||
echo "Claude response: $TEXT"
|
||||
|
||||
CLASSIFICATION=$(echo "$TEXT" | jq -r '.classification // "error"')
|
||||
CONFIDENCE=$(echo "$TEXT" | jq -r '.confidence // "low"')
|
||||
|
||||
echo "Classification: $CLASSIFICATION"
|
||||
echo "Confidence: $CONFIDENCE"
|
||||
echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=$CONFIDENCE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Post comment and close PR
|
||||
if: steps.classify.outputs.classification == 'resource_submission'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const templateUrl = `https://github.com/${owner}/${repo}/issues/new?template=recommend-resource.yml`;
|
||||
const contributingUrl = `https://github.com/${owner}/${repo}/blob/main/docs/CONTRIBUTING.md`;
|
||||
|
||||
const body = [
|
||||
'## ⚠️ Resource recommendations are not accepted via pull request',
|
||||
'',
|
||||
'Thank you for your interest in contributing to Awesome Claude Code!',
|
||||
'',
|
||||
'However, resource recommendations **must** be submitted through our issue template, not as a pull request. The entire resource pipeline — validation, review, and merging — is managed by automatioEven the maintainer does not use PRs to add entries to the list.',
|
||||
'',
|
||||
'**To submit your resource correctly:**',
|
||||
'',
|
||||
`1. 📖 Read the [CONTRIBUTING.md](${contributingUrl}) document`,
|
||||
`2. 📝 [Submit your resource using the official template](${templateUrl})`,
|
||||
'3. ✅ The bot will validate your submission automatically',
|
||||
'4. 👀 A maintainer will review it once validation passes',
|
||||
'',
|
||||
'If this PR is **not** a resource submission (e.g., it\'s a bug fix or improvement), please comment below and we\'ll reopen it.',
|
||||
'',
|
||||
'---',
|
||||
'*This PR was automatically closed.*',
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body,
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: ['needs-template'],
|
||||
});
|
||||
|
||||
- name: Flag low-confidence non-resource PR for review
|
||||
if: steps.classify.outputs.classification == 'not_resource_submission' && steps.classify.outputs.confidence == 'low'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['needs-review'],
|
||||
});
|
||||
216
.agent/knowledge/awesome_claude/.github/workflows/handle-resource-submission-commands.yml
vendored
Normal file
216
.agent/knowledge/awesome_claude/.github/workflows/handle-resource-submission-commands.yml
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
name: Handle Resource Submission Commands
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
process-commands:
|
||||
# Only run when:
|
||||
# 1. Comment is on an issue (not a PR)
|
||||
# 2. Issue has resource-submission label
|
||||
# 3. Commenter has write permissions (maintainer/owner)
|
||||
# 4. Comment contains one of the commands: /approve, /reject, /request-changes
|
||||
if: |
|
||||
github.event.issue.pull_request == null &&
|
||||
contains(github.event.issue.labels.*.name, 'resource-submission') &&
|
||||
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') &&
|
||||
(contains(github.event.comment.body, '/approve') || contains(github.event.comment.body, '/reject') || contains(github.event.comment.body, '/request-changes'))
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install PyYAML requests PyGithub python-dotenv
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: React to approval comment
|
||||
if: contains(github.event.comment.body, '/approve') && contains(github.event.issue.labels.*.name, 'validation-passed')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'rocket'
|
||||
});
|
||||
|
||||
- name: Parse issue and create PR
|
||||
id: create_pr
|
||||
if: contains(github.event.comment.body, '/approve') && contains(github.event.issue.labels.*.name, 'validation-passed')
|
||||
env:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
run: |
|
||||
# TODO: Consider emitting issue parsing output via GITHUB_OUTPUT to avoid temp files.
|
||||
# First parse the issue to get resource data
|
||||
python -m scripts.resources.parse_issue_form > resource_data.json
|
||||
|
||||
# Create the PR with the resource
|
||||
python -m scripts.resources.create_resource_pr \
|
||||
--issue-number $ISSUE_NUMBER \
|
||||
--resource-data resource_data.json
|
||||
|
||||
- name: Comment on issue with results
|
||||
if: contains(github.event.comment.body, '/approve') && contains(github.event.issue.labels.*.name, 'validation-passed')
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
CREATE_PR_SUCCESS: ${{ steps.create_pr.outputs.success }}
|
||||
PR_URL: ${{ steps.create_pr.outputs.pr_url }}
|
||||
with:
|
||||
script: |
|
||||
const pr_url = process.env.PR_URL || null;
|
||||
const success = (process.env.CREATE_PR_SUCCESS || '').toLowerCase() === 'true';
|
||||
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
let comment_body = '## ✅ Resource Approved!\n\n';
|
||||
|
||||
if (success && pr_url && pr_url !== 'null') {
|
||||
comment_body += `🎉 A pull request has been created with your resource: ${pr_url}\n\n`;
|
||||
comment_body += 'The PR will be merged shortly, and you\'ll be notified when your resource is live.\n\n';
|
||||
comment_body += 'Thank you for contributing to Awesome Claude Code!';
|
||||
|
||||
// Add approved label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
labels: ['approved', 'pr-created']
|
||||
});
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
state: 'closed',
|
||||
state_reason: 'completed'
|
||||
});
|
||||
} else {
|
||||
comment_body += '❌ There was an error creating the pull request.\n\n';
|
||||
comment_body += 'Please check the workflow logs for details.';
|
||||
|
||||
// Add error label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
labels: ['error-creating-pr']
|
||||
});
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: comment_body
|
||||
});
|
||||
|
||||
- name: Handle rejection
|
||||
if: contains(github.event.comment.body, '/reject')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const comment = context.payload.comment.body;
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
// Extract rejection reason
|
||||
const reasonMatch = comment.match(/\/reject\s+(.*)/);
|
||||
const reason = reasonMatch ? reasonMatch[1] : 'No reason provided';
|
||||
|
||||
// Add rejection comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: `## ❌ Submission Rejected\n\n**Reason:** ${reason}\n\n`
|
||||
});
|
||||
|
||||
// Update labels and close
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
labels: ['rejected']
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
|
||||
- name: React to request changes command
|
||||
if: contains(github.event.comment.body, '/request-changes')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id,
|
||||
content: 'eyes'
|
||||
});
|
||||
|
||||
- name: Handle request changes
|
||||
if: contains(github.event.comment.body, '/request-changes')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const comment = context.payload.comment.body;
|
||||
const issue_number = context.issue.number;
|
||||
|
||||
// Extract requested changes
|
||||
const changesMatch = comment.match(/\/request-changes\s+(.*)/s);
|
||||
const changes = changesMatch ? changesMatch[1] : 'Please review the submission requirements.';
|
||||
|
||||
// Add comment with maintainer mention
|
||||
const maintainer = context.payload.comment.user.login;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: `## 🔄 Changes Requested by @${maintainer}\n\n${changes}\n\nPlease edit your issue to address these points. The validation will run again automatically after you make changes.`
|
||||
});
|
||||
|
||||
// Update labels
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
labels: ['changes-requested']
|
||||
});
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f pr_result.json resource_data.json
|
||||
114
.agent/knowledge/awesome_claude/.github/workflows/notify-on-merge.yml
vendored
Normal file
114
.agent/knowledge/awesome_claude/.github/workflows/notify-on-merge.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Send Badge Notification on Resource PR Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
notify-if-resource-pr:
|
||||
# Only run when:
|
||||
# 1. PR was merged (not just closed)
|
||||
# 2. PR was created by github-actions bot (automated resource PR)
|
||||
# 3. PR does NOT have the 'do-not-disturb' label (allows skipping notifications)
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.user.login == 'github-actions[bot]' &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'do-not-disturb')
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Checkout the merged commit
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install PyGithub python-dotenv
|
||||
|
||||
- name: Extract resource information from PR
|
||||
id: extract_resource
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
with:
|
||||
script: |
|
||||
const pr_body = process.env.PR_BODY || '';
|
||||
const pr_title = process.env.PR_TITLE || '';
|
||||
|
||||
// Look for GitHub URL in PR body
|
||||
// PRs created by approve-resource-submission.yml typically have format:
|
||||
// "Adds new resource: [Resource Name](URL)"
|
||||
const urlMatch = pr_body.match(/\*\*Primary Link\*\*:\s*(https:\/\/github\.com\/[^\s\)]+)/i) ||
|
||||
pr_body.match(/Primary Link:\s*(https:\/\/github\.com\/[^\s\)]+)/i) ||
|
||||
pr_body.match(/\[.*?\]\((https:\/\/github\.com\/[^\)]+)\)/);
|
||||
|
||||
// Extract resource name from PR title or body
|
||||
const nameMatch = pr_title.match(/Add[s]?\s+(?:new\s+)?resource:\s*(.+)/i) ||
|
||||
pr_body.match(/\*\*Display Name\*\*:\s*(.+)/i) ||
|
||||
pr_body.match(/Display Name:\s*(.+)/i);
|
||||
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
const github_url = urlMatch[1].trim();
|
||||
const resource_name = nameMatch ? nameMatch[1].trim() : '';
|
||||
|
||||
console.log(`Found GitHub repository: ${github_url}`);
|
||||
console.log(`Resource name: ${resource_name || 'Not specified'}`);
|
||||
|
||||
// Set outputs for next steps
|
||||
core.setOutput('github_url', github_url);
|
||||
core.setOutput('resource_name', resource_name);
|
||||
core.setOutput('is_github_repo', 'true');
|
||||
} else {
|
||||
console.log('No GitHub repository URL found in PR - skipping notification');
|
||||
core.setOutput('is_github_repo', 'false');
|
||||
}
|
||||
|
||||
- name: Send badge notification
|
||||
if: steps.extract_resource.outputs.is_github_repo == 'true'
|
||||
env:
|
||||
AWESOME_CC_PAT_PUBLIC_REPO: ${{ secrets.AWESOME_CC_PAT_PUBLIC_REPO }}
|
||||
REPOSITORY_URL: ${{ steps.extract_resource.outputs.github_url }}
|
||||
RESOURCE_NAME: ${{ steps.extract_resource.outputs.resource_name }}
|
||||
DESCRIPTION: "" # Will use default description
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
run: |
|
||||
echo "Sending notification to: $REPOSITORY_URL"
|
||||
python -m scripts.badges.badge_notification || {
|
||||
echo "⚠️ Failed to send notification, but continuing workflow"
|
||||
echo "This might happen if:"
|
||||
echo "- The repository has issues disabled"
|
||||
echo "- The repository is private"
|
||||
echo "- We've already sent a notification"
|
||||
exit 0
|
||||
}
|
||||
|
||||
- name: Log notification result
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const is_github_repo = '${{ steps.extract_resource.outputs.is_github_repo }}';
|
||||
const github_url = '${{ steps.extract_resource.outputs.github_url }}';
|
||||
const resource_name = '${{ steps.extract_resource.outputs.resource_name }}';
|
||||
|
||||
if (is_github_repo === 'true') {
|
||||
console.log('✅ Notification workflow completed for:');
|
||||
console.log(` Repository: ${github_url}`);
|
||||
console.log(` Resource: ${resource_name || 'Unknown'}`);
|
||||
} else {
|
||||
console.log('ℹ️ No notification sent - resource is not a GitHub repository');
|
||||
}
|
||||
600
.agent/knowledge/awesome_claude/.github/workflows/submission-enforcement-v2.yml
vendored
Normal file
600
.agent/knowledge/awesome_claude/.github/workflows/submission-enforcement-v2.yml
vendored
Normal file
@@ -0,0 +1,600 @@
|
||||
name: Submission Enforcement
|
||||
# Unified workflow: cooldown enforcement for issues, Claude-powered PR
|
||||
# classification, and validation dispatch for clean issue submissions.
|
||||
#
|
||||
# Triggers:
|
||||
# issues opened/reopened → cooldown check → if clean → validate
|
||||
# issues edited → skip cooldown → validate directly
|
||||
# PR opened/reopened → classify with Claude → if resource submission → cooldown violation
|
||||
#
|
||||
# Cooldown state stored in a private ops repo as cooldown-state.json.
|
||||
# Requires ACC_OPS secret (fine-grained PAT) with:
|
||||
# - awesome-claude-code-ops: Contents read/write
|
||||
# - awesome-claude-code: Issues + Pull requests read/write
|
||||
# because we use a single token for BOTH repos in the enforcement step.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: >-
|
||||
cooldown-${{
|
||||
github.event.pull_request.user.login ||
|
||||
github.event.issue.user.login ||
|
||||
'unknown'
|
||||
}}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
OPS_OWNER: hesreallyhim
|
||||
OPS_REPO: awesome-claude-code-ops
|
||||
OPS_PATH: cooldown-state.json
|
||||
|
||||
jobs:
|
||||
enforce-cooldown:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'edited'
|
||||
outputs:
|
||||
allowed: ${{ steps.enforce.outputs.allowed }}
|
||||
repo_url: ${{ steps.enforce.outputs.repo_url }}
|
||||
cooldown_level: ${{ steps.enforce.outputs.cooldown_level }}
|
||||
|
||||
permissions:
|
||||
# These are for GITHUB_TOKEN only; our step uses ACC_OPS PAT explicitly.
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: identify-repo
|
||||
id: identify-repo
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isPR = context.eventName === 'pull_request_target';
|
||||
const author = isPR
|
||||
? context.payload.pull_request.user.login
|
||||
: context.payload.issue.user.login;
|
||||
const body = isPR
|
||||
? (context.payload.pull_request.body || '')
|
||||
: (context.payload.issue.body || '');
|
||||
|
||||
function extractUrls(text) {
|
||||
const pattern = /https?:\/\/github\.com\/([^\/\s]+)\/([^\/\s#?")\]]+)/gi;
|
||||
const results = [];
|
||||
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const owner = match[1];
|
||||
const repo = match[2]
|
||||
.replace(/\.git$/i, '')
|
||||
.replace(/[.,;:!?]+$/, '');
|
||||
if (!owner || !repo) continue;
|
||||
results.push({
|
||||
owner,
|
||||
repo,
|
||||
url: `https://github.com/${owner}/${repo}`,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function firstAuthorMatch(urls, authorLogin) {
|
||||
const authorLower = (authorLogin || '').toLowerCase();
|
||||
const match = urls.find(u => u.owner.toLowerCase() === authorLower);
|
||||
return match ? match.url : '';
|
||||
}
|
||||
|
||||
let repoUrl = '';
|
||||
const urls = extractUrls(body);
|
||||
|
||||
if (!isPR) {
|
||||
const linkLine = body.match(/^\s*\*\*Link:\*\*\s*(.+)\s*$/im);
|
||||
if (linkLine) {
|
||||
const templateUrls = extractUrls(linkLine[1]);
|
||||
repoUrl = firstAuthorMatch(templateUrls, author);
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoUrl) {
|
||||
repoUrl = firstAuthorMatch(urls, author);
|
||||
}
|
||||
|
||||
core.setOutput('repo_url', repoUrl);
|
||||
console.log(repoUrl ? `Repo URL identified: ${repoUrl}` : 'No matching repo URL identified.');
|
||||
|
||||
- name: Get PR changed files
|
||||
id: files
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const files = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 50,
|
||||
});
|
||||
return files.data.map(f => f.filename).join('\n');
|
||||
result-encoding: string
|
||||
|
||||
- name: Classify PR with Claude
|
||||
id: classify
|
||||
if: github.event_name == 'pull_request_target'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_FILES: ${{ steps.files.outputs.result }}
|
||||
run: |
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg body "${PR_BODY:0:2000}" \
|
||||
--arg files "$PR_FILES" \
|
||||
'{
|
||||
model: "claude-haiku-4-5-20251001",
|
||||
max_tokens: 50,
|
||||
system: "You classify GitHub pull requests for the awesome-claude-code repository (a curated awesome-list of tools, skills, hooks, and resources for Claude Code by Anthropic).\n\nA \"resource submission\" is any PR that attempts to add, recommend, or promote a tool, project, library, skill, MCP server, hook, workflow, guide, or similar resource to the list. This includes PRs that edit README.md or a resources CSV to insert a new entry.\n\nA \"not resource submission\" is a PR that fixes bugs, improves CI/workflows, corrects typos, updates documentation about the repo itself (not adding a new external resource), refactors code, or makes other repository maintenance changes.\n\nRespond with ONLY a JSON object, no markdown fences: {\"classification\": \"resource_submission\" | \"not_resource_submission\", \"confidence\": \"high\" | \"low\"}",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: ("PR Title: " + $title + "\n\nPR Body:\n" + $body + "\n\nChanged files:\n" + $files)
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "{"
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d "$PAYLOAD") || {
|
||||
echo "API call failed"
|
||||
echo "classification=error" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=none" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
}
|
||||
|
||||
RAW=$(echo "$RESPONSE" | jq -r '.content[0].text')
|
||||
TEXT="{${RAW}"
|
||||
TEXT=$(echo "$TEXT" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n')
|
||||
echo "Claude response: $TEXT"
|
||||
|
||||
CLASSIFICATION=$(echo "$TEXT" | jq -r '.classification // "error"')
|
||||
CONFIDENCE=$(echo "$TEXT" | jq -r '.confidence // "low"')
|
||||
|
||||
echo "Classification: $CLASSIFICATION"
|
||||
echo "Confidence: $CONFIDENCE"
|
||||
echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=$CONFIDENCE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enforce cooldown rules
|
||||
id: enforce
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
OPS_OWNER: ${{ env.OPS_OWNER }}
|
||||
OPS_REPO: ${{ env.OPS_REPO }}
|
||||
OPS_PATH: ${{ env.OPS_PATH }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body || '' }}
|
||||
REPO_URL: ${{ steps.identify-repo.outputs.repo_url || '' }}
|
||||
PR_CLASSIFICATION: ${{ steps.classify.outputs.classification || '' }}
|
||||
PR_CONFIDENCE: ${{ steps.classify.outputs.confidence || '' }}
|
||||
with:
|
||||
# Single-token approach: this step uses the PAT for BOTH repos.
|
||||
github-token: ${{ secrets.ACC_OPS }}
|
||||
script: |
|
||||
const opsOwner = process.env.OPS_OWNER;
|
||||
const opsRepo = process.env.OPS_REPO;
|
||||
const opsPath = process.env.OPS_PATH;
|
||||
|
||||
const isPR = context.eventName === 'pull_request_target';
|
||||
const repo = context.repo;
|
||||
const now = new Date();
|
||||
const repoUrl = process.env.REPO_URL || '';
|
||||
|
||||
const author = isPR
|
||||
? context.payload.pull_request.user.login
|
||||
: context.payload.issue.user.login;
|
||||
const number = isPR
|
||||
? context.payload.pull_request.number
|
||||
: context.payload.issue.number;
|
||||
core.setOutput('repo_url', '');
|
||||
core.setOutput('cooldown_level', '');
|
||||
|
||||
// ---- PR: skip bots ----
|
||||
if (isPR && context.payload.pull_request.user.type === 'Bot') {
|
||||
console.log(`Skipping bot PR by ${author}`);
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- PR: classification gate ----
|
||||
if (isPR) {
|
||||
const classification = process.env.PR_CLASSIFICATION;
|
||||
const confidence = process.env.PR_CONFIDENCE;
|
||||
|
||||
if (classification === 'error') {
|
||||
console.log('Classification failed — fail open.');
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
if (classification !== 'resource_submission') {
|
||||
if (confidence === 'low') {
|
||||
await github.rest.issues.addLabels({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
labels: ['needs-review'],
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
`PR #${number} classified as ${classification} (${confidence}) — no enforcement needed.`
|
||||
);
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`PR #${number} classified as resource_submission — enforcing.`);
|
||||
}
|
||||
|
||||
// ---- Issue: excused label bypass ----
|
||||
if (!isPR) {
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
if (labels.includes('excused')) {
|
||||
console.log(`Issue #${number} has excused label — skipping.`);
|
||||
core.setOutput('allowed', 'true');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Load cooldown state from ops repo ----
|
||||
let state = {};
|
||||
let fileSha = null;
|
||||
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner: opsOwner,
|
||||
repo: opsRepo,
|
||||
path: opsPath
|
||||
});
|
||||
state = JSON.parse(Buffer.from(data.content, 'base64').toString());
|
||||
fileSha = data.sha;
|
||||
console.log(`Loaded state (sha: ${fileSha})`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('No state file found. Starting fresh.');
|
||||
} else {
|
||||
console.log(`Error loading state: ${e.message}. Starting fresh.`);
|
||||
}
|
||||
}
|
||||
|
||||
const userState = state[author] || null;
|
||||
let stateChanged = false;
|
||||
|
||||
function recordViolation(reason) {
|
||||
const level = userState ? userState.cooldown_level : 0;
|
||||
|
||||
if (level >= 2) {
|
||||
// 3rd+ violation: permanent ban
|
||||
state[author] = {
|
||||
active_until: '9999-01-01T00:00:00Z',
|
||||
cooldown_level: level + 1,
|
||||
banned: true,
|
||||
last_violation: now.toISOString(),
|
||||
last_reason: reason
|
||||
};
|
||||
} else {
|
||||
// 1st violation: 7 days; 2nd violation: 14 days
|
||||
const days = level === 0 ? 7 : 14;
|
||||
const activeUntil = new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
state[author] = {
|
||||
active_until: activeUntil.toISOString(),
|
||||
cooldown_level: level + 1,
|
||||
last_violation: now.toISOString(),
|
||||
last_reason: reason
|
||||
};
|
||||
}
|
||||
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
async function closeWithComment(comment) {
|
||||
await github.rest.issues.createComment({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
if (isPR) {
|
||||
await github.rest.pulls.update({
|
||||
...repo,
|
||||
pull_number: number,
|
||||
state: 'closed'
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatRemaining(activeUntilISO) {
|
||||
const remaining = new Date(activeUntilISO) - now;
|
||||
const days = Math.ceil(remaining / (1000 * 60 * 60 * 24));
|
||||
if (days <= 0) return 'less than a day';
|
||||
if (days === 1) return '1 day';
|
||||
return `${days} days`;
|
||||
}
|
||||
|
||||
async function saveAndExit(
|
||||
allowed,
|
||||
selectedRepoUrl = '',
|
||||
selectedCooldownLevel = ''
|
||||
) {
|
||||
core.setOutput('allowed', allowed);
|
||||
core.setOutput('repo_url', selectedRepoUrl || '');
|
||||
core.setOutput('cooldown_level', selectedCooldownLevel || '');
|
||||
|
||||
if (!stateChanged) return;
|
||||
|
||||
const content = Buffer.from(JSON.stringify(state, null, 2)).toString('base64');
|
||||
|
||||
const commitMsg =
|
||||
`cooldown: ${author} — ` +
|
||||
(state[author]?.last_reason || 'clean') +
|
||||
` (#${number})`;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
owner: opsOwner,
|
||||
repo: opsRepo,
|
||||
path: opsPath,
|
||||
message: commitMsg,
|
||||
content
|
||||
};
|
||||
if (fileSha) params.sha = fileSha;
|
||||
|
||||
await github.rest.repos.createOrUpdateFileContents(params);
|
||||
console.log(`State saved: ${commitMsg}`);
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
console.log(
|
||||
`Conflict writing state (409). Violation for ${author} will be caught on next submission.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Error saving state: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// PR PATH: resource submission via PR is always a violation
|
||||
// ==========================================================
|
||||
if (isPR) {
|
||||
if (userState && userState.banned === true) {
|
||||
recordViolation('submitted-as-pr');
|
||||
} else if (userState && new Date(userState.active_until) > now) {
|
||||
recordViolation('submitted-as-pr-during-cooldown');
|
||||
} else {
|
||||
recordViolation('submitted-as-pr');
|
||||
}
|
||||
|
||||
const updated = state[author];
|
||||
const templateUrl =
|
||||
`https://github.com/${repo.owner}/${repo.repo}` +
|
||||
`/issues/new?template=recommend-resource.yml`;
|
||||
const contributingUrl =
|
||||
`https://github.com/${repo.owner}/${repo.repo}` +
|
||||
`/blob/main/docs/CONTRIBUTING.md`;
|
||||
|
||||
let cooldownNote = '';
|
||||
if (updated.banned) {
|
||||
cooldownNote =
|
||||
'\n\n⚠️ Due to repeated violations, this account has been ' +
|
||||
'permanently restricted from submitting recommendations.';
|
||||
} else {
|
||||
cooldownNote =
|
||||
`\n\nA cooldown of **${formatRemaining(updated.active_until)}** ` +
|
||||
`has been applied to this account.`;
|
||||
}
|
||||
|
||||
await closeWithComment(
|
||||
`## ⚠️ Resource submissions are not accepted via pull request\n\n` +
|
||||
`Resource recommendations **must** be submitted through the ` +
|
||||
`issue template, not as a pull request. The entire resource ` +
|
||||
`pipeline — validation, review, and merging — is managed by ` +
|
||||
`automation.\n\n` +
|
||||
`**To submit your resource correctly:**\n` +
|
||||
`1. 📖 Read [CONTRIBUTING.md](${contributingUrl})\n` +
|
||||
`2. 📝 [Submit using the official template](${templateUrl})\n\n` +
|
||||
`If this PR is **not** a resource submission (e.g., a bug fix ` +
|
||||
`or improvement), please comment below and we'll reopen it.` +
|
||||
cooldownNote +
|
||||
`\n\n---\n*This PR was automatically closed.*`
|
||||
);
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
labels: ['needs-template'],
|
||||
});
|
||||
|
||||
console.log(
|
||||
`VIOLATION (PR): ${author} — closed #${number}, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ISSUE PATH: cooldown and violation checks
|
||||
// ==========================================================
|
||||
const issueBody = process.env.ISSUE_BODY || '';
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
|
||||
// CHECK 1: Permanent ban
|
||||
if (userState && userState.banned === true) {
|
||||
await closeWithComment(
|
||||
`This account has been permanently restricted from ` +
|
||||
`submitting recommendations due to repeated violations. ` +
|
||||
`If you believe this is in error, please open a discussion ` +
|
||||
`or contact the maintainer.`
|
||||
);
|
||||
console.log(`BANNED: ${author} — rejected #${number}`);
|
||||
await saveAndExit('false', repoUrl, String(userState.cooldown_level || ''));
|
||||
return;
|
||||
}
|
||||
|
||||
// CHECK 2: Active cooldown
|
||||
if (userState) {
|
||||
const activeUntil = new Date(userState.active_until);
|
||||
|
||||
if (activeUntil > now) {
|
||||
const prevLevel = userState.cooldown_level;
|
||||
recordViolation('submitted-during-cooldown');
|
||||
|
||||
const updated = state[author];
|
||||
const waitTime = updated.banned
|
||||
? 'This restriction is now permanent.'
|
||||
: `Please wait at least **${formatRemaining(updated.active_until)}** before opening any more submissions.`;
|
||||
|
||||
await closeWithComment(
|
||||
`A cooldown period is currently in effect for your account. ` +
|
||||
`Submitting during an active cooldown extends the restriction.\n\n` +
|
||||
`${waitTime}\n\n` +
|
||||
`Please review the [CONTRIBUTING guidelines](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md) ` +
|
||||
`and [pinned issues](https://github.com/${repo.owner}/${repo.repo}/issues) ` +
|
||||
`before your next submission.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`COOLDOWN: ${author} — rejected #${number}, level ${prevLevel} → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${author}: cooldown expired. Checking for violations.`);
|
||||
}
|
||||
|
||||
// CHECK 3: Missing "resource-submission" label (not via form)
|
||||
if (!labels.includes('resource-submission')) {
|
||||
recordViolation('missing-resource-submission-label');
|
||||
|
||||
const updated = state[author];
|
||||
|
||||
await closeWithComment(
|
||||
`This submission was not made through the required web form. ` +
|
||||
`As noted in [CONTRIBUTING.md](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md), ` +
|
||||
`recommendations must be submitted using the ` +
|
||||
`[web form](https://github.com/${repo.owner}/${repo.repo}/issues/new?template=recommend-resource.yml).\n\n` +
|
||||
`A cooldown of **${formatRemaining(updated.active_until)}** has been applied. ` +
|
||||
`Please use the web form for your next submission.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`VIOLATION (no label): ${author} — rejected #${number}, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
// CHECK 4: Repo less than 1 week old
|
||||
const repoUrlPattern =
|
||||
/https?:\/\/github\.com\/([^\/\s]+)\/([^\/\s#?"]+)/g;
|
||||
const repoMatches = [...issueBody.matchAll(repoUrlPattern)];
|
||||
|
||||
if (repoMatches.length > 0) {
|
||||
const [, repoOwner, rawRepoName] = repoMatches[0];
|
||||
const repoName = rawRepoName.replace(/\.git$/, '');
|
||||
|
||||
try {
|
||||
const repoData = await github.rest.repos.get({
|
||||
owner: repoOwner,
|
||||
repo: repoName
|
||||
});
|
||||
|
||||
const created = new Date(repoData.data.created_at);
|
||||
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays < 7) {
|
||||
recordViolation('repo-too-young');
|
||||
|
||||
const updated = state[author];
|
||||
const readyDate = new Date(created);
|
||||
readyDate.setDate(readyDate.getDate() + 7);
|
||||
const readyStr = readyDate.toLocaleDateString('en-US', {
|
||||
month: 'long', day: 'numeric', year: 'numeric'
|
||||
});
|
||||
|
||||
await closeWithComment(
|
||||
`Thanks for the recommendation! This repository is less than a week old. ` +
|
||||
`We ask that projects have some time in the wild before being recommended — ` +
|
||||
`you're welcome to re-submit after **${readyStr}**.\n\n` +
|
||||
`A cooldown of **${formatRemaining(updated.active_until)}** has been applied.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`VIOLATION (repo age): ${author} — rejected #${number}, ` +
|
||||
`${repoOwner}/${repoName} is ${ageDays.toFixed(1)}d old, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Skipping repo age check for ${repoOwner}/${repoName}: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No GitHub URL in issue body. Skipping repo age check.');
|
||||
}
|
||||
|
||||
console.log(`CLEAN: ${author} — issue #${number} allowed through.`);
|
||||
await saveAndExit('true');
|
||||
|
||||
dispatch-intake:
|
||||
needs: enforce-cooldown
|
||||
if: |
|
||||
needs.enforce-cooldown.result == 'success' &&
|
||||
needs.enforce-cooldown.outputs.repo_url != '' &&
|
||||
needs.enforce-cooldown.outputs.cooldown_level == '1'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dispatch intake
|
||||
env:
|
||||
DISPATCH_URL: ${{ secrets.SC_DISPATCH_URL }}
|
||||
DISPATCH_TOKEN: ${{ secrets.SC_DISPATCH_TOKEN }}
|
||||
REPO_URL: ${{ needs.enforce-cooldown.outputs.repo_url }}
|
||||
SOURCE_URL: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
payload="$(jq -nc \
|
||||
--arg event_type "event_registered" \
|
||||
--arg repo_url "${REPO_URL}" \
|
||||
--arg source_url "${SOURCE_URL}" \
|
||||
'{event_type:$event_type, client_payload:{repo_url:$repo_url, source_url:$source_url}}')"
|
||||
|
||||
curl -fsS -X POST "${DISPATCH_URL}" \
|
||||
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
-d "${payload}" >/dev/null
|
||||
|
||||
validate:
|
||||
needs: enforce-cooldown
|
||||
if: |
|
||||
always() &&
|
||||
github.event_name == 'issues' &&
|
||||
(
|
||||
github.event.action == 'edited' ||
|
||||
needs.enforce-cooldown.outputs.allowed == 'true'
|
||||
)
|
||||
uses: ./.github/workflows/validate-new-issue.yml
|
||||
597
.agent/knowledge/awesome_claude/.github/workflows/submission-enforcement.yml
vendored
Normal file
597
.agent/knowledge/awesome_claude/.github/workflows/submission-enforcement.yml
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
name: Submission Enforcement
|
||||
# Unified workflow: cooldown enforcement for issues, Claude-powered PR
|
||||
# classification, and validation dispatch for clean issue submissions.
|
||||
#
|
||||
# Triggers:
|
||||
# issues opened/reopened → cooldown check → if clean → validate
|
||||
# issues edited → skip cooldown → validate directly
|
||||
# PR opened/reopened → classify with Claude → if resource submission → cooldown violation
|
||||
#
|
||||
# Cooldown state stored in a private ops repo as cooldown-state.json.
|
||||
# Requires ACC_OPS secret (fine-grained PAT) with:
|
||||
# - awesome-claude-code-ops: Contents read/write
|
||||
# - awesome-claude-code: Issues + Pull requests read/write
|
||||
# because we use a single token for BOTH repos in the enforcement step.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
concurrency:
|
||||
group: >-
|
||||
cooldown-${{
|
||||
github.event.pull_request.user.login ||
|
||||
github.event.issue.user.login ||
|
||||
'unknown'
|
||||
}}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
OPS_OWNER: hesreallyhim
|
||||
OPS_REPO: awesome-claude-code-ops
|
||||
OPS_PATH: cooldown-state.json
|
||||
|
||||
jobs:
|
||||
enforce-cooldown:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'edited'
|
||||
outputs:
|
||||
allowed: ${{ steps.enforce.outputs.allowed }}
|
||||
repo_url: ${{ steps.enforce.outputs.repo_url }}
|
||||
cooldown_level: ${{ steps.enforce.outputs.cooldown_level }}
|
||||
|
||||
permissions:
|
||||
# These are for GITHUB_TOKEN only; our step uses ACC_OPS PAT explicitly.
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: identify-repo
|
||||
id: identify-repo
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isPR = context.eventName === 'pull_request_target';
|
||||
const author = isPR
|
||||
? context.payload.pull_request.user.login
|
||||
: context.payload.issue.user.login;
|
||||
const body = isPR
|
||||
? (context.payload.pull_request.body || '')
|
||||
: (context.payload.issue.body || '');
|
||||
|
||||
function extractUrls(text) {
|
||||
const pattern = /https?:\/\/github\.com\/([^\/\s]+)\/([^\/\s#?")\]]+)/gi;
|
||||
const results = [];
|
||||
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const owner = match[1];
|
||||
const repo = match[2]
|
||||
.replace(/\.git$/i, '')
|
||||
.replace(/[.,;:!?]+$/, '');
|
||||
if (!owner || !repo) continue;
|
||||
results.push({
|
||||
owner,
|
||||
repo,
|
||||
url: `https://github.com/${owner}/${repo}`,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function firstAuthorMatch(urls, authorLogin) {
|
||||
const authorLower = (authorLogin || '').toLowerCase();
|
||||
const match = urls.find(u => u.owner.toLowerCase() === authorLower);
|
||||
return match ? match.url : '';
|
||||
}
|
||||
|
||||
let repoUrl = '';
|
||||
const urls = extractUrls(body);
|
||||
|
||||
if (!isPR) {
|
||||
const linkLine = body.match(/^\s*\*\*Link:\*\*\s*(.+)\s*$/im);
|
||||
if (linkLine) {
|
||||
const templateUrls = extractUrls(linkLine[1]);
|
||||
repoUrl = firstAuthorMatch(templateUrls, author);
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoUrl) {
|
||||
repoUrl = firstAuthorMatch(urls, author);
|
||||
}
|
||||
|
||||
core.setOutput('repo_url', repoUrl);
|
||||
console.log(repoUrl ? `Repo URL identified: ${repoUrl}` : 'No matching repo URL identified.');
|
||||
|
||||
- name: Get PR changed files
|
||||
id: files
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const files = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 50,
|
||||
});
|
||||
return files.data.map(f => f.filename).join('\n');
|
||||
result-encoding: string
|
||||
|
||||
- name: Classify PR with Claude
|
||||
id: classify
|
||||
if: github.event_name == 'pull_request_target'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_FILES: ${{ steps.files.outputs.result }}
|
||||
run: |
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg body "${PR_BODY:0:2000}" \
|
||||
--arg files "$PR_FILES" \
|
||||
'{
|
||||
model: "claude-haiku-4-5-20251001",
|
||||
max_tokens: 50,
|
||||
system: "You classify GitHub pull requests for the awesome-claude-code repository (a curated awesome-list of tools, skills, hooks, and resources for Claude Code by Anthropic).\n\nA \"resource submission\" is any PR that attempts to add, recommend, or promote a tool, project, library, skill, MCP server, hook, workflow, guide, or similar resource to the list. This includes PRs that edit README.md or a resources CSV to insert a new entry.\n\nA \"not resource submission\" is a PR that fixes bugs, improves CI/workflows, corrects typos, updates documentation about the repo itself (not adding a new external resource), refactors code, or makes other repository maintenance changes.\n\nRespond with ONLY a JSON object, no markdown fences: {\"classification\": \"resource_submission\" | \"not_resource_submission\", \"confidence\": \"high\" | \"low\"}",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: ("PR Title: " + $title + "\n\nPR Body:\n" + $body + "\n\nChanged files:\n" + $files)
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: "{"
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -sf https://api.anthropic.com/v1/messages \
|
||||
-H "content-type: application/json" \
|
||||
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d "$PAYLOAD") || {
|
||||
echo "API call failed"
|
||||
echo "classification=error" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=none" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
}
|
||||
|
||||
RAW=$(echo "$RESPONSE" | jq -r '.content[0].text')
|
||||
TEXT="{${RAW}"
|
||||
TEXT=$(echo "$TEXT" | sed 's/^```json//;s/^```//;s/```$//' | tr -d '\n')
|
||||
echo "Claude response: $TEXT"
|
||||
|
||||
CLASSIFICATION=$(echo "$TEXT" | jq -r '.classification // "error"')
|
||||
CONFIDENCE=$(echo "$TEXT" | jq -r '.confidence // "low"')
|
||||
|
||||
echo "Classification: $CLASSIFICATION"
|
||||
echo "Confidence: $CONFIDENCE"
|
||||
echo "classification=$CLASSIFICATION" >> "$GITHUB_OUTPUT"
|
||||
echo "confidence=$CONFIDENCE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enforce cooldown rules
|
||||
id: enforce
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
OPS_OWNER: ${{ env.OPS_OWNER }}
|
||||
OPS_REPO: ${{ env.OPS_REPO }}
|
||||
OPS_PATH: ${{ env.OPS_PATH }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body || '' }}
|
||||
REPO_URL: ${{ steps.identify-repo.outputs.repo_url || '' }}
|
||||
PR_CLASSIFICATION: ${{ steps.classify.outputs.classification || '' }}
|
||||
PR_CONFIDENCE: ${{ steps.classify.outputs.confidence || '' }}
|
||||
with:
|
||||
# Single-token approach: this step uses the PAT for BOTH repos.
|
||||
github-token: ${{ secrets.ACC_OPS }}
|
||||
script: |
|
||||
const opsOwner = process.env.OPS_OWNER;
|
||||
const opsRepo = process.env.OPS_REPO;
|
||||
const opsPath = process.env.OPS_PATH;
|
||||
|
||||
const isPR = context.eventName === 'pull_request_target';
|
||||
const repo = context.repo;
|
||||
const now = new Date();
|
||||
const repoUrl = process.env.REPO_URL || '';
|
||||
|
||||
const author = isPR
|
||||
? context.payload.pull_request.user.login
|
||||
: context.payload.issue.user.login;
|
||||
const number = isPR
|
||||
? context.payload.pull_request.number
|
||||
: context.payload.issue.number;
|
||||
core.setOutput('repo_url', '');
|
||||
core.setOutput('cooldown_level', '');
|
||||
|
||||
// ---- PR: skip bots ----
|
||||
if (isPR && context.payload.pull_request.user.type === 'Bot') {
|
||||
console.log(`Skipping bot PR by ${author}`);
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- PR: classification gate ----
|
||||
if (isPR) {
|
||||
const classification = process.env.PR_CLASSIFICATION;
|
||||
const confidence = process.env.PR_CONFIDENCE;
|
||||
|
||||
if (classification === 'error') {
|
||||
console.log('Classification failed — fail open.');
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
if (classification !== 'resource_submission') {
|
||||
if (confidence === 'low') {
|
||||
await github.rest.issues.addLabels({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
labels: ['needs-review'],
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
`PR #${number} classified as ${classification} (${confidence}) — no enforcement needed.`
|
||||
);
|
||||
core.setOutput('allowed', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`PR #${number} classified as resource_submission — enforcing.`);
|
||||
}
|
||||
|
||||
// ---- Issue: excused label bypass ----
|
||||
if (!isPR) {
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
if (labels.includes('excused')) {
|
||||
console.log(`Issue #${number} has excused label — skipping.`);
|
||||
core.setOutput('allowed', 'true');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Load cooldown state from ops repo ----
|
||||
let state = {};
|
||||
let fileSha = null;
|
||||
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner: opsOwner,
|
||||
repo: opsRepo,
|
||||
path: opsPath
|
||||
});
|
||||
state = JSON.parse(Buffer.from(data.content, 'base64').toString());
|
||||
fileSha = data.sha;
|
||||
console.log(`Loaded state (sha: ${fileSha})`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('No state file found. Starting fresh.');
|
||||
} else {
|
||||
console.log(`Error loading state: ${e.message}. Starting fresh.`);
|
||||
}
|
||||
}
|
||||
|
||||
const userState = state[author] || null;
|
||||
let stateChanged = false;
|
||||
|
||||
function recordViolation(reason) {
|
||||
const level = userState ? userState.cooldown_level : 0;
|
||||
|
||||
if (level >= 6) {
|
||||
state[author] = {
|
||||
active_until: '9999-01-01T00:00:00Z',
|
||||
cooldown_level: 6,
|
||||
banned: true,
|
||||
last_violation: now.toISOString(),
|
||||
last_reason: reason
|
||||
};
|
||||
} else {
|
||||
const hours = 24 * Math.pow(2, level);
|
||||
const activeUntil = new Date(now.getTime() + hours * 60 * 60 * 1000);
|
||||
state[author] = {
|
||||
active_until: activeUntil.toISOString(),
|
||||
cooldown_level: level + 1,
|
||||
last_violation: now.toISOString(),
|
||||
last_reason: reason
|
||||
};
|
||||
}
|
||||
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
async function closeWithComment(comment) {
|
||||
await github.rest.issues.createComment({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
if (isPR) {
|
||||
await github.rest.pulls.update({
|
||||
...repo,
|
||||
pull_number: number,
|
||||
state: 'closed'
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatRemaining(activeUntilISO) {
|
||||
const remaining = new Date(activeUntilISO) - now;
|
||||
const days = Math.ceil(remaining / (1000 * 60 * 60 * 24));
|
||||
if (days <= 0) return 'less than a day';
|
||||
if (days === 1) return '1 day';
|
||||
return `${days} days`;
|
||||
}
|
||||
|
||||
async function saveAndExit(
|
||||
allowed,
|
||||
selectedRepoUrl = '',
|
||||
selectedCooldownLevel = ''
|
||||
) {
|
||||
core.setOutput('allowed', allowed);
|
||||
core.setOutput('repo_url', selectedRepoUrl || '');
|
||||
core.setOutput('cooldown_level', selectedCooldownLevel || '');
|
||||
|
||||
if (!stateChanged) return;
|
||||
|
||||
const content = Buffer.from(JSON.stringify(state, null, 2)).toString('base64');
|
||||
|
||||
const commitMsg =
|
||||
`cooldown: ${author} — ` +
|
||||
(state[author]?.last_reason || 'clean') +
|
||||
` (#${number})`;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
owner: opsOwner,
|
||||
repo: opsRepo,
|
||||
path: opsPath,
|
||||
message: commitMsg,
|
||||
content
|
||||
};
|
||||
if (fileSha) params.sha = fileSha;
|
||||
|
||||
await github.rest.repos.createOrUpdateFileContents(params);
|
||||
console.log(`State saved: ${commitMsg}`);
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
console.log(
|
||||
`Conflict writing state (409). Violation for ${author} will be caught on next submission.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Error saving state: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// PR PATH: resource submission via PR is always a violation
|
||||
// ==========================================================
|
||||
if (isPR) {
|
||||
if (userState && userState.banned === true) {
|
||||
recordViolation('submitted-as-pr');
|
||||
} else if (userState && new Date(userState.active_until) > now) {
|
||||
recordViolation('submitted-as-pr-during-cooldown');
|
||||
} else {
|
||||
recordViolation('submitted-as-pr');
|
||||
}
|
||||
|
||||
const updated = state[author];
|
||||
const templateUrl =
|
||||
`https://github.com/${repo.owner}/${repo.repo}` +
|
||||
`/issues/new?template=recommend-resource.yml`;
|
||||
const contributingUrl =
|
||||
`https://github.com/${repo.owner}/${repo.repo}` +
|
||||
`/blob/main/docs/CONTRIBUTING.md`;
|
||||
|
||||
let cooldownNote = '';
|
||||
if (updated.banned) {
|
||||
cooldownNote =
|
||||
'\n\n⚠️ Due to repeated violations, this account has been ' +
|
||||
'permanently restricted from submitting recommendations.';
|
||||
} else {
|
||||
cooldownNote =
|
||||
`\n\nA cooldown of **${formatRemaining(updated.active_until)}** ` +
|
||||
`has been applied to this account.`;
|
||||
}
|
||||
|
||||
await closeWithComment(
|
||||
`## ⚠️ Resource submissions are not accepted via pull request\n\n` +
|
||||
`Resource recommendations **must** be submitted through the ` +
|
||||
`issue template, not as a pull request. The entire resource ` +
|
||||
`pipeline — validation, review, and merging — is managed by ` +
|
||||
`automation.\n\n` +
|
||||
`**To submit your resource correctly:**\n` +
|
||||
`1. 📖 Read [CONTRIBUTING.md](${contributingUrl})\n` +
|
||||
`2. 📝 [Submit using the official template](${templateUrl})\n\n` +
|
||||
`If this PR is **not** a resource submission (e.g., a bug fix ` +
|
||||
`or improvement), please comment below and we'll reopen it.` +
|
||||
cooldownNote +
|
||||
`\n\n---\n*This PR was automatically closed.*`
|
||||
);
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
...repo,
|
||||
issue_number: number,
|
||||
labels: ['needs-template'],
|
||||
});
|
||||
|
||||
console.log(
|
||||
`VIOLATION (PR): ${author} — closed #${number}, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ISSUE PATH: cooldown and violation checks
|
||||
// ==========================================================
|
||||
const issueBody = process.env.ISSUE_BODY || '';
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
|
||||
// CHECK 1: Permanent ban
|
||||
if (userState && userState.banned === true) {
|
||||
await closeWithComment(
|
||||
`This account has been permanently restricted from ` +
|
||||
`submitting recommendations due to repeated violations. ` +
|
||||
`If you believe this is in error, please open a discussion ` +
|
||||
`or contact the maintainer.`
|
||||
);
|
||||
console.log(`BANNED: ${author} — rejected #${number}`);
|
||||
await saveAndExit('false', repoUrl, String(userState.cooldown_level || ''));
|
||||
return;
|
||||
}
|
||||
|
||||
// CHECK 2: Active cooldown
|
||||
if (userState) {
|
||||
const activeUntil = new Date(userState.active_until);
|
||||
|
||||
if (activeUntil > now) {
|
||||
const prevLevel = userState.cooldown_level;
|
||||
recordViolation('submitted-during-cooldown');
|
||||
|
||||
const updated = state[author];
|
||||
const waitTime = updated.banned
|
||||
? 'This restriction is now permanent.'
|
||||
: `Please wait at least **${formatRemaining(updated.active_until)}** before opening any more submissions.`;
|
||||
|
||||
await closeWithComment(
|
||||
`A cooldown period is currently in effect for your account. ` +
|
||||
`Submitting during an active cooldown extends the restriction.\n\n` +
|
||||
`${waitTime}\n\n` +
|
||||
`Please review the [CONTRIBUTING guidelines](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md) ` +
|
||||
`and [pinned issues](https://github.com/${repo.owner}/${repo.repo}/issues) ` +
|
||||
`before your next submission.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`COOLDOWN: ${author} — rejected #${number}, level ${prevLevel} → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${author}: cooldown expired. Checking for violations.`);
|
||||
}
|
||||
|
||||
// CHECK 3: Missing "resource-submission" label (not via form)
|
||||
if (!labels.includes('resource-submission')) {
|
||||
recordViolation('missing-resource-submission-label');
|
||||
|
||||
const updated = state[author];
|
||||
|
||||
await closeWithComment(
|
||||
`This submission was not made through the required web form. ` +
|
||||
`As noted in [CONTRIBUTING.md](https://github.com/hesreallyhim/awesome-claude-code/blob/main/docs/CONTRIBUTING.md), ` +
|
||||
`recommendations must be submitted using the ` +
|
||||
`[web form](https://github.com/${repo.owner}/${repo.repo}/issues/new?template=recommend-resource.yml).\n\n` +
|
||||
`A cooldown of **${formatRemaining(updated.active_until)}** has been applied. ` +
|
||||
`Please use the web form for your next submission.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`VIOLATION (no label): ${author} — rejected #${number}, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
|
||||
// CHECK 4: Repo less than 1 week old
|
||||
const repoUrlPattern =
|
||||
/https?:\/\/github\.com\/([^\/\s]+)\/([^\/\s#?"]+)/g;
|
||||
const repoMatches = [...issueBody.matchAll(repoUrlPattern)];
|
||||
|
||||
if (repoMatches.length > 0) {
|
||||
const [, repoOwner, rawRepoName] = repoMatches[0];
|
||||
const repoName = rawRepoName.replace(/\.git$/, '');
|
||||
|
||||
try {
|
||||
const repoData = await github.rest.repos.get({
|
||||
owner: repoOwner,
|
||||
repo: repoName
|
||||
});
|
||||
|
||||
const created = new Date(repoData.data.created_at);
|
||||
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays < 7) {
|
||||
recordViolation('repo-too-young');
|
||||
|
||||
const updated = state[author];
|
||||
const readyDate = new Date(created);
|
||||
readyDate.setDate(readyDate.getDate() + 7);
|
||||
const readyStr = readyDate.toLocaleDateString('en-US', {
|
||||
month: 'long', day: 'numeric', year: 'numeric'
|
||||
});
|
||||
|
||||
await closeWithComment(
|
||||
`Thanks for the recommendation! This repository is less than a week old. ` +
|
||||
`We ask that projects have some time in the wild before being recommended — ` +
|
||||
`you're welcome to re-submit after **${readyStr}**.\n\n` +
|
||||
`A cooldown of **${formatRemaining(updated.active_until)}** has been applied.`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`VIOLATION (repo age): ${author} — rejected #${number}, ` +
|
||||
`${repoOwner}/${repoName} is ${ageDays.toFixed(1)}d old, level → ${updated.cooldown_level}`
|
||||
);
|
||||
await saveAndExit('false', repoUrl, String(updated.cooldown_level));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Skipping repo age check for ${repoOwner}/${repoName}: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No GitHub URL in issue body. Skipping repo age check.');
|
||||
}
|
||||
|
||||
console.log(`CLEAN: ${author} — issue #${number} allowed through.`);
|
||||
await saveAndExit('true');
|
||||
|
||||
dispatch-intake:
|
||||
needs: enforce-cooldown
|
||||
if: |
|
||||
needs.enforce-cooldown.result == 'success' &&
|
||||
needs.enforce-cooldown.outputs.repo_url != '' &&
|
||||
needs.enforce-cooldown.outputs.cooldown_level == '1'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Dispatch intake
|
||||
env:
|
||||
DISPATCH_URL: ${{ secrets.SC_DISPATCH_URL }}
|
||||
DISPATCH_TOKEN: ${{ secrets.SC_DISPATCH_TOKEN }}
|
||||
REPO_URL: ${{ needs.enforce-cooldown.outputs.repo_url }}
|
||||
SOURCE_URL: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
payload="$(jq -nc \
|
||||
--arg event_type "event_registered" \
|
||||
--arg repo_url "${REPO_URL}" \
|
||||
--arg source_url "${SOURCE_URL}" \
|
||||
'{event_type:$event_type, client_payload:{repo_url:$repo_url, source_url:$source_url}}')"
|
||||
|
||||
curl -fsS -X POST "${DISPATCH_URL}" \
|
||||
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
-d "${payload}" >/dev/null
|
||||
|
||||
validate:
|
||||
needs: enforce-cooldown
|
||||
if: |
|
||||
always() &&
|
||||
github.event_name == 'issues' &&
|
||||
(
|
||||
github.event.action == 'edited' ||
|
||||
needs.enforce-cooldown.outputs.allowed == 'true'
|
||||
)
|
||||
uses: ./.github/workflows/validate-new-issue.yml
|
||||
43
.agent/knowledge/awesome_claude/.github/workflows/update-github-release-data.yml
vendored
Normal file
43
.agent/knowledge/awesome_claude/.github/workflows/update-github-release-data.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Update GitHub Release Data
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 3:00 AM UTC
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-github-release-data:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
|
||||
- name: Update GitHub release data
|
||||
run: |
|
||||
python -m scripts.maintenance.update_github_release_data
|
||||
|
||||
- name: Commit and push if changed
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add THE_RESOURCES_TABLE.csv
|
||||
git diff --quiet && git diff --staged --quiet || (git commit -m "chore: update GitHub release data [skip ci]" && git push)
|
||||
58
.agent/knowledge/awesome_claude/.github/workflows/update-repo-ticker.yml
vendored
Normal file
58
.agent/knowledge/awesome_claude/.github/workflows/update-repo-ticker.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Update Repo Ticker Data
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch: # Allow manual trigger for testing
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
jobs:
|
||||
update-ticker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Backup previous day's data
|
||||
run: |
|
||||
if [ -f data/repo-ticker.csv ]; then
|
||||
cp data/repo-ticker.csv data/repo-ticker-previous.csv
|
||||
echo "✓ Backed up previous data"
|
||||
else
|
||||
echo "⚠ No previous data to backup (first run)"
|
||||
fi
|
||||
|
||||
- name: Fetch GitHub repo data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
python -m scripts.ticker.fetch_repo_ticker_data
|
||||
|
||||
- name: Generate ticker SVGs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
python -m scripts.ticker.generate_ticker_svg
|
||||
|
||||
- name: Commit and push if changed
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add data/repo-ticker.csv data/repo-ticker-previous.csv assets/repo-ticker.svg assets/repo-ticker-light.svg assets/repo-ticker-awesome.svg
|
||||
git diff --quiet && git diff --staged --quiet || (git commit -m "chore: update repo ticker data and SVGs [skip ci]" && git push)
|
||||
150
.agent/knowledge/awesome_claude/.github/workflows/validate-links.yml
vendored
Normal file
150
.agent/knowledge/awesome_claude/.github/workflows/validate-links.yml
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
name: Validate Links
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 2:00 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate-links:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Track Github API Usage
|
||||
uses: hesreallyhim/github-api-usage-monitor@v1
|
||||
with:
|
||||
diagnostics: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: make install
|
||||
|
||||
- name: Run link validation
|
||||
id: validate
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
run: |
|
||||
make validate-github
|
||||
has_broken_links=$(python -c "import json; data=json.load(open('validation_results.json')); print('true' if data['newly_broken'] else 'false')")
|
||||
echo "has_broken_links=${has_broken_links}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload validation results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: validation-results
|
||||
path: |
|
||||
validation_results.json
|
||||
THE_RESOURCES_TABLE.csv
|
||||
|
||||
- name: Check for existing issue
|
||||
if: steps.validate.outputs.has_broken_links == 'true'
|
||||
id: check_issue
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: 'broken-links'
|
||||
});
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const existingIssue = issues.data.find(issue =>
|
||||
issue.title.includes('Broken Links Report') &&
|
||||
issue.title.includes(today)
|
||||
);
|
||||
|
||||
core.setOutput('issue_number', existingIssue ? existingIssue.number : '');
|
||||
|
||||
- name: Create or update issue
|
||||
if: steps.validate.outputs.has_broken_links == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const results = JSON.parse(fs.readFileSync('validation_results.json', 'utf8'));
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
let issueBody = `## 🔗 Broken Links Report\n\n`;
|
||||
issueBody += `This automated scan found **${results.newly_broken_links.length}** new broken link(s) in the repository.\n\n`;
|
||||
issueBody += `### Broken Links:\n\n`;
|
||||
|
||||
for (const link of results.newly_broken_links) {
|
||||
issueBody += `- **${link.name}**\n`;
|
||||
issueBody += ` - URL: ${link.url}\n`;
|
||||
}
|
||||
|
||||
issueBody += `### Summary\n\n`;
|
||||
issueBody += `- Broken links: ${results.newly_broken_links.length}\n`;
|
||||
issueBody += `- Scan completed: ${results.timestamp}\n\n`;
|
||||
issueBody += `---\n`;
|
||||
issueBody += `*This issue was automatically created by the [link validation workflow](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/workflows/validate-links.yml).*`;
|
||||
|
||||
const existingIssueNumber = Number("${{ steps.check_issue.outputs.issue_number }}") || 0;
|
||||
|
||||
if (existingIssueNumber) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: existingIssueNumber,
|
||||
body: issueBody
|
||||
});
|
||||
console.log(`Updated existing issue #${existingIssueNumber}`);
|
||||
} else {
|
||||
const issue = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `🚨 Broken Links Report - ${today}`,
|
||||
body: issueBody,
|
||||
labels: ['broken-links', 'automated']
|
||||
});
|
||||
console.log(`Created new issue #${issue.data.number}`);
|
||||
}
|
||||
|
||||
- name: Close old broken link issues
|
||||
if: steps.validate.outputs.has_broken_links == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: 'broken-links'
|
||||
});
|
||||
|
||||
for (const issue of issues.data) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
state_reason: 'completed'
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '✅ All links are now working! Closing this issue.'
|
||||
});
|
||||
|
||||
console.log(`Closed issue #${issue.number}`);
|
||||
}
|
||||
263
.agent/knowledge/awesome_claude/.github/workflows/validate-new-issue.yml
vendored
Normal file
263
.agent/knowledge/awesome_claude/.github/workflows/validate-new-issue.yml
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
name: Validate New Issue
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
# Called by submission-enforcement.yml after cooldown clears,
|
||||
# or directly on issue edits. The enforcement workflow handles
|
||||
# missing-label and informal submission detection, so this
|
||||
# workflow only validates properly-submitted resources.
|
||||
|
||||
jobs:
|
||||
validate-resource:
|
||||
name: Validate Resource Submission
|
||||
# Only run on issues with the resource-submission label
|
||||
if: contains(github.event.issue.labels.*.name, 'resource-submission')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
scripts/
|
||||
templates/
|
||||
THE_RESOURCES_TABLE.csv
|
||||
pyproject.toml
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install PyGithub PyYAML requests python-dotenv
|
||||
|
||||
- name: Parse and validate submission
|
||||
id: validate
|
||||
env:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
run: |
|
||||
python -m scripts.resources.parse_issue_form --validate 2>&1 | tail -n 1 > validation_result.json
|
||||
|
||||
if grep -q '"valid": true' validation_result.json; then
|
||||
echo "Validation passed!"
|
||||
else
|
||||
echo "Validation failed!"
|
||||
fi
|
||||
|
||||
echo "=== Validation Result ==="
|
||||
python -m json.tool validation_result.json || cat validation_result.json
|
||||
|
||||
- name: Remove old validation comments
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue_number = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
});
|
||||
|
||||
for (const comment of comments.data) {
|
||||
if (comment.user.type === 'Bot' && comment.body.includes('## 🤖 Validation Results')) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- name: Post validation results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const validation_result = JSON.parse(fs.readFileSync('validation_result.json', 'utf8'));
|
||||
|
||||
let comment_body = '## 🤖 Validation Results\n\n';
|
||||
|
||||
if (validation_result.valid) {
|
||||
comment_body += '✅ **All validation checks passed!**\n\n';
|
||||
comment_body += 'Your submission is ready for review by a maintainer.\n\n';
|
||||
comment_body += '### Validated Data:\n';
|
||||
comment_body += '```json\n';
|
||||
comment_body += JSON.stringify(validation_result.data, null, 2);
|
||||
comment_body += '\n```\n';
|
||||
} else {
|
||||
comment_body += '❌ **Validation failed**\n\n';
|
||||
comment_body += 'Please fix the following issues and edit your submission:\n\n';
|
||||
|
||||
for (const error of validation_result.errors) {
|
||||
comment_body += `- ❗ ${error}\n`;
|
||||
}
|
||||
|
||||
if (validation_result.warnings && validation_result.warnings.length > 0) {
|
||||
comment_body += '\n### Warnings:\n';
|
||||
for (const warning of validation_result.warnings) {
|
||||
comment_body += `- ⚠️ ${warning}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
comment_body += '\n**Note:** You can edit your issue to fix these problems, and validation will run again automatically.';
|
||||
}
|
||||
|
||||
comment_body += '\n\n---\n';
|
||||
comment_body += '<sub>This comment is automatically updated when you edit the issue.</sub>';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment_body
|
||||
});
|
||||
|
||||
- name: Update issue labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const issue_number = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const validation_result = JSON.parse(fs.readFileSync('validation_result.json', 'utf8'));
|
||||
const validation_passed = validation_result.valid;
|
||||
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
});
|
||||
|
||||
let labels = issue.labels.map(label => label.name);
|
||||
|
||||
labels = labels.filter(label =>
|
||||
label !== 'validation-passed' &&
|
||||
label !== 'validation-failed' &&
|
||||
label !== 'pending-validation'
|
||||
);
|
||||
|
||||
if (validation_passed && labels.includes('changes-requested')) {
|
||||
labels = labels.filter(label => label !== 'changes-requested');
|
||||
}
|
||||
|
||||
if (validation_passed) {
|
||||
labels.push('validation-passed');
|
||||
} else {
|
||||
labels.push('validation-failed');
|
||||
}
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels,
|
||||
});
|
||||
|
||||
- name: Notify maintainer if changes were made
|
||||
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'changes-requested')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const validation_result = JSON.parse(fs.readFileSync('validation_result.json', 'utf8'));
|
||||
const issue_number = context.issue.number;
|
||||
const current_validation_status = validation_result.valid;
|
||||
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
let maintainer = null;
|
||||
let changesRequestedTime = null;
|
||||
for (let i = comments.data.length - 1; i >= 0; i--) {
|
||||
const comment = comments.data[i];
|
||||
const match = comment.body.match(/## 🔄 Changes Requested by @(\w+)/);
|
||||
if (match) {
|
||||
maintainer = match[1];
|
||||
changesRequestedTime = new Date(comment.created_at);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!maintainer) return;
|
||||
|
||||
let lastNotificationTime = null;
|
||||
let lastNotifiedStatus = null;
|
||||
let hasNotifiedAfterRequest = false;
|
||||
|
||||
for (const comment of comments.data) {
|
||||
if (comment.body.includes('## 📝 Issue Updated') && comment.user.type === 'Bot') {
|
||||
const commentTime = new Date(comment.created_at);
|
||||
if (commentTime > changesRequestedTime) {
|
||||
hasNotifiedAfterRequest = true;
|
||||
|
||||
const metaMatch = comment.body.match(/<!-- notification-meta: status=(\w+) -->/);
|
||||
if (metaMatch) {
|
||||
lastNotifiedStatus = metaMatch[1] === 'true';
|
||||
}
|
||||
|
||||
if (!lastNotificationTime || commentTime > lastNotificationTime) {
|
||||
lastNotificationTime = commentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shouldNotify = false;
|
||||
let notificationReason = '';
|
||||
|
||||
if (!hasNotifiedAfterRequest) {
|
||||
shouldNotify = true;
|
||||
notificationReason = 'first edit after changes requested';
|
||||
} else if (lastNotifiedStatus !== null && lastNotifiedStatus !== current_validation_status) {
|
||||
shouldNotify = true;
|
||||
notificationReason = 'validation status changed';
|
||||
}
|
||||
|
||||
if (shouldNotify) {
|
||||
let notification_body = `## 📝 Issue Updated\n\n`;
|
||||
notification_body += `@${maintainer} - The submitter has edited their issue in response to your requested changes.\n\n`;
|
||||
|
||||
if (current_validation_status) {
|
||||
notification_body += `✅ **The updated submission now passes all validation checks!**\n\n`;
|
||||
notification_body += `You may want to review the changes and consider approving the submission.`;
|
||||
} else {
|
||||
notification_body += `❌ **The submission still has validation errors.**\n\n`;
|
||||
notification_body += `The submitter may need additional guidance to fix the remaining issues.`;
|
||||
}
|
||||
|
||||
notification_body += `\n\n<!-- notification-meta: status=${current_validation_status} -->`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: notification_body
|
||||
});
|
||||
|
||||
console.log(`Notification sent (reason: ${notificationReason})`);
|
||||
} else {
|
||||
console.log('Skipping notification - no significant changes detected');
|
||||
}
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
rm -f validation_result.json
|
||||
Reference in New Issue
Block a user