Skip to content

[Resource]: eslint-plugin-mcp-security #41

[Resource]: eslint-plugin-mcp-security

[Resource]: eslint-plugin-mcp-security #41

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