wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation

This commit is contained in:
2026-03-29 22:08:40 +09:00
parent aca7bf592a
commit 2507de45d3
4289 changed files with 732689 additions and 28672 deletions

View 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

View 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

View 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

View 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.

View 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

View 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' }}

View 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'],
});

View 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'],
});

View 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

View 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');
}

View 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

View 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

View 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)

View 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)

View 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}`);
}

View 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