Bump dompurify from 3.4.11 to 3.4.12 in /socratiq #327
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); |