This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5622-0731313a73fa36c47cef9d7cfa4c87abc8dfe69e in repository https://gitbox.apache.org/repos/asf/texera.git
commit 6723f074bc50f8e43f29e1e46bb7c665a0e032be Author: Matthew B. <[email protected]> AuthorDate: Fri Jun 12 01:40:15 2026 -0700 ci: warn when a PR or issue does not follow the template (#5622) ### What changes were proposed in this PR? - Adds a non-blocking GitHub Actions workflow (`.github/workflows/template-compliance-warning.yml`) that comments when a PR or issue is opened/edited without following the template, and deletes the comment automatically once the description is fixed. - For PRs it strips the template's `<!-- -->` guidance and flags any required section that is missing or blank; for issues (GitHub form templates that already enforce required fields) it only flags a fully blank body. - Keeps the warning wording in `.github/template-compliance-warning.txt` so editing the message does not touch workflow logic. - Kept cheap on CI: a single `github-script` job with no build and only a sparse-checkout of the message file, triggered on `opened`/`edited` (never `synchronize`), skipping drafts and bots, and posting one self-resolving sticky comment instead of duplicates. ### Any related issues, documentation, discussions? Closes: #5621 ### How was this PR tested? - Validated the workflow YAML parses: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/template-compliance-warning.yml'))"`. - Exercised the detection logic in Node against the real `.github/PULL_REQUEST_TEMPLATE`: an unfilled template flags all three required sections empty, a properly filled body returns no problems, an empty body and a template with headings deleted are both flagged, and an issue with content passes. - The workflow itself runs only on real `pull_request_target`/`issues` events, so end-to-end behavior (comment posted then auto-removed) is verifiable once merged; it cannot run from the PR branch beforehand. tested here: https://github.com/Ma77Ball/texera/issues/60 <img width="1404" height="980" alt="image" src="https://github.com/user-attachments/assets/1301fc83-8b28-481c-ae96-e137359d28af" /> ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.8 in compliance with ASF --- .github/template-compliance-warning.txt | 9 + .github/workflows/contributor-checks.yml | 320 +++++++++++++++++++++ .../workflows/welcome-first-time-contributor.yml | 138 --------- 3 files changed, 329 insertions(+), 138 deletions(-) diff --git a/.github/template-compliance-warning.txt b/.github/template-compliance-warning.txt new file mode 100644 index 0000000000..b0f9272b63 --- /dev/null +++ b/.github/template-compliance-warning.txt @@ -0,0 +1,9 @@ +👋 Thanks for opening this {{kind}}, @{{author}}! + +It looks like the {{kind}} description doesn't quite follow our template yet: + +{{details}} + +Filling out the template helps reviewers understand and triage your contribution faster. Please edit the description to complete it. This message will disappear automatically once the template is followed. + +You can find the template prompts by editing the description, or see [CONTRIBUTING.md](https://github.com/{{owner}}/{{repo}}/blob/main/CONTRIBUTING.md) for the full contribution flow. diff --git a/.github/workflows/contributor-checks.yml b/.github/workflows/contributor-checks.yml new file mode 100644 index 0000000000..a281ee7976 --- /dev/null +++ b/.github/workflows/contributor-checks.yml @@ -0,0 +1,320 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Two contributor-facing checks that share one runner. They fire on the +# same events and both only post a comment, so running them as two steps +# of a single job avoids spinning up a second runner. +# +# 1. Welcome first-time contributors when they open an issue or pull +# request, pointing them at the comment-driven commands defined in +# `comment-commands.yml` (/take, /request-review, /sub-issue, etc.). +# Detection uses the search API rather than `author_association`, +# which is FIRST_TIME_CONTRIBUTOR only on the first commit/PR and so +# misses someone opening their first issue. Searching +# `repo:<repo> is:issue author:<login>` with `total_count <= 1` covers +# both issues and PRs, tolerating the brief indexing delay where the +# just-opened item may not be in results yet. Runs on `opened` only. +# +# 2. Warn when an issue or PR does not follow our template, clearing the +# warning automatically once it is fixed. Issues are matched to their +# template by GitHub issue type (Bug/Feature/Task); a typeless issue +# is flagged as not using a template. PRs are checked against the PR +# template's required sections. Runs on opens and edits (and issue +# type changes) so the warning can resolve itself, and skips draft PRs. +# +# Uses `pull_request_target` so PRs from forks are handled; a +# `pull_request` run from a fork gets a read-only token and could not +# comment. +name: Contributor welcome and template check +on: + issues: + types: [opened, edited, typed, untyped] + pull_request_target: + types: [opened, edited] + +permissions: + issues: write + pull-requests: write + +jobs: + contributor-checks: + if: github.event.sender.type != 'Bot' + runs-on: ubuntu-latest + steps: + # pull_request_target and issues both resolve to the trusted base + # branch (never the fork head), so checking out the message + # templates below is safe. They live in .txt files so editing the + # wording does not touch workflow logic. + - uses: actions/checkout@v5 + with: + persist-credentials: false + sparse-checkout: | + .github/welcome-first-time-contributor.txt + .github/template-compliance-warning.txt + sparse-checkout-cone-mode: false + # Step 1: welcome first-time contributors. Only on `opened`; the + # other triggers exist for the template check (step 2). + - name: Welcome first-time contributors + if: github.event.action == 'opened' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const isPR = context.eventName === 'pull_request_target'; + const subject = isPR + ? context.payload.pull_request + : context.payload.issue; + const author = subject.user.login; + const issue_number = subject.number; + const { owner, repo } = context.repo; + + // Hidden marker for idempotency: if a previous run already + // welcomed this issue/PR, the marker will be in an existing + // comment and we skip. Lets us survive workflow re-runs, + // reopen races, and future manual triggers. + const MARKER = '<!-- texera:welcome-first-time-contributor -->'; + try { + const existing = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number, per_page: 100 }, + ); + if (existing.some((c) => (c.body || '').includes(MARKER))) { + core.info(`Already welcomed on #${issue_number}; skipping.`); + return; + } + } catch (e) { + core.warning( + `listComments on #${issue_number} failed: ${e.message}`, + ); + // Fall through — better to risk a duplicate welcome than + // skip a genuine first-timer over a transient API error. + } + + // Count prior items of the same kind by this author. The + // just-opened item may or may not be indexed yet, so we + // treat <=1 as "first time" (covers both 0 — not yet + // indexed — and 1 — only the new item). + const q = `repo:${owner}/${repo} is:${isPR ? 'pr' : 'issue'} author:${author}`; + let total = 0; + try { + const { data } = await github.rest.search.issuesAndPullRequests({ + q, per_page: 1, + }); + total = data.total_count; + } catch (e) { + core.warning( + `Search for prior items by ${author} failed: ${e.message}`, + ); + return; + } + core.info( + `Author ${author} has ${total} ${isPR ? 'PR' : 'issue'}(s) ` + + `in ${owner}/${repo} (including this one if indexed).`, + ); + if (total > 1) { + core.info(`${author} is not a first-time contributor; skipping.`); + return; + } + + // Message body lives in .github/welcome-first-time-contributor.txt + // so wording edits skip CI. Substitute the runtime placeholders + // and prepend the idempotency marker. + const fs = require('fs'); + const template = fs.readFileSync( + '.github/welcome-first-time-contributor.txt', 'utf8', + ); + const body = MARKER + '\n' + template + .replaceAll('{{author}}', author) + .replaceAll('{{owner}}', owner) + .replaceAll('{{repo}}', repo); + + try { + await github.rest.issues.createComment({ + owner, repo, issue_number, body, + }); + core.info(`Posted welcome comment on #${issue_number}`); + } catch (e) { + core.warning( + `Failed to post welcome on #${issue_number}: ${e.message}`, + ); + } + + # Step 2: warn when the template is not followed, and clear the + # warning once it is. Runs on every trigger (open, edit, and issue + # type changes) so it can resolve itself. + - name: Warn when the template is not followed + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const isPR = context.eventName === 'pull_request_target'; + const subject = isPR + ? context.payload.pull_request + : context.payload.issue; + + // Drafts are work-in-progress; don't nag until they're ready. + if (isPR && subject.draft) { + core.info(`#${subject.number} is a draft; skipping.`); + return; + } + + const author = subject.user.login; + const issue_number = subject.number; + const kind = isPR ? 'pull request' : 'issue'; + const { owner, repo } = context.repo; + const body = subject.body || ''; + + // Strip HTML comments (the template's <!-- ... --> guidance) + // before judging whether a section actually has content. + const stripped = body.replace(/<!--[\s\S]*?-->/g, ''); + + // Pick the required sections for whichever template applies. + // PRs use the single PR template. Issues are matched by their + // GitHub issue type (set by the form template the author + // chose), so each issue is checked against the right + // template's fields. Only fields marked `required: true` in + // the templates are listed here. + const PR_SECTIONS = [ + 'What changes were proposed in this PR?', + 'How was this PR tested?', + 'Was this PR authored or co-authored using generative AI tooling?', + ]; + const ISSUE_SECTIONS = { + Bug: ['What happened?', 'How to reproduce?', 'Version/Branch'], + Feature: ['Feature Summary', 'Proposed Solution or Design'], + Task: ['Task Summary'], + }; + let requiredSections = null; + if (isPR) { + requiredSections = PR_SECTIONS; + } else { + const typeName = subject.type && subject.type.name; + requiredSections = ISSUE_SECTIONS[typeName] || null; + } + + // Build the list of problems with the description. Each entry + // is a user-facing bullet. An empty list means "compliant". + const problems = []; + + if (!isPR && !requiredSections) { + // All our issue templates set an issue type, so a missing or + // unrecognized type means no template was used (e.g. a blank + // issue). Flag it outright. + problems.push( + `This ${kind} doesn't appear to use one of our templates ` + + `(Bug, Feature, or Task). Please open it using a template ` + + `so the required details are captured.`, + ); + } else if (stripped.trim().length === 0) { + problems.push( + `The description is empty. Please open the ${kind} using ` + + `the provided template and fill it out.`, + ); + } else { + // PR, or an issue with a recognized type: check each required + // section. Capture the text from its heading to the next + // heading (or end of body), treating a blank value or + // GitHub's "_No response_" placeholder (shown for an empty + // field) as not filled in. + for (const heading of requiredSections) { + // Escape regex metacharacters in the heading text. + const esc = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // The trailing `(?![\s\S])` is the end-of-string case (JS + // regex has no `\Z`); with the `m` flag a bare `$` would + // match every line end, not just the end of the body. + const re = new RegExp( + `^#{1,6}\\s*${esc}\\s*$([\\s\\S]*?)(?=^#{1,6}\\s|(?![\\s\\S]))`, + 'm', + ); + const m = stripped.match(re); + if (!m) { + problems.push( + `The **${heading}** section is missing; please keep ` + + `the template's headings.`, + ); + } else { + const content = m[1].trim(); + if (content.length === 0 || content === '_No response_') { + problems.push( + `The **${heading}** section is empty; please fill it in.`, + ); + } + } + } + } + + const MARKER = '<!-- texera:template-compliance -->'; + + // Find a previous warning comment from this workflow. + let existing = null; + try { + const comments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number, per_page: 100 }, + ); + existing = comments.find((c) => (c.body || '').includes(MARKER)); + } catch (e) { + core.warning(`listComments on #${issue_number} failed: ${e.message}`); + // Without the comment list we can't safely de-dupe; bail to + // avoid posting a duplicate warning. + return; + } + + // Compliant now: remove any stale warning and stop. + if (problems.length === 0) { + core.info(`#${issue_number} follows the template.`); + if (existing) { + try { + await github.rest.issues.deleteComment({ + owner, repo, comment_id: existing.id, + }); + core.info(`Cleared resolved warning on #${issue_number}.`); + } catch (e) { + core.warning(`Failed to delete warning: ${e.message}`); + } + } + return; + } + + // Not compliant: render the message and post/update the sticky + // comment. + const fs = require('fs'); + const template = fs.readFileSync( + '.github/template-compliance-warning.txt', 'utf8', + ); + const details = problems.map((p) => `- ${p}`).join('\n'); + const message = MARKER + '\n' + template + .replaceAll('{{author}}', author) + .replaceAll('{{owner}}', owner) + .replaceAll('{{repo}}', repo) + .replaceAll('{{kind}}', kind) + .replaceAll('{{details}}', details); + + try { + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body: message, + }); + core.info(`Updated template warning on #${issue_number}.`); + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number, body: message, + }); + core.info(`Posted template warning on #${issue_number}.`); + } + } catch (e) { + core.warning(`Failed to post warning on #${issue_number}: ${e.message}`); + } diff --git a/.github/workflows/welcome-first-time-contributor.yml b/.github/workflows/welcome-first-time-contributor.yml deleted file mode 100644 index 5e85ff30b3..0000000000 --- a/.github/workflows/welcome-first-time-contributor.yml +++ /dev/null @@ -1,138 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Welcome first-time contributors when they open an issue or pull -# request, pointing them at the comment-driven commands defined in -# `comment-commands.yml` (/take, /request-review, /sub-issue, etc.). -# -# Detection uses the search API rather than `author_association`: -# `author_association` is FIRST_TIME_CONTRIBUTOR only on the first -# *commit/PR*, so it misses someone opening their first issue (they -# show up as NONE alongside any non-member who has commented before). -# Searching `repo:<repo> is:issue author:<login>` with `total_count -# <= 1` cleanly covers both issues and PRs, tolerating the brief -# indexing delay where the just-opened item may not be in results yet. -# -# Uses `pull_request_target` so PRs from forks still get a welcome -# comment — `pull_request` from forks runs with a read-only token. -name: Welcome first-time contributor -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - -permissions: - issues: write - pull-requests: write - -jobs: - welcome: - if: github.event.sender.type != 'Bot' - runs-on: ubuntu-latest - steps: - # Check out the base ref (pull_request_target / issues both resolve to - # the trusted base branch, never the fork head) so we can read the - # welcome message template below. The template lives in its own .txt - # file so editing the wording does not trigger a full CI run; see the - # `ci` label exclusion in .github/labeler.yml. - - uses: actions/checkout@v5 - with: - persist-credentials: false - sparse-checkout: .github/welcome-first-time-contributor.txt - sparse-checkout-cone-mode: false - - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const isPR = context.eventName === 'pull_request_target'; - const subject = isPR - ? context.payload.pull_request - : context.payload.issue; - const author = subject.user.login; - const issue_number = subject.number; - const { owner, repo } = context.repo; - - // Hidden marker for idempotency: if a previous run already - // welcomed this issue/PR, the marker will be in an existing - // comment and we skip. Lets us survive workflow re-runs, - // reopen races, and future manual triggers. - const MARKER = '<!-- texera:welcome-first-time-contributor -->'; - try { - const existing = await github.paginate( - github.rest.issues.listComments, - { owner, repo, issue_number, per_page: 100 }, - ); - if (existing.some((c) => (c.body || '').includes(MARKER))) { - core.info(`Already welcomed on #${issue_number}; skipping.`); - return; - } - } catch (e) { - core.warning( - `listComments on #${issue_number} failed: ${e.message}`, - ); - // Fall through — better to risk a duplicate welcome than - // skip a genuine first-timer over a transient API error. - } - - // Count prior items of the same kind by this author. The - // just-opened item may or may not be indexed yet, so we - // treat <=1 as "first time" (covers both 0 — not yet - // indexed — and 1 — only the new item). - const q = `repo:${owner}/${repo} is:${isPR ? 'pr' : 'issue'} author:${author}`; - let total = 0; - try { - const { data } = await github.rest.search.issuesAndPullRequests({ - q, per_page: 1, - }); - total = data.total_count; - } catch (e) { - core.warning( - `Search for prior items by ${author} failed: ${e.message}`, - ); - return; - } - core.info( - `Author ${author} has ${total} ${isPR ? 'PR' : 'issue'}(s) ` + - `in ${owner}/${repo} (including this one if indexed).`, - ); - if (total > 1) { - core.info(`${author} is not a first-time contributor; skipping.`); - return; - } - - // Message body lives in .github/welcome-first-time-contributor.txt - // so wording edits skip CI. Substitute the runtime placeholders - // and prepend the idempotency marker. - const fs = require('fs'); - const template = fs.readFileSync( - '.github/welcome-first-time-contributor.txt', 'utf8', - ); - const body = MARKER + '\n' + template - .replaceAll('{{author}}', author) - .replaceAll('{{owner}}', owner) - .replaceAll('{{repo}}', repo); - - try { - await github.rest.issues.createComment({ - owner, repo, issue_number, body, - }); - core.info(`Posted welcome comment on #${issue_number}`); - } catch (e) { - core.warning( - `Failed to post welcome on #${issue_number}: ${e.message}`, - ); - }
