Mask password values in object selector previews #175
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: Pull request standards | |
| on: | |
| pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout | |
| types: | |
| - opened | |
| - edited | |
| - reopened | |
| - ready_for_review | |
| branches: | |
| - dev | |
| permissions: {} | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| check: | |
| name: Check pull request follows contribution standards | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write # To label and comment on pull requests | |
| steps: | |
| - name: Check pull request standards | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| // Exempt bots (Copilot agent, dependabot), drafts, and maintainers. | |
| if (pr.user.type === "Bot") { | |
| core.info(`Skipping bot author: ${pr.user.login}`); | |
| return; | |
| } | |
| if (pr.draft) { | |
| core.info("Skipping draft pull request"); | |
| return; | |
| } | |
| try { | |
| await github.rest.orgs.checkMembershipForUser({ | |
| org: "home-assistant", | |
| username: pr.user.login, | |
| }); | |
| core.info(`Skipping organization member: ${pr.user.login}`); | |
| return; | |
| } catch (error) { | |
| core.info(`${pr.user.login} is not an organization member, checking standards`); | |
| } | |
| const label = "Needs Template"; | |
| const marker = "<!-- pr-standards-check -->"; | |
| const { owner, repo } = context.repo; | |
| const issue_number = pr.number; | |
| const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, ""); | |
| const normalized = body.toLowerCase(); | |
| // Ignore 404s from mutations that race manual edits or cancelled runs. | |
| const ignoreMissing = async (fn) => { | |
| try { | |
| await fn(); | |
| } catch (error) { | |
| if (error.status === 404) { | |
| core.info("Target already removed, nothing to do"); | |
| return; | |
| } | |
| throw error; | |
| } | |
| }; | |
| // Hide/restore our comment via GraphQL (REST cannot minimize). | |
| const setMinimized = async (subjectId, minimized) => { | |
| const mutation = minimized | |
| ? `mutation($id: ID!) { | |
| minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) { | |
| clientMutationId | |
| } | |
| }` | |
| : `mutation($id: ID!) { | |
| unminimizeComment(input: { subjectId: $id }) { | |
| clientMutationId | |
| } | |
| }`; | |
| try { | |
| await github.graphql(mutation, { id: subjectId }); | |
| } catch (error) { | |
| core.info( | |
| `Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}` | |
| ); | |
| } | |
| }; | |
| // Content of a "## <name>" section, or null when the heading is absent. | |
| const section = (name) => { | |
| const match = body.match( | |
| new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i") | |
| ); | |
| return match ? match[1] : null; | |
| }; | |
| const problems = []; | |
| const requiredHeadings = [ | |
| "## proposed change", | |
| "## type of change", | |
| "## checklist", | |
| ]; | |
| if (requiredHeadings.some((h) => !normalized.includes(h))) { | |
| problems.push( | |
| "Use the pull request template without removing its sections." | |
| ); | |
| } | |
| const typeOfChange = section("type of change"); | |
| if (typeOfChange !== null) { | |
| const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length; | |
| if (ticked !== 1) { | |
| problems.push( | |
| 'Select exactly one option under "Type of change".' | |
| ); | |
| } | |
| } | |
| const proposedChange = section("proposed change"); | |
| if (proposedChange !== null && proposedChange.trim().length === 0) { | |
| problems.push('Describe your changes under "Proposed change".'); | |
| } | |
| const isValid = problems.length === 0; | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number, per_page: 100 } | |
| ); | |
| const existing = comments.find((c) => c.body.includes(marker)); | |
| const hasLabel = pr.labels.some((l) => l.name === label); | |
| if (isValid) { | |
| core.info("Pull request standards met"); | |
| if (hasLabel) { | |
| await ignoreMissing(() => | |
| github.rest.issues.removeLabel({ | |
| owner, repo, issue_number, name: label, | |
| }) | |
| ); | |
| } | |
| if (existing) { | |
| await setMinimized(existing.node_id, true); | |
| } | |
| return; | |
| } | |
| core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`); | |
| if (!hasLabel) { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number, labels: [label], | |
| }); | |
| } | |
| const message = | |
| `${marker}\n` + | |
| `Hey @${pr.user.login}!\n\n` + | |
| `Thank you for your contribution! To help reviewers, please update ` + | |
| `this pull request to follow our pull request standards:\n\n` + | |
| problems.map((p) => `- ${p}`).join("\n") + | |
| `\n\n` + | |
| `Please complete the ` + | |
| `[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` + | |
| `and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` + | |
| `for more on creating a great pull request (see point 6).`; | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, comment_id: existing.id, body: message, | |
| }); | |
| await setMinimized(existing.node_id, false); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number, body: message, | |
| }); | |
| } | |
| // Fail this check so it can block the PR from being merged | |
| core.setFailed( | |
| `Pull request standards not met:\n- ${problems.join("\n- ")}` | |
| ); |