Skip to content

Bump dompurify from 3.4.11 to 3.4.12 in /socratiq #327

Bump dompurify from 3.4.11 to 3.4.12 in /socratiq

Bump dompurify from 3.4.11 to 3.4.12 in /socratiq #327

name: '🔧 Infra · 🤖 All Contributors Auto-credit on Merge'
# =============================================================================
# 🤖 Auto-credit Contributors on PR Merge
# =============================================================================
#
# When a PR is merged to dev, this workflow detects the contributor's project
# and contribution type, then applies the credit directly. This avoids relying
# on a bot-created issue comment to trigger a second workflow.
#
# Flow:
# 1. SKIP_CHECK — bots, maintainer allowlist, already-credited PRs
# 2. DETECT_PROJECT — top-level dir of changed files → projects.json key
# (deterministic — no LLM)
# 3. CLASSIFY_TYPE — LLM reads PR title + body + file list, returns types
# (LLM because title prefixes alone aren't reliable)
# 4. APPLY_CREDIT — update the project .all-contributorsrc and README
# tables, or ask for clarification if detection fails
#
# Triggers:
# - pull_request_target: closed (and merged == true)
#
# Why pull_request_target (not pull_request):
# For PRs from forks, the GITHUB_TOKEN on `pull_request` is read-only — the
# declared `pull-requests: write` in this workflow is silently ignored, and
# the comment POST returns 403. `pull_request_target` runs in the base-repo
# context with the full token, which is exactly what we need.
#
# This is safe here: we never check out fork code (sparse checkout pulls
# from dev only) and the LLM prompt only ingests title/body/file-paths as
# data — there is no shell eval of fork-controlled input.
#
# Related:
# - all-contributors-add.yml — manual comment entry point using the same
# contributor helper
# - update-contributors.yml — regenerates root config from per-project
# - .github/workflows/contributors/auto_credit.py — project detection
#
# =============================================================================
on:
pull_request_target:
types: [closed]
env:
TARGET_BRANCH: 'dev'
# GitHub logins to skip — maintainers don't auto-credit themselves.
# Comma-separated. Bots (login ending in '[bot]') are always skipped.
MAINTAINERS: 'profvjreddi'
# LLM model. Kept in sync with all-contributors-add.yml — see also that
# workflow's LLM_MODEL env. Both should run the same model.
LLM_MODEL: 'gemma3:12b'
jobs:
auto-credit:
name: Auto-credit
if: |
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'dev'
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
# =====================================================================
# STEP 0: Sparse-checkout the contributors helpers
# =====================================================================
- name: Checkout helpers (sparse)
uses: actions/checkout@v6
with:
ref: ${{ env.TARGET_BRANCH }}
sparse-checkout: |
.github/workflows/contributors/projects.json
.github/workflows/contributors/projects.py
.github/workflows/contributors/auto_credit.py
sparse-checkout-cone-mode: false
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
# =====================================================================
# STEP 1: Skip checks (maintainer, bot, already-credited)
# =====================================================================
- name: Skip checks
id: skip
uses: actions/github-script@v9
env:
MAINTAINERS: ${{ env.MAINTAINERS }}
with:
script: |
const author = context.payload.pull_request.user.login;
const userType = context.payload.pull_request.user.type || '';
const maintainers = (process.env.MAINTAINERS || '')
.split(',').map(s => s.trim()).filter(Boolean);
if (userType === 'Bot' || author.endsWith('[bot]')) {
console.log(`Skipping bot: ${author}`);
core.setOutput('skip', 'true');
core.setOutput('reason', 'bot');
return;
}
if (maintainers.includes(author)) {
console.log(`Skipping maintainer: ${author}`);
core.setOutput('skip', 'true');
core.setOutput('reason', 'maintainer');
return;
}
// Already credited? Look for any prior @all-contributors comment
// mentioning this author. The add workflow is idempotent (set-union)
// but we don't want to spam duplicate trigger comments.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100
});
const triggerRe = new RegExp(
'@all-contributors\\s+(please add\\s+)?@' + author + '\\b',
'i'
);
const alreadyCredited = comments.some(c => triggerRe.test(c.body || ''));
if (alreadyCredited) {
console.log(`Already credited: @${author}`);
core.setOutput('skip', 'true');
core.setOutput('reason', 'already_credited');
return;
}
core.setOutput('skip', 'false');
core.setOutput('author', author);
# =====================================================================
# STEP 2: Detect project(s) from PR file paths (deterministic)
# =====================================================================
- name: Detect project(s)
if: steps.skip.outputs.skip != 'true'
id: project
uses: actions/github-script@v9
with:
script: |
const { execFileSync } = require('child_process');
const fs = require('fs');
const prNum = context.payload.pull_request.number;
// Fetch all changed files (paginate)
let files = [];
let page = 1;
while (true) {
const { data } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNum,
per_page: 100,
page
});
files = files.concat(data.map(f => f.filename));
if (data.length < 100) break;
page += 1;
}
// Cap files passed to the LLM step (large PRs would blow the prompt).
// Project detection uses ALL files; the LLM gets a representative sample.
const inputPath = '/tmp/auto_credit_input.json';
fs.writeFileSync(inputPath, JSON.stringify({ files }));
const stdout = execFileSync('python3', [
'.github/workflows/contributors/auto_credit.py',
inputPath
], { encoding: 'utf-8' });
const detected = JSON.parse(stdout);
console.log('Project detection:', JSON.stringify(detected));
// Sample up to 30 files for the LLM prompt — enough to convey the
// file mix without bloating the context. Prefer files in detected
// project dirs so the LLM sees the substantive ones first.
const sampledFiles = files.slice(0, 30);
core.setOutput('projects', detected.projects.join(', '));
core.setOutput('confidence', detected.confidence);
core.setOutput('files_sample', sampledFiles.join('\n'));
# =====================================================================
# STEP 3: LLM classifies contribution type(s) from PR context
# =====================================================================
# The LLM looks at the PR title, body, and a sample of changed file
# paths. Title prefixes alone (fix:/feat:/docs:) aren't reliable — a
# "fix:" PR can be code, doc, or design depending on what was touched.
- name: Classify contribution types with LLM
if: |
steps.skip.outputs.skip != 'true' &&
steps.project.outputs.confidence == 'high'
uses: ai-action/ollama-action@v2
id: llm
with:
model: ${{ env.LLM_MODEL }}
prompt: |
Classify the contribution type(s) for this merged pull request.
PR TITLE: ${{ github.event.pull_request.title }}
PR DESCRIPTION:
${{ github.event.pull_request.body }}
CHANGED FILES (sample):
${{ steps.project.outputs.files_sample }}
CONTRIBUTION TYPES (pick one or more):
- bug: Found, reported, or root-caused a bug
- code: Wrote code, implemented features or fixes (.py/.ts/.js/.sh/etc.)
- doc: Wrote or improved documentation, lab content, prose (.md/.qmd/.rst)
- design: UI/UX/visual design, CSS/SCSS styling, color/layout (.css/.scss/.svg)
- ideas: Suggested ideas, proposed features
- review: Reviewed code or PRs
- test: Wrote or improved tests, QA verification (test files / tests/ dirs)
- tool: Built tools, scripts, automation, CLI utilities
RULES:
- Return MULTIPLE types when both apply. A bug fix that changes
code is ["bug", "code"]. A bug fix that changes only .qmd/.md
prose is ["bug", "doc"]. A CSS-only fix is ["bug", "design"].
- Use the file paths as the primary evidence for what was changed.
The PR title prefix is a hint, not a rule.
- If the PR adds a new test file, include "test" in addition to
whatever the test covers ("code, test" or "doc, test").
EXAMPLES:
Title: "fix(kits): typo in raspi lab"
Files: kits/contents/raspi/foo.qmd
Output: {"types": ["bug", "doc"]}
Title: "feat(tinytorch): add tensor reshape op"
Files: tinytorch/src/01_tensor/reshape.py, tinytorch/tests/test_reshape.py
Output: {"types": ["code", "test"]}
Title: "fix(scss): scope dark-mode overrides"
Files: tinytorch/quarto/assets/styles/style.scss
Output: {"types": ["bug", "design"]}
Title: "docs: update install instructions"
Files: README.md, kits/README.md
Output: {"types": ["doc"]}
Return ONLY a JSON object: {"types": ["<type>", ...]}.
No explanation, no other text.
# =====================================================================
# STEP 4: Parse LLM output and decide whether credit can be applied
# =====================================================================
- name: Parse contribution credit
id: parse
if: steps.skip.outputs.skip != 'true'
uses: actions/github-script@v9
env:
AUTHOR: ${{ steps.skip.outputs.author }}
PROJECTS: ${{ steps.project.outputs.projects }}
PROJECT_CONFIDENCE: ${{ steps.project.outputs.confidence }}
LLM_RESPONSE: ${{ steps.llm.outputs.response || '' }}
with:
script: |
const author = process.env.AUTHOR;
const projects = process.env.PROJECTS;
const projectConf = process.env.PROJECT_CONFIDENCE;
const llmResponse = process.env.LLM_RESPONSE || '';
const VALID_TYPES = ['bug','code','doc','design','ideas','review','test','tool'];
// --- Parse LLM types ---
let types = [];
try {
const m = llmResponse.match(/\{[\s\S]*?\}/);
if (m) {
const parsed = JSON.parse(m[0]);
if (Array.isArray(parsed.types)) {
types = parsed.types
.map(t => String(t).toLowerCase().trim())
.filter(t => VALID_TYPES.includes(t));
}
}
} catch (e) {
console.log('LLM response parse failed:', e.message);
}
const typesStr = types.join(', ');
console.log(`LLM classified types: [${typesStr}]`);
const haveProject = projectConf === 'high' && projects;
const haveTypes = types.length > 0;
// --- Both detected: continue to the apply steps ---
if (haveProject && haveTypes) {
const projectList = projects.split(',').map(s => s.trim()).filter(Boolean);
core.setOutput('success', 'true');
core.setOutput('username', author);
core.setOutput('types', JSON.stringify(types));
core.setOutput('types_csv', typesStr);
core.setOutput('projects', JSON.stringify(projectList));
core.setOutput('projects_csv', projectList.join(', '));
return;
}
// --- Otherwise: ask the maintainer to specify the missing piece ---
core.setOutput('success', 'false');
const projectPart = haveProject ? projects : '<project>';
const typePart = haveTypes ? typesStr : '<type>';
const missing = [];
if (!haveProject) missing.push('project');
if (!haveTypes) missing.push('contribution type');
const body = [
`Thanks @${author} for the contribution! 🎉`,
``,
`I couldn't auto-detect the ${missing.join(' and ')} for this PR. To credit @${author}, reply with:`,
``,
'```',
`@all-contributors please add @${author} for ${typePart} in ${projectPart}`,
'```',
``,
`**Types:** bug, code, doc, design, ideas, review, test, tool _(multi: \`bug, code\`)_`,
`**Projects:** book, tinytorch, kits, labs, mlsysim, staffml, slides, instructors, periodictable _(multi: \`kits, book\`)_`,
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
console.log(`Posted clarification (missing: ${missing.join(', ')})`);
# =====================================================================
# STEP 5: Checkout full repo, apply credit, and push generated files
# =====================================================================
- name: Disable lingering sparse-checkout state
if: steps.parse.outputs.success == 'true'
shell: bash
run: |
git sparse-checkout disable 2>/dev/null || true
rm -f .git/info/sparse-checkout
- name: Checkout repository (full)
if: steps.parse.outputs.success == 'true'
uses: actions/checkout@v6
with:
ref: ${{ env.TARGET_BRANCH }}
fetch-depth: 0
- name: Apply contributor credit
id: apply
if: steps.parse.outputs.success == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 .github/workflows/contributors/add_contributor.py \
--username '${{ steps.parse.outputs.username }}' \
--types '${{ steps.parse.outputs.types }}' \
--projects '${{ steps.parse.outputs.projects }}'
- name: Configure Git
if: steps.parse.outputs.success == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Commit and push changes
id: commit
if: steps.parse.outputs.success == 'true'
run: |
set -euo pipefail
UPDATED_DIRS='${{ steps.apply.outputs.updated_dirs }}'
USERNAME="${{ steps.parse.outputs.username }}"
TYPES="${{ steps.parse.outputs.types_csv }}"
PROJECTS_LIST="${{ steps.parse.outputs.projects_csv }}"
for dir in $(echo "$UPDATED_DIRS" | python3 -c "import sys,json; print(' '.join(json.load(sys.stdin)))"); do
git add "${dir}/.all-contributorsrc" "${dir}/README.md" 2>/dev/null || true
done
git add README.md 2>/dev/null || true
if git diff --staged --quiet; then
echo "No contributor changes to commit"
echo "committed=false" >> "$GITHUB_OUTPUT"
else
git commit -m "docs: add @${USERNAME} as contributor for ${TYPES} (${PROJECTS_LIST})"
git pull --rebase origin ${{ env.TARGET_BRANCH }}
git push origin ${{ env.TARGET_BRANCH }}
echo "committed=true" >> "$GITHUB_OUTPUT"
fi
# =====================================================================
# STEP 6: Post confirmation without using a workflow-trigger command
# =====================================================================
- name: Post auto-credit confirmation
if: steps.parse.outputs.success == 'true'
uses: actions/github-script@v9
with:
script: |
const username = '${{ steps.parse.outputs.username }}';
const types = '${{ steps.parse.outputs.types_csv }}';
const projects = '${{ steps.parse.outputs.projects_csv }}';
const committed = '${{ steps.commit.outputs.committed }}' === 'true';
const body = [
`Thanks @${username}! 🎉`,
``,
committed
? `I added @${username} to **${projects}** for: ${types}.`
: `@${username} was already credited in **${projects}** for: ${types}.`,
``,
`The contributor tables are now handled directly by this workflow; no follow-up command is needed.`
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});