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}`,
-              );
-            }

Reply via email to