Publish OpenScreen 1.4.11 update assets

This commit is contained in:
huanld
2026-06-05 16:33:17 +07:00
commit 4825fa3043
448 changed files with 80631 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
* @siddharthvaddem
+149
View File
@@ -0,0 +1,149 @@
name: Bug Report
description: Create a report to help us improve
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: checkboxes
attributes:
label: Search existing issues
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: e.g., When I click submit, nothing happens...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
placeholder: e.g., The form should submit and show a success message
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: Drag and drop images here or paste them
validations:
required: false
- type: dropdown
id: os-type
attributes:
label: OS
description: Operating system
options:
- Windows
- macOS
- Linux
- iOS
- Android
- Other
validations:
required: false
- type: input
id: os-version
attributes:
label: OS Version
description: Please specify your OS version
placeholder: e.g., Windows 11, macOS Sonoma, Ubuntu 22.04
validations:
required: false
- type: input
id: os-other
attributes:
label: Other OS
description: If you selected "Other" for OS, please specify your operating system
placeholder: e.g., FreeBSD, Solaris
validations:
required: false
- type: dropdown
id: browser
attributes:
label: Browser
description: What browser are you using?
options:
- Chrome
- Firefox
- Safari
- Edge
- Other
validations:
required: false
- type: input
id: browser-version
attributes:
label: Browser Version
description: Please specify your browser version
placeholder: e.g., 120.0, 121.0.1
validations:
required: false
- type: input
id: browser-other
attributes:
label: Other Browser
description: If you selected "Other" for Browser, please specify your browser
placeholder: e.g., Brave, Vivaldi, Opera
validations:
required: false
- type: dropdown
id: device-type
attributes:
label: Device Type
description: Device category
options:
- Desktop
- Laptop
- Tablet
- Mobile
- Other
validations:
required: false
- type: input
id: device-other
attributes:
label: Other Device
description: If you selected "Other" for Device Type, please specify your device
placeholder: e.g., Smart TV, IoT device
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here.
placeholder: Links, references, or any additional information
validations:
required: false
@@ -0,0 +1,48 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature]: "
labels: ["enhancement", "feature-request"]
body:
- type: checkboxes
attributes:
label: Search existing issues
description: Please search to see if an issue already exists for this feature request.
options:
- label: I have searched the existing issues
required: true
- type: textarea
id: problem-description
attributes:
label: Is your feature request related to a problem?
description: A clear and concise description of what the problem is.
placeholder: e.g., I'm always frustrated when I have to...
validations:
required: true
- type: textarea
id: solution-description
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: Describe the feature or change you're proposing
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
placeholder: Have you considered any workarounds or alternative approaches?
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
placeholder: Links, mockups, or any additional information
validations:
required: false
+43
View File
@@ -0,0 +1,43 @@
# Pull Request Template
## Description
<!-- Briefly describe the purpose of this PR. -->
## Motivation
<!-- Explain why this change is needed. What problem does it solve? -->
## Type of Change
- [ ] New Feature
- [ ] Bug Fix
- [ ] Refactor / Code Cleanup
- [ ] Documentation Update
- [ ] Other (please specify)
## Related Issue(s)
<!-- Link to any related issue(s) (e.g., #123) -->
## Screenshots / Video
<!-- Include screenshots or a short video demonstrating the change. If the change adds a new UI feature, attach an image. If it adds functionality best shown via video, embed a video. -->
**Screenshot** (if applicable):
```markdown
![Screenshot Description](path/to/screenshot.png)
```
**Video** (if applicable):
```html
<video src="path/to/video.mp4" controls width="600"></video>
```
## Testing
<!-- Describe how reviewers can test the changes. Include steps, commands, or environment setup. -->
## Checklist
- [ ] I have performed a self-review of my code.
- [ ] I have added any necessary screenshots or videos.
- [ ] I have linked related issue(s) and updated the changelog if applicable.
---
*Thank you for contributing!*
+253
View File
@@ -0,0 +1,253 @@
name: Build Electron App
on:
workflow_dispatch:
inputs:
arch:
description: 'Architecture to build'
required: true
default: 'both'
type: choice
options:
- arm64
- x64
- both
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build Windows app
run: npm run build:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Windows build
uses: actions/upload-artifact@v4
with:
name: windows-installer
path: release/**/*.exe
retention-days: 30
build-macos:
runs-on: macos-latest
strategy:
matrix:
arch: ${{ github.event.inputs.arch == 'both' && fromJSON('["arm64", "x64"]') || fromJSON(format('["{0}"]', github.event.inputs.arch)) }}
steps:
# ─── Checkout ─────────────────────────────────────────────
- name: Checkout code
uses: actions/checkout@v4
# ─── Setup Node.js ────────────────────────────────────────
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# ─── Setup Python (needed by some native deps) ────────────
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# ─── Install Dependencies ─────────────────────────────────
- name: Install dependencies
run: npm ci
# ─── Import Code Signing Certificate ──────────────────────
# This is the KEY step that makes CI signing work.
# We create a temporary keychain, import the .p12 cert into it,
# and set it as the default so codesign can find it.
- name: Import code signing certificate
env:
MAC_CERTIFICATE_P12: ${{ secrets.MAC_CERTIFICATE_P12 }}
MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
run: |
# Create a temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
# Create and configure keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Decode and import certificate
echo "$MAC_CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-k "$KEYCHAIN_PATH" \
-P "$MAC_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
# Allow codesign to access the keychain without UI prompt
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Add to keychain search path (makes it the default)
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
# Verify the identity is available
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
# Clean up the .p12 file
rm -f $RUNNER_TEMP/certificate.p12
# ─── Build Vite + Electron ────────────────────────────────
- name: Build Vite + Electron
run: npx tsc && npx vite build
# ─── Package with electron-builder ────────────────────────
# electron-builder handles deep codesigning the .app bundle
# "notarize: false" in electron-builder.json5 prevents it from
# trying its own notarization flow
- name: Package .app bundle
run: npx electron-builder --mac --${{ matrix.arch }} --dir
env:
CSC_NAME: "Samir Patil (N26FZ4GW28)"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ─── Read version from package.json ───────────────────────
- name: Get version
id: version
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
# ─── Locate the .app bundle ───────────────────────────────
- name: Find .app bundle
id: find_app
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "=== Release directory contents ==="
ls -laR "release/${VERSION}/" || echo "release/${VERSION}/ not found"
echo "=== Searching for .app bundle ==="
APP_BUNDLE=$(find "release/${VERSION}" -maxdepth 4 -name "*.app" -type d | head -n1)
if [ -z "$APP_BUNDLE" ]; then
echo "::error::No .app bundle found in release/${VERSION}/"
exit 1
fi
echo "app_bundle=$APP_BUNDLE" >> $GITHUB_OUTPUT
echo "Found: $APP_BUNDLE"
# ─── Verify .app signature ────────────────────────────────
- name: Verify .app code signature
run: codesign --verify --deep --strict "${{ steps.find_app.outputs.app_bundle }}"
# ─── Create DMG ───────────────────────────────────────────
- name: Create DMG
id: dmg
run: |
VERSION="${{ steps.version.outputs.version }}"
ARCH="${{ matrix.arch }}"
DMG_NAME="Openscreen-Mac-${ARCH}-${VERSION}.dmg"
RELEASE_DIR="release/${VERSION}"
DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}"
STAGING="${RELEASE_DIR}/dmg-staging"
mkdir -p "$STAGING"
cp -R "${{ steps.find_app.outputs.app_bundle }}" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
hdiutil create \
-srcfolder "$STAGING" \
-volname "Openscreen" \
-fs HFS+ \
-fsargs "-c c=64,a=16,e=16" \
-format UDBZ \
"$DMG_OUTPUT"
rm -rf "$STAGING"
echo "dmg_path=$DMG_OUTPUT" >> $GITHUB_OUTPUT
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
# ─── Sign DMG ─────────────────────────────────────────────
- name: Sign DMG
run: |
codesign --force \
--sign "Developer ID Application: Samir Patil (N26FZ4GW28)" \
--timestamp \
"${{ steps.dmg.outputs.dmg_path }}"
# ─── Notarize DMG ────────────────────────────────────────
# On CI we can't use keychain profiles for notarytool, so we
# pass credentials directly via env vars / flags
- name: Notarize DMG
run: |
xcrun notarytool submit "${{ steps.dmg.outputs.dmg_path }}" \
--apple-id "${{ secrets.APPLE_ID }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \
--wait
timeout-minutes: 15
# ─── Staple ───────────────────────────────────────────────
- name: Staple notarization ticket
run: xcrun stapler staple "${{ steps.dmg.outputs.dmg_path }}"
# ─── Validate ─────────────────────────────────────────────
- name: Validate stapled DMG
run: |
xcrun stapler validate "${{ steps.dmg.outputs.dmg_path }}"
spctl -a -vv -t install "${{ steps.dmg.outputs.dmg_path }}"
# ─── Upload Artifact ──────────────────────────────────────
- name: Upload notarized DMG
uses: actions/upload-artifact@v4
with:
name: openscreen-mac-${{ matrix.arch }}
path: ${{ steps.dmg.outputs.dmg_path }}
retention-days: 30
# ─── Cleanup Keychain ─────────────────────────────────────
- name: Cleanup keychain
if: always()
run: security delete-keychain $RUNNER_TEMP/build.keychain-db || true
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
- name: Install dependencies
run: npm ci
# bsdtar (from libarchive-tools) is required by fpm to build pacman
# packages. AppImage and deb don't need it; ubuntu-latest doesn't ship it.
- name: Install pacman build dependencies
run: sudo apt-get update && sudo apt-get install -y libarchive-tools
- name: Build Linux app
run: npm run build:linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Linux build
uses: actions/upload-artifact@v4
with:
name: linux-installer
path: |
release/**/*.AppImage
release/**/*.zsync
release/**/*.deb
release/**/*.pacman
retention-days: 30
+118
View File
@@ -0,0 +1,118 @@
name: Bump Nix package on release
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to bump (e.g. v1.5.0)"
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
steps:
- name: Resolve tag and version
id: meta
env:
GH_EVENT_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
TAG="${GH_EVENT_TAG:-$INPUT_TAG}"
if [[ -z "$TAG" ]]; then
echo "::error::No tag resolved from release event or workflow input"
exit 1
fi
VERSION="${TAG#v}"
BRANCH="chore/bump-nix-${VERSION}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
- name: Compute npmDepsHash
id: hash
run: |
set -euo pipefail
HASH=$(nix run nixpkgs#prefetch-npm-deps -- package-lock.json)
if [[ -z "$HASH" ]]; then
echo "::error::prefetch-npm-deps returned an empty hash"
exit 1
fi
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
echo "Computed npmDepsHash: $HASH"
- name: Update nix/package.nix
env:
VERSION: ${{ steps.meta.outputs.version }}
HASH: ${{ steps.hash.outputs.hash }}
run: |
set -euo pipefail
# Update version line: ` version = "<anything>";`
sed -i -E "s|^([[:space:]]*version[[:space:]]*=[[:space:]]*)\"[^\"]*\";|\1\"${VERSION}\";|" nix/package.nix
# Update npmDepsHash line: ` npmDepsHash = "<anything>";`
sed -i -E "s|^([[:space:]]*npmDepsHash[[:space:]]*=[[:space:]]*)\"[^\"]*\";|\1\"${HASH}\";|" nix/package.nix
echo "=== diff ==="
git --no-pager diff nix/package.nix || true
- name: Create PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.meta.outputs.version }}
HASH: ${{ steps.hash.outputs.hash }}
BRANCH: ${{ steps.meta.outputs.branch }}
TAG: ${{ steps.meta.outputs.tag }}
run: |
set -euo pipefail
if git diff --quiet -- nix/package.nix; then
echo "nix/package.nix already at v${VERSION} with this hash — nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Replace any prior bump branch to keep the workflow idempotent.
git push origin --delete "$BRANCH" 2>/dev/null || true
git checkout -b "$BRANCH"
git add nix/package.nix
git commit -m "chore: bump nix package to v${VERSION}"
git push -u origin "$BRANCH"
gh pr create \
--title "chore: bump nix package to v${VERSION}" \
--base main \
--head "$BRANCH" \
--body "$(cat <<EOF
Automated bump triggered by release \`${TAG}\`.
- \`version\` → \`${VERSION}\`
- \`npmDepsHash\` → \`${HASH}\` (computed via \`prefetch-npm-deps package-lock.json\`)
Merge this so Nix users (NixOS, Home Manager, \`nix run github:siddharthvaddem/openscreen\`) pick up the new release.
> Note: PRs opened by \`GITHUB_TOKEN\` don't auto-trigger CI. The diff is two lines — review the change here, then merge. If you want CI to run, push an empty commit to this branch or close-and-reopen the PR.
EOF
)"
+58
View File
@@ -0,0 +1,58 @@
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx tsc --noEmit
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test
- run: npm run test:browser:install
- run: npm run test:browser
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx vite build
+519
View File
@@ -0,0 +1,519 @@
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}`);
}
+26
View File
@@ -0,0 +1,26 @@
name: Publish release to WinGet
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish to winget (e.g. v1.4.0)"
required: true
type: string
jobs:
publish:
runs-on: windows-latest
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
steps:
- uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: SiddharthVaddem.OpenScreen
# Match the Windows installer asset attached to each release.
# Today: "Openscreen.Setup.latest.exe". Adjust this regex if you
# ever rename the installer to include a version (e.g. "Setup\.\d+\.\d+\.\d+\.exe").
installers-regex: 'Setup\..*\.exe$'
release-tag: ${{ inputs.tag || github.event.release.tag_name }}
token: ${{ secrets.WINGET_ACC_TOKEN }}
+168
View File
@@ -0,0 +1,168 @@
name: Update Homebrew Cask
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish to the tap (e.g. v1.4.0)"
required: true
type: string
permissions:
contents: read
jobs:
update-cask:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
env:
TAP_OWNER: siddharthvaddem
TAP_REPO: homebrew-openscreen
CASK_NAME: openscreen
steps:
- name: Resolve tag and version
id: meta
env:
GH_EVENT_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
TAG="${GH_EVENT_TAG:-$INPUT_TAG}"
if [[ -z "$TAG" ]]; then
echo "::error::No tag resolved from release event or workflow input"
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Find macOS DMG assets
id: assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
NAMES=$(gh release view "$TAG" --repo "$REPO" --json assets --jq '.assets[].name')
# arm64 DMG: explicit "arm64" / "apple silicon" / fallback to any .dmg
# whose name does NOT contain "x64" or non-mac platform markers.
ARM_NAME=$(echo "$NAMES" | grep -iE '\.dmg$' \
| grep -iE '(arm64|apple[-_. ]?silicon)' | head -n1 || true)
if [[ -z "$ARM_NAME" ]]; then
ARM_NAME=$(echo "$NAMES" | grep -iE '\.dmg$' \
| grep -iv 'x64' | grep -iv 'linux' | grep -iv 'win' | head -n1 || true)
fi
# x64 DMG
X64_NAME=$(echo "$NAMES" | grep -iE '\.dmg$' \
| grep -iE '(x64|x86[-_]?64|intel)' | head -n1 || true)
if [[ -z "$ARM_NAME" || -z "$X64_NAME" ]]; then
echo "::error::Could not locate both arm64 and x64 DMGs in release assets"
echo "Available assets:"
echo "$NAMES"
exit 1
fi
echo "arm_name=$ARM_NAME" >> "$GITHUB_OUTPUT"
echo "x64_name=$X64_NAME" >> "$GITHUB_OUTPUT"
echo "Found arm64 asset: $ARM_NAME"
echo "Found x64 asset: $X64_NAME"
- name: Download DMGs and compute sha256
id: shas
env:
REPO: ${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
ARM_NAME: ${{ steps.assets.outputs.arm_name }}
X64_NAME: ${{ steps.assets.outputs.x64_name }}
run: |
set -euo pipefail
BASE="https://github.com/${REPO}/releases/download/${TAG}"
curl -fsSL --retry 3 -o /tmp/arm.dmg "${BASE}/${ARM_NAME}"
curl -fsSL --retry 3 -o /tmp/x64.dmg "${BASE}/${X64_NAME}"
ARM_SHA=$(sha256sum /tmp/arm.dmg | awk '{print $1}')
X64_SHA=$(sha256sum /tmp/x64.dmg | awk '{print $1}')
echo "arm_sha=$ARM_SHA" >> "$GITHUB_OUTPUT"
echo "x64_sha=$X64_SHA" >> "$GITHUB_OUTPUT"
- name: Checkout tap
uses: actions/checkout@v4
with:
repository: ${{ env.TAP_OWNER }}/${{ env.TAP_REPO }}
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Write cask file
env:
REPO: ${{ github.repository }}
TAG: ${{ steps.meta.outputs.tag }}
VERSION: ${{ steps.meta.outputs.version }}
ARM_NAME: ${{ steps.assets.outputs.arm_name }}
X64_NAME: ${{ steps.assets.outputs.x64_name }}
ARM_SHA: ${{ steps.shas.outputs.arm_sha }}
X64_SHA: ${{ steps.shas.outputs.x64_sha }}
run: |
set -euo pipefail
mkdir -p tap/Casks
BASE="https://github.com/${REPO}/releases/download/${TAG}"
# #{version} is Ruby interpolation written literally to the cask
# file (bash heredoc leaves "#{...}" alone). \${VERSION}, \${ARM_SHA},
# etc. are bash variables expanded by the heredoc. The literal
# #{version} fixes Homebrew's "URL is unversioned" audit warning by
# making the version string statically detectable.
cat > "tap/Casks/${CASK_NAME}.rb" <<EOF
cask "${CASK_NAME}" do
version "${VERSION}"
on_arm do
sha256 "${ARM_SHA}"
url "https://github.com/${REPO}/releases/download/v#{version}/${ARM_NAME}"
end
on_intel do
sha256 "${X64_SHA}"
url "https://github.com/${REPO}/releases/download/v#{version}/${X64_NAME}"
end
name "Openscreen"
desc "Screen recorder and video editor"
homepage "https://github.com/${REPO}"
auto_updates false
depends_on macos: ">= :big_sur"
app "Openscreen.app"
zap trash: [
"~/Library/Application Support/Openscreen",
"~/Library/Caches/com.siddharthvaddem.openscreen",
"~/Library/Logs/Openscreen",
"~/Library/Preferences/com.siddharthvaddem.openscreen.plist",
"~/Library/Saved Application State/com.siddharthvaddem.openscreen.savedState",
]
end
EOF
- name: Commit and push to tap
working-directory: tap
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add "Casks/${CASK_NAME}.rb"
if git diff --cached --quiet; then
echo "Cask already up to date for ${VERSION} — nothing to commit."
exit 0
fi
git commit -m "Bump ${CASK_NAME} to ${VERSION}"
git push