You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asyncband.apache.org by xu...@apache.org on 2026/09/01 04:42:42 UTC

(asyncband) branch xuanwo/discussion-mailing-list-links created (now 496f150)

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a change to branch xuanwo/discussion-mailing-list-links
in repository https://gitbox.apache.org/repos/asf/asyncband.git


      at 496f150  ci: link discussions to mailing list threads

This branch includes the following new commits:

     new 496f150  ci: link discussions to mailing list threads

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@asyncband.apache.org
For additional commands, e-mail: commits-help@asyncband.apache.org


(asyncband) 01/01: ci: link discussions to mailing list threads

Posted by xu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/discussion-mailing-list-links
in repository https://gitbox.apache.org/repos/asf/asyncband.git

commit 496f1505ba971c77d1f335572fe82da8480beae2
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Tue Sep 1 12:42:24 2026 +0800

    ci: link discussions to mailing list threads
---
 .asf.yaml                                    |   9 ++
 .github/workflows/discussion-thread-link.yml | 232 +++++++++++++++++++++++++++
 2 files changed, 241 insertions(+)

diff --git a/.asf.yaml b/.asf.yaml
index f8f8ddf..6308283 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -37,6 +37,15 @@ github:
     issues: true
     projects: false
     discussions: true
+  custom_subjects:
+    new_discussion: "{title}"
+    edit_discussion: "Re: {title}"
+    close_discussion: "Re: {title}"
+    close_discussion_with_comment: "Re: {title}"
+    reopen_discussion: "Re: {title}"
+    new_comment_discussion: "Re: {title}"
+    edit_comment_discussion: "Re: {title}"
+    delete_comment_discussion: "Re: {title}"
   pull_requests:
     allow_auto_merge: true
     allow_update_branch: true
diff --git a/.github/workflows/discussion-thread-link.yml b/.github/workflows/discussion-thread-link.yml
new file mode 100644
index 0000000..b3fa452
--- /dev/null
+++ b/.github/workflows/discussion-thread-link.yml
@@ -0,0 +1,232 @@
+# 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.
+
+name: Append mailing-list thread link to new Discussion
+
+on:
+  discussion:
+    types: [created]
+  workflow_dispatch:
+    inputs:
+      discussion_url:
+        description: 'Discussion URL to backfill mailing list link'
+        required: true
+
+permissions:
+  discussions: write
+  contents: read
+
+jobs:
+  link-thread:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    steps:
+      - name: Append thread URL
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+        with:
+          script: |
+            const { execFile } = require('node:child_process');
+            const { promisify } = require('node:util');
+
+            const execFileAsync = promisify(execFile);
+
+            let owner = context.repo.owner;
+            let repo = context.repo.repo;
+
+            const manualUrl = context.payload.inputs && context.payload.inputs.discussion_url
+              ? context.payload.inputs.discussion_url.trim()
+              : '';
+
+            let discussion = context.payload.discussion || null;
+            let number = discussion ? discussion.number : null;
+
+            let discussionRepoMismatch = false;
+
+            if (manualUrl) {
+              const match = manualUrl.match(/github\.com\/([^/]+)\/([^/]+)\/discussions\/(\d+)/i);
+              if (!match) {
+                core.setFailed(`Invalid discussion URL: ${manualUrl}`);
+                return;
+              }
+              owner = match[1];
+              repo = match[2];
+              number = Number(match[3]);
+              if (!number || Number.isNaN(number)) {
+                core.setFailed(`Invalid discussion number in URL: ${manualUrl}`);
+                return;
+              }
+              discussionRepoMismatch =
+                owner.toLowerCase() !== context.repo.owner.toLowerCase() ||
+                repo.toLowerCase() !== context.repo.repo.toLowerCase();
+            }
+
+            if (!discussion && !manualUrl) {
+              core.setFailed('Discussion payload missing and no discussion_url input provided');
+              return;
+            }
+
+            async function loadDiscussion() {
+              const query = `
+                query ($owner: String!, $repo: String!, $number: Int!) {
+                  repository(owner: $owner, name: $repo) {
+                    discussion(number: $number) {
+                      id
+                      number
+                      title
+                      body
+                      comments(first: 100) {
+                        nodes {
+                          body
+                        }
+                      }
+                    }
+                  }
+                }
+              `;
+              const result = await github.graphql(query, { owner, repo, number });
+              const current = result.repository && result.repository.discussion;
+              if (!current) {
+                throw new Error(`Discussion not found: ${owner}/${repo}#${number}`);
+              }
+              return current;
+            }
+
+            const currentDiscussion = await loadDiscussion();
+            const title = currentDiscussion.title || discussion?.title || '';
+
+            function normalizeSubject(value) {
+              return (value || '')
+                .trim()
+                .replace(/^(re:\s*)+/i, '')
+                .toLowerCase();
+            }
+
+            async function runAsfml(args) {
+              const { stdout } = await execFileAsync('npx', ['--yes', 'asfml', ...args], {
+                maxBuffer: 1024 * 1024,
+              });
+              return stdout;
+            }
+
+            async function locateThreadUrl() {
+              const normalizedTitle = normalizeSubject(title);
+              if (!normalizedTitle) {
+                core.info('Discussion title missing, cannot query mailing list');
+                return null;
+              }
+
+              const stdout = await runAsfml([
+                'search',
+                'dev@asyncband.apache.org',
+                title,
+                '--since',
+                '7d',
+                '--limit',
+                '20',
+                '--format',
+                'json',
+              ]);
+              const emails = JSON.parse(stdout);
+              if (!Array.isArray(emails) || !emails.length) {
+                return null;
+              }
+
+              const candidate =
+                emails.find(email => normalizeSubject(email.subject) === normalizedTitle) ||
+                emails[0];
+              const mid = candidate.mid || candidate.id;
+              if (!mid) {
+                return null;
+              }
+
+              try {
+                const rootStdout = await runAsfml(['read', mid, '--root', '--format', 'json']);
+                const root = JSON.parse(rootStdout);
+                const tid = root.mid || root.id || mid;
+                if (tid) {
+                  core.info('Found thread via asfml search');
+                  return `https://lists.apache.org/thread/${tid}`;
+                }
+              } catch (error) {
+                core.info(`Unable to resolve root email via asfml: ${error.message}`);
+              }
+
+              core.info('Found candidate email via asfml search');
+              return `https://lists.apache.org/thread/${mid}`;
+            }
+
+            const deadline = Date.now() + 15 * 60 * 1000;
+            const delays = [5, 10, 20, 30, 45, 60, 90, 120];
+            let attempt = 0;
+            let threadUrl = null;
+
+            while (Date.now() < deadline && !threadUrl) {
+              attempt += 1;
+              try {
+                threadUrl = await locateThreadUrl();
+              } catch (error) {
+                core.info(`Search failed: ${error.message}`);
+              }
+              if (threadUrl) {
+                break;
+              }
+              const wait = delays[Math.min(attempt - 1, delays.length - 1)];
+              core.info(`Thread not found yet, retrying in ${wait}s...`);
+              await new Promise(resolve => setTimeout(resolve, wait * 1000));
+            }
+
+            if (!threadUrl) {
+              core.setFailed('Timeout: thread not found yet');
+              return;
+            }
+
+            if (discussionRepoMismatch) {
+              core.warning(
+                `Discussion repository ${owner}/${repo} does not match workflow repository ` +
+                `${context.repo.owner}/${context.repo.repo}. Skipping comment.`
+              );
+              core.info(`Computed thread URL: ${threadUrl}`);
+              return;
+            }
+
+            const body = currentDiscussion.body || '';
+            if (body.includes(threadUrl)) {
+              core.info('Thread URL already present');
+              return;
+            }
+
+            const comments = currentDiscussion.comments?.nodes || [];
+            if (comments.some(comment => (comment.body || '').includes(threadUrl))) {
+              core.info('Thread URL already present in comments');
+              return;
+            }
+
+            const mutation = `
+              mutation ($discussionId: ID!, $body: String!) {
+                addDiscussionComment(input: { discussionId: $discussionId, body: $body }) {
+                  comment {
+                    url
+                  }
+                }
+              }
+            `;
+            const result = await github.graphql(mutation, {
+              discussionId: currentDiscussion.id,
+              body: `**Mailing list thread:** ${threadUrl}`,
+            });
+
+            core.info(`Commented ${threadUrl}: ${result.addDiscussionComment.comment.url}`);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@asyncband.apache.org
For additional commands, e-mail: commits-help@asyncband.apache.org