[Bug]: Severe UI lag and stuttering after stopping recording on macOS (M1) #558
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR to Discord Forum | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, edited, labeled, unlabeled, closed] | |
| pull_request_review: | |
| types: [submitted] | |
| issue_comment: | |
| types: [created] | |
| schedule: | |
| - cron: "0 12 * * 1" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: read | |
| jobs: | |
| notify: | |
| if: github.event_name != 'schedule' && github.actor != 'github-actions[bot]' | |
| concurrency: | |
| group: discord-pr-sync-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} | |
| cancel-in-progress: false | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Sync PR activity to Discord forum thread | |
| id: sync | |
| uses: actions/github-script@v7 | |
| env: | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| DISCORD_PR_FORUM_WEBHOOK: ${{ secrets.DISCORD_PR_FORUM_WEBHOOK }} | |
| DISCORD_WEBHOOK_USERNAME: ${{ secrets.DISCORD_WEBHOOK_USERNAME }} | |
| DISCORD_WEBHOOK_AVATAR_URL: ${{ secrets.DISCORD_WEBHOOK_AVATAR_URL }} | |
| DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} | |
| DISCORD_REVIEWER_ROLE_ID: ${{ secrets.DISCORD_REVIEWER_ROLE_ID }} | |
| DISCORD_ALERT_WEBHOOK_URL: ${{ secrets.DISCORD_ALERT_WEBHOOK_URL }} | |
| with: | |
| script: | | |
| const WEBHOOK_USERNAME = (process.env.DISCORD_WEBHOOK_USERNAME || "OpenScreen").trim(); | |
| const WEBHOOK_AVATAR = (process.env.DISCORD_WEBHOOK_AVATAR_URL || "").trim(); | |
| const THREAD_MARKER_REGEX = /<!--\s*discord-thread-id:(\d+)\s*-->/i; | |
| const webhookUrl = (process.env.DISCORD_WEBHOOK_URL || process.env.DISCORD_PR_FORUM_WEBHOOK || "").trim(); | |
| const botToken = (process.env.DISCORD_BOT_TOKEN || "").trim(); | |
| const reviewerRoleId = (process.env.DISCORD_REVIEWER_ROLE_ID || "").trim(); | |
| const alertWebhookUrl = (process.env.DISCORD_ALERT_WEBHOOK_URL || "").trim(); | |
| const TAGS = { | |
| open: "1493976692967080096", | |
| draft: "1493976782028935279", | |
| ready: "1493976833626996756", | |
| changes: "1493976909875515564", | |
| approved: "1493976951038152764", | |
| merged: "1493977049709281320", | |
| closed: "1493977108102516786", | |
| }; | |
| const labelTagMap = { | |
| bug: "1493977562773458975", | |
| enhancement: "1493977619216207993", | |
| documentation: "1493978565153394830", | |
| }; | |
| function cleanDescription(text, maxLen = 3500) { | |
| if (!text) return "No description provided."; | |
| const normalized = text | |
| .replace(/\r\n/g, "\n") | |
| .replace(/\n{3,}/g, "\n\n") | |
| .trim(); | |
| if (normalized.length <= maxLen) return normalized; | |
| return `${normalized.slice(0, maxLen - 1)}…`; | |
| } | |
| function trimThreadName(name) { | |
| return name.length > 95 ? name.slice(0, 95) : name; | |
| } | |
| function extractThreadId(body) { | |
| if (!body) return null; | |
| const match = body.match(THREAD_MARKER_REGEX); | |
| return match ? match[1] : null; | |
| } | |
| function upsertThreadMarker(body, threadId) { | |
| const cleaned = (body || "").replace(THREAD_MARKER_REGEX, "").trim(); | |
| return `${cleaned}\n\n<!-- discord-thread-id:${threadId} -->`.trim(); | |
| } | |
| async function discordPost(payload, options = {}) { | |
| const endpoint = new URL(webhookUrl); | |
| endpoint.searchParams.set("wait", "true"); | |
| if (options.threadId) endpoint.searchParams.set("thread_id", String(options.threadId)); | |
| const response = await fetch(endpoint.toString(), { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: WEBHOOK_USERNAME, | |
| avatar_url: WEBHOOK_AVATAR, | |
| allowed_mentions: { parse: [] }, | |
| ...payload, | |
| }) | |
| }); | |
| const contentType = (response.headers.get("content-type") || "").toLowerCase(); | |
| const text = await response.text(); | |
| if (!response.ok) { | |
| throw new Error(`Discord API error ${response.status}: ${text}`); | |
| } | |
| if (!text) return {}; | |
| if (contentType.includes("application/json")) return JSON.parse(text); | |
| // Some proxy/CDN edge responses may return HTML with 2xx; avoid crashing on JSON parse. | |
| core.warning(`Discord webhook returned non-JSON response (content-type: ${contentType || "unknown"}).`); | |
| return {}; | |
| } | |
| async function patchDiscordThread(threadId, patchBody) { | |
| if (!botToken || !threadId) return; | |
| const response = await fetch(`https://discord.com/api/v10/channels/${threadId}`, { | |
| method: "PATCH", | |
| headers: { | |
| "Authorization": `Bot ${botToken}`, | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify(patchBody), | |
| }); | |
| if (!response.ok) { | |
| const text = await response.text(); | |
| core.warning(`Discord thread patch failed (${response.status}): ${text}`); | |
| } | |
| } | |
| function desiredStatusTag(prState) { | |
| if (prState.merged && TAGS.merged) return TAGS.merged; | |
| if (prState.closed && !prState.merged && TAGS.closed) return TAGS.closed; | |
| if (prState.reviewState === "CHANGES_REQUESTED" && TAGS.changes) return TAGS.changes; | |
| if (prState.reviewState === "APPROVED" && TAGS.approved) return TAGS.approved; | |
| if (prState.draft && TAGS.draft) return TAGS.draft; | |
| if (!prState.draft && TAGS.ready) return TAGS.ready; | |
| return TAGS.open || null; | |
| } | |
| function tagIdsFromLabels(labels) { | |
| const out = []; | |
| for (const label of labels) { | |
| const mapped = labelTagMap[label.toLowerCase()] || labelTagMap[label]; | |
| if (mapped) out.push(String(mapped)); | |
| } | |
| return out; | |
| } | |
| async function getPullRequest() { | |
| if (context.eventName === "pull_request_target" || context.eventName === "pull_request_review") { | |
| return context.payload.pull_request || null; | |
| } | |
| if (context.eventName === "issue_comment") { | |
| const issue = context.payload.issue; | |
| if (!issue?.pull_request) return null; | |
| const { data } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: issue.number, | |
| }); | |
| return data; | |
| } | |
| return null; | |
| } | |
| async function getReviewState(owner, repo, pullNumber) { | |
| const { data } = await github.rest.pulls.listReviews({ owner, repo, pull_number: pullNumber, per_page: 100 }); | |
| let hasChanges = false; | |
| let hasApproved = false; | |
| for (const r of data) { | |
| const s = (r.state || "").toUpperCase(); | |
| if (s === "CHANGES_REQUESTED") hasChanges = true; | |
| if (s === "APPROVED") hasApproved = true; | |
| } | |
| if (hasChanges) return "CHANGES_REQUESTED"; | |
| if (hasApproved) return "APPROVED"; | |
| return "NONE"; | |
| } | |
| async function sendFailureAlert(message) { | |
| if (!alertWebhookUrl) return; | |
| try { | |
| await fetch(alertWebhookUrl, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: "OpenScreen", | |
| avatar_url: WEBHOOK_AVATAR, | |
| content: `⚠️ PR Discord sync failed\n${message}\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| allowed_mentions: { parse: [] } | |
| }) | |
| }); | |
| } catch { | |
| core.warning("Failed to send failure alert webhook."); | |
| } | |
| } | |
| try { | |
| const pr = await getPullRequest(); | |
| if (!pr) { | |
| core.info("No PR context found. Skipping."); | |
| return; | |
| } | |
| if (!webhookUrl) { | |
| const strictEvents = new Set(["pull_request_target", "workflow_dispatch"]); | |
| const msg = | |
| `Discord sync skipped: webhook secret unavailable for event '${context.eventName}'. ` + | |
| "Set either DISCORD_WEBHOOK_URL or DISCORD_PR_FORUM_WEBHOOK in repository secrets."; | |
| if (strictEvents.has(context.eventName)) { | |
| core.setFailed(msg); | |
| } else { | |
| core.warning(msg); | |
| } | |
| return; | |
| } | |
| const action = context.payload.action || ""; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const number = pr.number; | |
| const title = pr.title; | |
| const author = pr.user?.login || "unknown"; | |
| const url = pr.html_url; | |
| const authorUrl = pr.user?.html_url || ""; | |
| const authorAvatar = pr.user?.avatar_url || ""; | |
| const base = pr.base?.ref || ""; | |
| const head = pr.head?.ref || ""; | |
| const repoFullName = pr.base?.repo?.full_name || `${owner}/${repo}`; | |
| const labels = (pr.labels || []).map((l) => l.name); | |
| const body = (pr.body || "").trim(); | |
| const reviewState = await getReviewState(owner, repo, number); | |
| let threadId = extractThreadId(body); | |
| const shouldCreateThread = | |
| context.eventName === "pull_request_target" && | |
| ["opened", "reopened", "ready_for_review"].includes(action) && | |
| !threadId; | |
| if (shouldCreateThread) { | |
| const fields = [ | |
| { name: "PR", value: `[#${number}](${url})`, inline: true }, | |
| { name: "Author", value: `[${author}](${authorUrl || url})`, inline: true }, | |
| { name: "Status", value: pr.draft ? "Draft" : "Open", inline: true }, | |
| { name: "Branches", value: `\`${head}\` -> \`${base}\``, inline: true }, | |
| { name: "Changes", value: `+${pr.additions} / -${pr.deletions}`, inline: true }, | |
| { name: "Files Changed", value: String(pr.changed_files), inline: true } | |
| ]; | |
| if (labels.length) { | |
| fields.push({ | |
| name: "Labels", | |
| value: labels.map((l) => `\`${l}\``).join(" "), | |
| inline: false, | |
| }); | |
| } | |
| const statusTag = desiredStatusTag({ draft: pr.draft, reviewState, merged: false, closed: false }); | |
| const mappedLabelTags = tagIdsFromLabels(labels); | |
| const appliedTags = [...new Set([statusTag, ...mappedLabelTags].filter(Boolean))]; | |
| const createPayload = { | |
| content: action === "ready_for_review" ? "🔔 PR is now ready for review" : "🔔 New pull request opened", | |
| thread_name: trimThreadName(`PR #${number} - ${title}`), | |
| applied_tags: appliedTags, | |
| embeds: [ | |
| { | |
| title: `PR #${number}: ${title}`, | |
| url, | |
| description: cleanDescription(body), | |
| color: pr.draft ? 15105570 : 1998671, | |
| author: { | |
| name: author, | |
| url: authorUrl || undefined, | |
| icon_url: authorAvatar || undefined, | |
| }, | |
| fields, | |
| footer: { text: repoFullName }, | |
| timestamp: new Date().toISOString(), | |
| }, | |
| ], | |
| }; | |
| const result = await discordPost(createPayload); | |
| const createdThreadId = result.channel_id || null; | |
| if (createdThreadId) { | |
| const updatedBody = upsertThreadMarker(body, createdThreadId); | |
| await github.rest.pulls.update({ owner, repo, pull_number: number, body: updatedBody }); | |
| core.info(`Created Discord thread ${createdThreadId} and stored mapping.`); | |
| } else { | |
| core.warning("Discord thread created but channel_id missing in response."); | |
| } | |
| return; | |
| } | |
| if (!threadId) { | |
| core.info("No mapped Discord thread ID found; skipping update event."); | |
| return; | |
| } | |
| if (context.eventName === "pull_request_target" && ["edited", "labeled", "unlabeled", "ready_for_review", "converted_to_draft"].includes(action)) { | |
| const statusTag = desiredStatusTag({ | |
| draft: action === "converted_to_draft" ? true : pr.draft, | |
| reviewState, | |
| merged: false, | |
| closed: false, | |
| }); | |
| const mappedLabelTags = tagIdsFromLabels(labels); | |
| const appliedTags = [...new Set([statusTag, ...mappedLabelTags].filter(Boolean))]; | |
| await patchDiscordThread(threadId, { | |
| name: trimThreadName(`PR #${number} - ${title}`), | |
| ...(appliedTags.length ? { applied_tags: appliedTags } : {}), | |
| }); | |
| } | |
| let updateMessage = null; | |
| let updateEmbed = null; | |
| if (context.eventName === "pull_request_target") { | |
| if (action === "synchronize") { | |
| const { data: commits } = await github.rest.pulls.listCommits({ owner, repo, pull_number: number, per_page: 5 }); | |
| const list = commits.map((c) => `- \`${c.sha.slice(0, 7)}\` ${c.commit.message.split("\n")[0]}`).join("\n") || "- No commit details"; | |
| updateMessage = `🧩 New commits pushed to PR #${number}`; | |
| updateEmbed = { | |
| title: `Commit Update • PR #${number}`, | |
| url: `${url}/files`, | |
| description: `${list}`, | |
| color: 1998671, | |
| footer: { text: repoFullName }, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| } else if (action === "edited") { | |
| updateMessage = `✏️ PR #${number} details were edited`; | |
| updateEmbed = { | |
| title: `PR Updated • #${number}`, | |
| url, | |
| description: cleanDescription(body, 1200), | |
| color: 1998671, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| } else if (action === "closed") { | |
| const isMerged = !!pr.merged; | |
| const statusTag = desiredStatusTag({ draft: false, reviewState, merged: isMerged, closed: true }); | |
| const mappedLabelTags = tagIdsFromLabels(labels); | |
| const appliedTags = [...new Set([statusTag, ...mappedLabelTags].filter(Boolean))]; | |
| await patchDiscordThread(threadId, { | |
| ...(appliedTags.length ? { applied_tags: appliedTags } : {}), | |
| ...(isMerged ? { archived: true, locked: true } : {}), | |
| }); | |
| updateMessage = isMerged | |
| ? `✅ PR #${number} was merged` | |
| : `🛑 PR #${number} was closed without merge`; | |
| updateEmbed = { | |
| title: isMerged ? `Merged • PR #${number}` : `Closed • PR #${number}`, | |
| url, | |
| description: isMerged ? "This PR has been merged into the base branch." : "This PR was closed before merge.", | |
| color: isMerged ? 5763719 : 15158332, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| } else if (action === "ready_for_review") { | |
| updateMessage = `🚀 PR #${number} moved from draft to ready for review`; | |
| if (reviewerRoleId) updateMessage += ` <@&${reviewerRoleId}>`; | |
| } else if (action === "converted_to_draft") { | |
| updateMessage = `📝 PR #${number} converted to draft`; | |
| } | |
| } else if (context.eventName === "pull_request_review") { | |
| const review = context.payload.review; | |
| if (review) { | |
| const state = (review.state || "commented").toUpperCase(); | |
| const reviewer = review.user?.login || "reviewer"; | |
| updateMessage = `🧪 Review ${state} by **${reviewer}** on PR #${number}`; | |
| if (state === "CHANGES_REQUESTED" && reviewerRoleId) updateMessage += ` <@&${reviewerRoleId}>`; | |
| updateEmbed = { | |
| title: `Review ${state} • PR #${number}`, | |
| url: review.html_url || url, | |
| description: cleanDescription(review.body || "No review note.", 1000), | |
| color: state === "APPROVED" ? 5763719 : state === "CHANGES_REQUESTED" ? 15158332 : 1998671, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| if (state === "CHANGES_REQUESTED" || state === "APPROVED") { | |
| const statusTag = desiredStatusTag({ draft: pr.draft, reviewState: state, merged: false, closed: false }); | |
| const mappedLabelTags = tagIdsFromLabels(labels); | |
| const appliedTags = [...new Set([statusTag, ...mappedLabelTags].filter(Boolean))]; | |
| await patchDiscordThread(threadId, { | |
| ...(appliedTags.length ? { applied_tags: appliedTags } : {}), | |
| }); | |
| } | |
| } | |
| } else if (context.eventName === "issue_comment") { | |
| const comment = context.payload.comment; | |
| if (comment) { | |
| const commenter = comment.user?.login || "user"; | |
| updateMessage = `💬 New comment by **${commenter}** on PR #${number}`; | |
| updateEmbed = { | |
| title: `New PR Comment • #${number}`, | |
| url: comment.html_url || url, | |
| description: cleanDescription(comment.body || "No comment body.", 1000), | |
| color: 1998671, | |
| timestamp: new Date().toISOString(), | |
| }; | |
| } | |
| } | |
| if (!updateMessage && !updateEmbed) { | |
| core.info("No Discord update message for this event/action. Skipping."); | |
| return; | |
| } | |
| const payload = { content: updateMessage || "" }; | |
| if (updateEmbed) payload.embeds = [updateEmbed]; | |
| await discordPost(payload, { threadId }); | |
| core.info(`Posted update to Discord thread ${threadId}.`); | |
| } catch (err) { | |
| const msg = err && err.message ? err.message : String(err); | |
| core.setFailed(msg); | |
| const alertWebhook = process.env.DISCORD_ALERT_WEBHOOK_URL; | |
| if (alertWebhook) { | |
| try { | |
| await fetch(alertWebhook, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| username: "OpenScreen", | |
| avatar_url: WEBHOOK_AVATAR, | |
| content: `⚠️ PR->Discord sync failed\n${msg}\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| allowed_mentions: { parse: [] } | |
| }) | |
| }); | |
| } catch { | |
| core.warning("Failed to send alert webhook."); | |
| } | |
| } | |
| } | |
| weekly-contributor-leaderboard: | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Post weekly contributor leaderboard | |
| uses: actions/github-script@v7 | |
| env: | |
| DISCORD_SPOTLIGHT_WEBHOOK_URL: ${{ secrets.DISCORD_SPOTLIGHT_WEBHOOK_URL }} | |
| DISCORD_WEBHOOK_USERNAME: ${{ secrets.DISCORD_WEBHOOK_USERNAME }} | |
| DISCORD_WEBHOOK_AVATAR_URL: ${{ secrets.DISCORD_WEBHOOK_AVATAR_URL }} | |
| with: | |
| script: | | |
| const spotlightWebhook = (process.env.DISCORD_SPOTLIGHT_WEBHOOK_URL || "").trim(); | |
| const webhookUsername = (process.env.DISCORD_WEBHOOK_USERNAME || "OpenScreen").trim(); | |
| const webhookAvatar = (process.env.DISCORD_WEBHOOK_AVATAR_URL || "").trim(); | |
| if (!spotlightWebhook) { | |
| core.info("DISCORD_SPOTLIGHT_WEBHOOK_URL missing. Skipping leaderboard post."); | |
| return; | |
| } | |
| const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const q = `repo:${owner}/${repo} is:pr is:merged merged:>=${since.substring(0, 10)}`; | |
| const search = await github.rest.search.issuesAndPullRequests({ | |
| q, | |
| per_page: 100, | |
| }); | |
| const counter = new Map(); | |
| for (const item of search.data.items) { | |
| const login = item.user?.login; | |
| if (!login) continue; | |
| counter.set(login, (counter.get(login) || 0) + 1); | |
| } | |
| const ranked = [...counter.entries()] | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 10); | |
| const totalMerged = search.data.items.length; | |
| const lines = ranked.length | |
| ? ranked.map(([user, count], idx) => `${idx + 1}. **${user}** - ${count} merged PR(s)`).join("\n") | |
| : "No merged PRs this week."; | |
| const payload = { | |
| username: webhookUsername, | |
| ...(webhookAvatar ? { avatar_url: webhookAvatar } : {}), | |
| embeds: [ | |
| { | |
| title: "🌟 Weekly Contributor Leaderboard", | |
| description: lines, | |
| color: 1998671, | |
| fields: [ | |
| { name: "Merged PRs (7d)", value: String(totalMerged), inline: true }, | |
| { name: "Repository", value: `${owner}/${repo}`, inline: true }, | |
| { name: "Period", value: "Last 7 days", inline: true } | |
| ], | |
| timestamp: new Date().toISOString() | |
| } | |
| ], | |
| allowed_mentions: { parse: [] } | |
| }; | |
| const res = await fetch(`${spotlightWebhook}?wait=true`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!res.ok) { | |
| const txt = await res.text(); | |
| core.setFailed(`Leaderboard post failed ${res.status}: ${txt}`); | |
| } |