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