Просмотр исходного кода

fix(triage): parse SteamCMD app_info and respect manual Linux labels (#5063)

The Linux support check regex-matched the whole SteamCMD app_info dump, so
the Steamworks SDK redistributable depots (1004-1006) counted as Linux server
support and Windows-only servers were labelled "linux support confirmed".
Parse the app_info VDF and decide from common.oslist, launch entries and the
server's own depots, ignoring redistributable and shared install depots.

The check also overwrote manual corrections of the Linux status labels on
every issue event. If the last change to either label was made by a person,
keep it and note the override in the check comment.
Daniel Gibbs 2 дней назад
Родитель
Сommit
907d271154
1 измененных файлов с 104 добавлено и 18 удалено
  1. 104 18
      .github/workflows/action-issue-triage-automation.yml

+ 104 - 18
.github/workflows/action-issue-triage-automation.yml

@@ -236,6 +236,38 @@ jobs:
               return false;
             }
 
+            function parseVdf(text) {
+              const root = {};
+              const stack = [root];
+              let pendingKey = null;
+              const tokenPattern = /"((?:[^"\\]|\\.)*)"|([{}])/g;
+              let match;
+              while ((match = tokenPattern.exec(text)) !== null) {
+                const current = stack[stack.length - 1];
+                if (match[2] === '{') {
+                  const child = {};
+                  if (pendingKey !== null) current[pendingKey] = child;
+                  stack.push(child);
+                  pendingKey = null;
+                } else if (match[2] === '}') {
+                  if (stack.length > 1) stack.pop();
+                  pendingKey = null;
+                } else if (pendingKey === null) {
+                  pendingKey = match[1];
+                } else {
+                  current[pendingKey] = match[1];
+                  pendingKey = null;
+                }
+              }
+              return root;
+            }
+
+            function parseSteamAppInfo(output, appId) {
+              const start = output.search(new RegExp(`^"${appId}"\\s*$`, 'm'));
+              if (start === -1) return null;
+              return parseVdf(output.slice(start))[String(appId)] ?? null;
+            }
+
             function runSteamCmdLinuxCheck(appId) {
               if (!appId) {
                 return { status: 'skipped', reason: 'No Steam AppID provided.' };
@@ -271,23 +303,44 @@ jobs:
                   maxBuffer: 10 * 1024 * 1024,
                 });
 
-                const normalized = output.toLowerCase();
-                const linuxSignals = [
-                  /"oslist"\s+"linux"/i,
-                  /"oslist"\s+"linux,windows"/i,
-                  /"oslist"\s+"windows,linux"/i,
-                  /"platforms"[\s\S]*?"linux"\s+"1"/i,
-                  /linux32/i,
-                  /linux64/i,
-                ];
-                const windowsOnlySignals = [
-                  /"oslist"\s+"windows"/i,
-                  /"platforms"[\s\S]*?"windows"\s+"1"/i,
-                ];
-
-                const hasLinuxSignal = linuxSignals.some((re) => re.test(normalized));
+                const appInfo = parseSteamAppInfo(output, appId);
+                if (!appInfo) {
+                  return {
+                    status: 'unknown',
+                    reason: `SteamCMD app_info returned no data for AppID ${appId}.`,
+                  };
+                }
+
+                const osListOf = (node) => String(node?.config?.oslist ?? '').toLowerCase();
+                const isObject = (value) => value && typeof value === 'object';
+                const commonOsList = String(appInfo.common?.oslist ?? '').toLowerCase();
+                const launches = Object.values(appInfo.config?.launch ?? {}).filter(isObject);
+                const serverDepots = Object.entries(appInfo.depots ?? {}).filter(
+                  ([id, depot]) =>
+                    /^\d+$/.test(id) &&
+                    isObject(depot) &&
+                    !['1004', '1005', '1006'].includes(id) &&
+                    depot.depotfromapp === undefined &&
+                    depot.sharedinstall !== '1'
+                );
+
+                const hasLinuxSignal =
+                  commonOsList.includes('linux') ||
+                  launches.some(
+                    (launch) =>
+                      osListOf(launch).includes('linux') ||
+                      (!osListOf(launch) && /\.(sh|x86_64)$/i.test(launch.executable ?? ''))
+                  ) ||
+                  serverDepots.some(([, depot]) => osListOf(depot).includes('linux'));
                 const hasWindowsOnlySignal =
-                  !hasLinuxSignal && windowsOnlySignals.some((re) => re.test(normalized));
+                  !hasLinuxSignal &&
+                  (commonOsList === 'windows' ||
+                    (launches.length > 0 &&
+                      launches.every(
+                        (launch) =>
+                          osListOf(launch) === 'windows' ||
+                          /\.(exe|bat|cmd)$/i.test(launch.executable ?? '')
+                      )));
 
                 if (hasLinuxSignal) {
                   return {
@@ -1010,8 +1063,36 @@ jobs:
               const CONFIRMED_LINUX_LABEL = 'status: linux support confirmed';
               const LINUX_MARKER = '<!-- linux-support-check -->';
               const steamDbLink = steamAppId ? `https://steamdb.info/app/${steamAppId}/` : null;
-              const shouldApplyNoLinuxLabel = confirmedNoLinux || suggestsNoLinux;
-              const shouldApplyConfirmedLinuxLabel = confirmedLinuxSupport && !confirmedNoLinux && !suggestsNoLinux;
+
+              let linuxLabelOverrideBy = null;
+              try {
+                const issueEvents = await github.paginate(github.rest.issues.listEvents, {
+                  owner,
+                  repo,
+                  issue_number: issueNumber,
+                  per_page: 100,
+                });
+                const lastLinuxLabelEvent = issueEvents
+                  .filter(
+                    (e) =>
+                      ['labeled', 'unlabeled'].includes(e.event) &&
+                      [NO_LINUX_LABEL, CONFIRMED_LINUX_LABEL].includes(e.label?.name)
+                  )
+                  .at(-1);
+                if (lastLinuxLabelEvent && lastLinuxLabelEvent.actor?.type !== 'Bot') {
+                  linuxLabelOverrideBy = lastLinuxLabelEvent.actor?.login ?? 'a maintainer';
+                  console.log(`Linux support label last set by ${linuxLabelOverrideBy}; keeping it.`);
+                }
+              } catch (err) {
+                console.log(`Could not read issue events: ${err.message}`);
+              }
+
+              const shouldApplyNoLinuxLabel = linuxLabelOverrideBy
+                ? existingLabels.has(NO_LINUX_LABEL)
+                : confirmedNoLinux || suggestsNoLinux;
+              const shouldApplyConfirmedLinuxLabel = linuxLabelOverrideBy
+                ? existingLabels.has(CONFIRMED_LINUX_LABEL) && !existingLabels.has(NO_LINUX_LABEL)
+                : confirmedLinuxSupport && !confirmedNoLinux && !suggestsNoLinux;
 
               if (shouldApplyNoLinuxLabel) {
                 // Auto-create the label if it does not exist yet.
@@ -1162,9 +1243,14 @@ jobs:
                 ? `**Label applied:** ${CONFIRMED_LINUX_LABEL}\n\n`
                 : '';
 
+              const overrideBlock = linuxLabelOverrideBy
+                ? `**Maintainer override:** the Linux support label was set manually by @${linuxLabelOverrideBy} and is no longer changed automatically.\n\n`
+                : '';
+
               const linuxCommentBody =
                 `${LINUX_MARKER}\n` +
                 `${linuxCommentHeader}\n\n` +
+                `${overrideBlock}` +
                 `${verdictLine}\n\n` +
                 `${confirmedLinuxLabelBlock}` +
                 `${steamBlock}` +