Files

268 lines
12 KiB
YAML

name: Convert compat issue to report PR
# `/compat` comment command on a compat mirror issue (created by the "Mirror
# game-status issues" poller): converts the mirror's upstream game-status
# report into src/content/compat/<game>-<os>.md and opens ONE PR for that game
# (branch compat/<game>-<os>). One game per PR keeps reviews separate — the
# old flow dumped every converted game into a single standing report PR.
#
# The mirror body's `## Source` footer records the upstream KytyPS5 issue
# number / URL / creation date (see scripts/lib/status-issues.mjs); the rest
# of the body is the upstream issue form, parsed by scripts/issue-to-compat.mjs.
# Re-running `/compat` (e.g. after the upstream issue was fixed) overwrites
# that game's report and updates the same PR in place. On success the comment
# gets a 👀 reaction and the mirror issue is closed — closed mirror = converted.
#
# Fields the issue can't express correctly can be fixed first with the /setos /
# /setid /settitle /setstatus commands (compat-set-fields.yml): they append a
# `## Overrides` section to the mirror body which issue-to-compat.mjs reads
# here — overrides win over the values parsed from the issue (e.g. an OS that
# doesn't generalize, or a missing PPSA-XXXXX). Comment `/compat` again after
# setting fields.
#
# Failures (not a mirror, unparseable fields, PR-step hiccup) post the reason
# back on the issue as a comment — `❌ /compat failed. <reason>` — so the
# maintainer sees it without digging through the workflow log; the run still
# goes red. The failing step writes /tmp/compat-failure.txt and the final
# "Post the failure as a comment" step (if: failure()) reads it.
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
issue_number:
description: "Mirror issue number to convert"
required: true
type: string
permissions:
contents: write
pull-requests: write
issues: write # reaction + close the mirror issue + failure comments
# Serialize /compat runs per issue so two quick comments can't race on the
# same branch.
concurrency:
group: compat-convert-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: true
jobs:
convert:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.issue.pull_request == null &&
startsWith(github.event.comment.body, '/compat') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Resolve mirror issue context
id: issue_context
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }}
EVENT_BODY: ${{ github.event.issue.body }}
EVENT_TITLE: ${{ github.event.issue.title }}
REPO: ${{ github.repository }}
run: |
node - <<'NODE'
(async () => {
const fs = require("node:fs");
const { mirrorSource } = await import("./scripts/lib/status-issues.mjs");
let body = process.env.EVENT_BODY ?? "";
let title = process.env.EVENT_TITLE ?? "";
const issueNum = process.env.ISSUE_NUMBER;
const repo = process.env.REPO;
const token = process.env.GH_TOKEN;
let issueUrl = `https://github.com/${repo}/issues/${issueNum}`;
if ((!body || !title) && issueNum) {
const res = await fetch(`https://api.github.com/repos/${repo}/issues/${issueNum}`, {
headers: { authorization: `Bearer ${token}`, "user-agent": "gh-actions" }
});
if (!res.ok) {
const msg = `Failed to fetch mirror issue #${issueNum}: HTTP ${res.status}`;
fs.writeFileSync("/tmp/compat-failure.txt", msg);
console.error("::error::" + msg);
process.exit(1);
}
const data = await res.json();
body = data.body ?? "";
title = data.title ?? title;
issueUrl = data.html_url || issueUrl;
}
fs.writeFileSync("/tmp/mirror-issue-body.txt", body);
const src = mirrorSource(body);
if (!src) {
const msg =
"This issue is not a compat mirror — its body has no `## Source` footer " +
"linking a KytyPS5 issue. /compat converts issues mirrored by the " +
"\"Mirror game-status issues\" workflow.";
fs.writeFileSync("/tmp/compat-failure.txt", msg);
console.error("::error::" + msg);
process.exit(1);
}
const cleanTitle = String(title ?? "").replace(/[\r\n]+/g, " ").trim();
const out = process.env.GITHUB_OUTPUT;
if (out) {
fs.appendFileSync(
out,
`mirror_number=${issueNum}\nmirror_title=${cleanTitle}\nmirror_url=${issueUrl}\nnumber=${src.number}\nurl=${src.url}\ncreated=${src.created}\n`
);
}
})();
NODE
- name: Convert the issue to a report
id: convert
env:
ISSUE_DATE: ${{ steps.issue_context.outputs.created }}
ISSUE_NUM: ${{ steps.issue_context.outputs.number }}
ISSUE_URL: ${{ steps.issue_context.outputs.url }}
MIRROR_TITLE: ${{ steps.issue_context.outputs.mirror_title }}
run: |
# Capture the converter's output so a failure can be posted back as
# a readable comment (the final step reads /tmp/compat-failure.txt).
if ! CONVERT_OUT="$(node scripts/issue-to-compat.mjs \
--issue-body-file /tmp/mirror-issue-body.txt \
--issue-title "$MIRROR_TITLE" \
--date "$ISSUE_DATE" \
--source "KytyPS5 issue #$ISSUE_NUM" \
--source-url "$ISSUE_URL" 2>&1)"; then
MSG="$(printf '%s' "$CONVERT_OUT" | sed -n 's/^\[issue-to-compat\] //p' | tail -1)"
[ -n "$MSG" ] || MSG="$(printf '%s' "$CONVERT_OUT" | tail -3 | tr '\n' ' ')"
printf '%s' "$MSG" > /tmp/compat-failure.txt
exit 1
fi
SLUG="$(printf '%s\n' "$CONVERT_OUT" | sed -n 's#^\[issue-to-compat\] wrote src/content/compat/\([^/]*\)\.md$#\1#p' | head -1)"
[ -n "$SLUG" ] || {
echo "::error::The converter did not report a written report file."
printf 'The converter did not report a written report file.' > /tmp/compat-failure.txt
exit 1
}
TITLE="$(sed -n 's/^title: "\(.*\)"$/\1/p' "src/content/compat/$SLUG.md" | head -1)"
OS="$(sed -n 's/^os: "\(.*\)"$/\1/p' "src/content/compat/$SLUG.md" | head -1)"
echo "slug=$SLUG" >> "$GITHUB_OUTPUT"
echo "pr_title=compat: $TITLE ($OS)" >> "$GITHUB_OUTPUT"
if ! node scripts/validate-compat.mjs >/tmp/compat-validate.log 2>&1; then
printf 'The generated report is invalid — %s' "$(tail -1 /tmp/compat-validate.log)" > /tmp/compat-failure.txt
exit 1
fi
- name: Open or update the report PR
id: cpr
uses: peter-evans/create-pull-request@v6
continue-on-error: true
with:
token: ${{ secrets.ACTIONS_BOT_TOKEN || secrets.GITHUB_TOKEN }}
add-paths: |
src/content/compat/
commit-message: "${{ steps.convert.outputs.pr_title }}"
branch: compat/${{ steps.convert.outputs.slug }}
title: ${{ steps.convert.outputs.pr_title }}
body: |
Auto-converted from ${{ steps.issue_context.outputs.mirror_url }} via `/compat`.
${{ steps.issue_context.outputs.url }}
<!-- mirror-issue: ${{ steps.issue_context.outputs.mirror_number }} -->
delete-branch: true
- name: Fail the job if the report PR could not be opened
if: steps.cpr.outcome == 'failure'
env:
SLUG: ${{ steps.convert.outputs.slug }}
run: |
printf 'Failed to open or update the report PR (branch compat/%s) — the branch may already exist without a PR. See the workflow log for the create-pull-request error.' "$SLUG" > /tmp/compat-failure.txt
exit 1
- name: Enable auto-merge on the report PR
id: auto_merge
if: steps.cpr.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.ACTIONS_BOT_TOKEN || secrets.GITHUB_TOKEN }}
CPR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
CPR_URL: ${{ steps.cpr.outputs.pull-request-url }}
SLUG: ${{ steps.convert.outputs.slug }}
run: |
PR_NUMBER="$CPR_NUMBER"
PR_URL="$CPR_URL"
if [ -z "$PR_NUMBER" ]; then
PR_NUMBER="$(gh pr list --head "compat/${SLUG}" --state open --json number --jq '.[0].number // empty')"
if [ -n "$PR_NUMBER" ]; then
PR_URL="$(gh pr view "$PR_NUMBER" --json url --jq '.url // empty')"
fi
fi
if [ -n "$PR_NUMBER" ]; then
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
if gh pr merge "$PR_NUMBER" --auto --squash; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
PR_STATUS="$(gh pr view "$PR_NUMBER" --json autoMergeRequest,state --jq '.autoMergeRequest != null or .state == "MERGED"' 2>/dev/null || true)"
if [ "$PR_STATUS" = "true" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::Failed to enable auto-merge on PR #$PR_NUMBER"
fi
fi
else
echo "enabled=noop" >> "$GITHUB_OUTPUT"
fi
- name: Acknowledge and close the mirror
if: steps.cpr.outcome == 'success' && steps.convert.outputs.slug != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
PR_TITLE: ${{ steps.convert.outputs.pr_title }}
PR_URL: ${{ steps.auto_merge.outputs.pr_url || steps.cpr.outputs.pull-request-url }}
AUTO_MERGE_ENABLED: ${{ steps.auto_merge.outputs.enabled }}
MIRROR_NUMBER: ${{ steps.issue_context.outputs.mirror_number }}
run: |
if [ "$EVENT_NAME" = "issue_comment" ]; then
gh api --method POST \
"repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content=eyes || true
fi
if [ "$AUTO_MERGE_ENABLED" = "false" ]; then
gh issue comment "$MIRROR_NUMBER" \
-b "⚠️ Report PR was opened: ${PR_TITLE} (${PR_URL}), but auto-merge could not be enabled. The mirror issue will remain open until the PR is merged manually."
elif [ "$AUTO_MERGE_ENABLED" = "noop" ]; then
gh issue close "$MIRROR_NUMBER" \
--comment "Compatibility report (${PR_TITLE}) is already up to date on main; no pull request needed."
else
gh issue close "$MIRROR_NUMBER" \
--comment "Converted — report PR: ${PR_TITLE} (${PR_URL})"
fi
- name: Post the failure as a comment
if: failure()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TARGET_ISSUE: ${{ steps.issue_context.outputs.mirror_number || inputs.issue_number || github.event.issue.number }}
run: |
if [ ! -f /tmp/compat-failure.txt ]; then
echo "no captured failure message — skipping comment"
exit 0
fi
if [ -n "$TARGET_ISSUE" ]; then
MSG="$(cat /tmp/compat-failure.txt)"
gh api --method POST \
"repos/${REPO}/issues/${TARGET_ISSUE}/comments" \
-f "body=❌ **/compat failed.** $MSG" || true
fi