fix: restrict teacher course deletion to owned courses #52
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: PR Guard | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review] | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| guard-small-pr: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate PR size and rules | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const core = require("@actions/core"); | |
| const { context, github } = require("@actions/github"); | |
| const pr = context.payload.pull_request; | |
| if (!pr) { | |
| core.info("No pull request found in payload. Skipping."); | |
| return; | |
| } | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const excludedLogins = new Set(["udaycodespace"]); | |
| const trustedLogins = new Set(["udaycodespace"]); | |
| const bypassLabels = new Set(["approved-small-pr"]); | |
| const authorLogin = String(pr.user?.login || "").toLowerCase().trim(); | |
| const authorType = String(pr.user?.type || ""); | |
| const prNumber = pr.number; | |
| const additions = Number(pr.additions || 0); | |
| const deletions = Number(pr.deletions || 0); | |
| const changedFiles = Number(pr.changed_files || 0); | |
| const changedLines = additions + deletions; | |
| const labels = (pr.labels || []).map(label => String(label.name || "").toLowerCase().trim()); | |
| const state = String(pr.state || "").toLowerCase(); | |
| const isDraft = Boolean(pr.draft); | |
| const isMerged = Boolean(pr.merged); | |
| const isBot = | |
| authorType === "Bot" || | |
| authorLogin.endsWith("[bot]") || | |
| authorLogin === "github-actions"; | |
| if (!authorLogin) { | |
| core.info("Missing author login. Skipping."); | |
| return; | |
| } | |
| if (state !== "open") { | |
| core.info(`PR #${prNumber} is not open. Skipping.`); | |
| return; | |
| } | |
| if (isMerged) { | |
| core.info(`PR #${prNumber} is already merged. Skipping.`); | |
| return; | |
| } | |
| if (isBot) { | |
| core.info(`PR #${prNumber} is from a bot (${authorLogin}). Skipping.`); | |
| return; | |
| } | |
| if (excludedLogins.has(authorLogin) || trustedLogins.has(authorLogin)) { | |
| core.info(`PR #${prNumber} is from trusted/excluded login ${authorLogin}. Skipping.`); | |
| return; | |
| } | |
| if (isDraft) { | |
| core.info(`PR #${prNumber} is a draft. Skipping.`); | |
| return; | |
| } | |
| if (labels.some(label => bypassLabels.has(label))) { | |
| core.info(`PR #${prNumber} has bypass label. Skipping.`); | |
| return; | |
| } | |
| const minimumChangedLines = 20; | |
| if (changedLines >= minimumChangedLines) { | |
| core.info( | |
| `PR #${prNumber} passed size rule: additions=${additions}, deletions=${deletions}, total=${changedLines}, files=${changedFiles}.` | |
| ); | |
| return; | |
| } | |
| const marker = "<!-- learnhub-pr-guard -->"; | |
| const closeLabel = "auto-closed-small-pr"; | |
| const existingComments = await github.paginate(github.rest.issues.listComments, { | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| per_page: 100 | |
| }); | |
| const alreadyCommented = existingComments.some(comment => | |
| String(comment.body || "").includes(marker) | |
| ); | |
| const intro = | |
| `${marker}\n` + | |
| `Hi @${pr.user.login}, thanks for raising this PR.\n\n`; | |
| const explanation = | |
| `The change looks valid, but this PR was closed because it does not currently follow our contribution rules.\n\n` + | |
| `Please make sure:\n` + | |
| `- the PR follows \`CONTRIBUTING.md\`\n` + | |
| `- the contribution is meaningful and properly scoped\n` + | |
| `- the PR has at least **20 changed lines** in total, unless it is pre-approved\n` + | |
| `- if this is a valid smaller change, ask for the \`approved-small-pr\` label first\n\n`; | |
| const stats = | |
| `Current PR stats:\n` + | |
| `- Additions: ${additions}\n` + | |
| `- Deletions: ${deletions}\n` + | |
| `- Total changed lines: ${changedLines}\n` + | |
| `- Changed files: ${changedFiles}\n\n`; | |
| const nextSteps = | |
| `You can update the branch and open a new PR after following the guidelines.\n`; | |
| const body = intro + explanation + stats + nextSteps; | |
| if (!alreadyCommented) { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body | |
| }); | |
| core.info(`Posted policy comment on PR #${prNumber}.`); | |
| } else { | |
| core.info(`Policy comment already exists on PR #${prNumber}.`); | |
| } | |
| if (!labels.includes(closeLabel)) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| labels: [closeLabel] | |
| }); | |
| core.info(`Added label ${closeLabel} to PR #${prNumber}.`); | |
| } catch (error) { | |
| core.warning(`Could not add label: ${error.message}`); | |
| } | |
| } | |
| await github.rest.pulls.update({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| state: "closed" | |
| }); | |
| core.info( | |
| `Closed PR #${prNumber} because changedLines=${changedLines} < ${minimumChangedLines}.` | |
| ); |