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

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new c26e2cbe0 ci: Auto add ML thread link in github discussion (#6709)
c26e2cbe0 is described below

commit c26e2cbe09fbe246789eafac298113fd75d5462c
Author: Xuanwo <[email protected]>
AuthorDate: Mon Oct 20 16:22:58 2025 +0900

    ci: Auto add ML thread link in github discussion (#6709)
    
    * ci: Auto add ML thread link in github discussion
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Fix licenses
    
    Signed-off-by: Xuanwo <[email protected]>
    
    ---------
    
    Signed-off-by: Xuanwo <[email protected]>
---
 .github/workflows/discussion-thread-link.yml | 188 +++++++++++++++++++++++++++
 1 file changed, 188 insertions(+)

diff --git a/.github/workflows/discussion-thread-link.yml 
b/.github/workflows/discussion-thread-link.yml
new file mode 100644
index 000000000..96aa61f11
--- /dev/null
+++ b/.github/workflows/discussion-thread-link.yml
@@ -0,0 +1,188 @@
+# 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]
+
+permissions:
+  discussions: write
+  contents: read
+
+jobs:
+  link-thread:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Append thread URL
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const { context, github, core } = require('@actions/github');
+
+            const owner = context.repo.owner;
+            const repo = context.repo.repo;
+            const discussion = context.payload.discussion;
+            const number = discussion.number;
+            const title = discussion.title || '';
+
+            const baseUrl = 'https://lists.apache.org/api/stats.lua';
+            const list = 'dev';
+            const domain = 'opendal.apache.org';
+            const searchParams = { header_subject: title, d: 'lte=7d' };
+
+            function normalize(value) {
+              return (value || '').trim().toLowerCase();
+            }
+
+            function flatten(nodes) {
+              const result = [];
+              const stack = [...(nodes || [])];
+              while (stack.length) {
+                const current = stack.pop();
+                result.push(current);
+                if (Array.isArray(current.children) && 
current.children.length) {
+                  stack.push(...current.children);
+                }
+              }
+              return result;
+            }
+
+            async function fetchStats(params) {
+              const searchParams = new URLSearchParams({ list, domain });
+              for (const [key, value] of Object.entries(params)) {
+                if (value) {
+                  searchParams.set(key, value);
+                }
+              }
+              const response = await 
fetch(`${baseUrl}?${searchParams.toString()}`, {
+                headers: { accept: 'application/json' },
+              });
+              if (!response.ok) {
+                throw new Error(`lists API ${response.status}`);
+              }
+              return response.json();
+            }
+
+            function extractTid(stats, normalizedTitle) {
+              const threads = flatten(stats.thread_struct);
+              if (!threads.length) {
+                return null;
+              }
+
+              const emails = Array.isArray(stats.emails) ? stats.emails : [];
+              const rootEmails = emails
+                .filter(email => !email['in-reply-to'])
+                .sort((a, b) => a.epoch - b.epoch);
+
+              for (const email of rootEmails) {
+                const match = threads.find(thread => thread.tid === email.id);
+                if (match) {
+                  return match.tid;
+                }
+              }
+
+              const normalizedSubjects = new Set([normalizedTitle]);
+              for (const email of rootEmails) {
+                normalizedSubjects.add(normalize(email.subject));
+              }
+
+              const rootThreads = threads.filter(thread => thread.nest === 1);
+              for (const subject of normalizedSubjects) {
+                const match = rootThreads.find(thread => 
normalize(thread.subject) === subject);
+                if (match) {
+                  return match.tid;
+                }
+              }
+
+              if (rootThreads.length === 1) {
+                return rootThreads[0].tid;
+              }
+
+              for (const subject of normalizedSubjects) {
+                const match = threads.find(thread => normalize(thread.subject) 
=== subject);
+                if (match) {
+                  return match.tid;
+                }
+              }
+
+              return null;
+            }
+
+            async function locateThreadTid() {
+              const normalizedTitle = normalize(title);
+              if (!normalizedTitle) {
+                core.info('Discussion title missing, cannot query mailing 
list');
+                return null;
+              }
+              try {
+                const stats = await fetchStats(searchParams);
+                const tid = extractTid(stats, normalizedTitle);
+                if (tid) {
+                  core.info('Found thread via header_subject search');
+                  return tid;
+                }
+              } catch (error) {
+                core.info(`Search failed: ${error.message}`);
+              }
+              return null;
+            }
+
+            const deadline = Date.now() + 15 * 60 * 1000;
+            const delays = [5, 10, 20, 30, 45, 60, 90, 120];
+            let attempt = 0;
+            let tid = null;
+
+            while (Date.now() < deadline && !tid) {
+              attempt += 1;
+              tid = await locateThreadTid();
+              if (tid) {
+                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 (!tid) {
+              core.setFailed('Timeout: thread not found yet');
+              return;
+            }
+
+            const threadUrl = `https://lists.apache.org/thread/${tid}`;
+
+            const { data: current } = await github.request(
+              'GET /repos/{owner}/{repo}/discussions/{number}',
+              { owner, repo, number }
+            );
+
+            const body = current.body || '';
+            if (body.includes(threadUrl)) {
+              core.info('Thread URL already present');
+              return;
+            }
+
+            const separator = body.trim().length ? '\n\n' : '';
+            const newBody = `${body}${separator}---\n**Mailing list thread:** 
${threadUrl}\n`;
+
+            await github.request(
+              'PATCH /repos/{owner}/{repo}/discussions/{discussion_number}',
+              { owner, repo, discussion_number: number, body: newBody }
+            );
+
+            core.info(`Appended ${threadUrl}`);

Reply via email to