Publish OpenScreen 1.4.11 update assets
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
)"
|
||||
@@ -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
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user