diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index cfee36d65..2a3000ef8 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @siddharthvaddem
+* @EtienneLescot
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 79f39d4de..9d8ea49e1 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,43 +1,36 @@
-# Pull Request Template
-
-## Description
-
-
-## Motivation
-
-
-## Type of Change
-- [ ] New Feature
-- [ ] Bug Fix
-- [ ] Refactor / Code Cleanup
-- [ ] Documentation Update
-- [ ] Other (please specify)
-
-## Related Issue(s)
-
-
-## Screenshots / Video
-
-
-**Screenshot** (if applicable):
-
-```markdown
-
-```
-
-**Video** (if applicable):
-
-```html
-
-```
+## Summary
+
+
+## Related issue
+
+
+
+Fixes #
+
+## Type of change
+- [ ] Bug fix
+- [ ] Feature
+- [ ] Enhancement
+- [ ] Documentation
+- [ ] Refactor / maintenance
+- [ ] Performance
+- [ ] Security
+
+## Release impact
+- [ ] Patch
+- [ ] Minor
+- [ ] Major / breaking change
+- [ ] No release note needed
+
+## Desktop impact
+- [ ] Windows
+- [ ] macOS
+- [ ] Linux
+- [ ] Installer / packaging
+- [ ] Not platform-specific
+
+## Screenshots / video
+
## Testing
-
-
-## 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!*
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 73029f6bc..44a73e2ac 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,36 +1,49 @@
-
name: Build Electron App
on:
+ push:
+ tags:
+ - "v*"
workflow_dispatch:
inputs:
arch:
- description: 'Architecture to build'
+ description: "macOS architecture to build"
required: true
- default: 'both'
+ default: "both"
type: choice
options:
- arm64
- x64
- both
+ release_tag:
+ description: "Optional release tag to create or update, e.g. v1.5.0"
+ required: false
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: build-${{ github.ref_name }}-${{ github.event.inputs.release_tag || 'artifacts' }}
+ cancel-in-progress: false
jobs:
build-windows:
+ name: Windows installer
runs-on: windows-latest
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
- node-version: '22'
+ node-version: 22
+ cache: npm
- name: Install dependencies
run: npm ci
- # Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
- # (and to avoid 429s when the platform matrix builds hit it at once).
- name: Cache caption assets
uses: actions/cache@v4
with:
@@ -38,151 +51,129 @@ jobs:
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
- name: Build Windows app
- run: npm run build:win
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Upload Windows build
+ run: npm run build:win -- --publish never
+
+ - name: Upload Windows installer
uses: actions/upload-artifact@v4
with:
- name: windows-installer
- path: release/**/*.exe
+ name: openscreen-windows
+ path: release/**/Openscreen.Setup.*.exe
+ if-no-files-found: error
retention-days: 30
build-macos:
+ name: macOS ${{ matrix.arch }} DMG
runs-on: macos-latest
strategy:
+ fail-fast: false
matrix:
- arch: ${{ github.event.inputs.arch == 'both' && fromJSON('["arm64", "x64"]') || fromJSON(format('["{0}"]', github.event.inputs.arch)) }}
-
+ arch: ${{ fromJSON((github.event_name == 'workflow_dispatch' && github.event.inputs.arch != 'both') && format('["{0}"]', github.event.inputs.arch) || '["arm64", "x64"]') }}
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'
+ python-version: "3.11"
- # ─── Install Dependencies ─────────────────────────────────
- name: Install dependencies
run: npm ci
- # ─── Ensure sharp prebuilt binary (+ bundled libvips) ─────
- # `npm ci` can leave sharp without its prebuilt binary/vendored libvips,
- # which makes electron-builder's native rebuild fail ("vips-cpp.42 not
- # found"). Re-fetch the prebuilt (never compile from source).
- name: Ensure sharp prebuilt
run: npm rebuild sharp
env:
npm_config_build_from_source: "false"
- # ─── Cache caption assets ─────────────────────────────────
- # Avoid re-fetching the Whisper model from HuggingFace every run (and 429s under the matrix).
- name: Cache caption assets
uses: actions/cache@v4
with:
path: caption-assets
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
- # ─── 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: Resolve macOS signing
+ id: signing
+ env:
+ MAC_CERTIFICATE_P12: ${{ secrets.MAC_CERTIFICATE_P12 }}
+ MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
+ MAC_CSC_NAME: ${{ secrets.MAC_CSC_NAME }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ run: |
+ if [[ -n "$MAC_CERTIFICATE_P12" && -n "$MAC_CERTIFICATE_PASSWORD" && -n "$MAC_CSC_NAME" && -n "$APPLE_ID" && -n "$APPLE_TEAM_ID" && -n "$APPLE_APP_SPECIFIC_PASSWORD" ]]; then
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
+ fi
+
- name: Import code signing certificate
+ if: steps.signing.outputs.enabled == 'true'
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)
+ 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 \
+ 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"
+ rm -f "$RUNNER_TEMP/certificate.p12"
- # Clean up the .p12 file
- rm -f $RUNNER_TEMP/certificate.p12
-
- # ─── Build Vite + Electron ────────────────────────────────
- name: Build Vite + Electron
run: npx tsc && npx vite build
- # ─── Build native macOS helpers ───────────────────────────
- # The package step below calls electron-builder directly (not `npm run build:mac`),
- # so the Swift screencapturekit + cursor helpers must be compiled here first or the
- # packaged app ships without them (no recording, "cursor helper couldn't be found").
- # Build the matrix arch into electron/native/bin/darwin- so each DMG gets its
- # own native binary.
- name: Build native macOS helpers
run: npm run build:native:mac
env:
OPENSCREEN_MAC_HELPER_ARCHS: ${{ matrix.arch }}
- # ─── 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
+ run: npx electron-builder --mac --${{ matrix.arch }} --dir --publish never
env:
- CSC_NAME: "Samir Patil (N26FZ4GW28)"
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CSC_NAME: ${{ secrets.MAC_CSC_NAME }}
+ CSC_IDENTITY_AUTO_DISCOVERY: ${{ steps.signing.outputs.enabled == 'true' && 'true' || 'false' }}
- # ─── Read version from package.json ───────────────────────
- name: Get version
id: version
- run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
+ run: |
+ VERSION="$(node -e "console.log(require('./package.json').version)")"
+ echo "version=$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
+ 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}/"
+ find "release/${VERSION}" -maxdepth 4 -print || true
exit 1
fi
- echo "app_bundle=$APP_BUNDLE" >> $GITHUB_OUTPUT
- echo "Found: $APP_BUNDLE"
+ echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT"
- # ─── Verify .app signature ────────────────────────────────
- name: Verify .app code signature
+ if: steps.signing.outputs.enabled == 'true'
run: codesign --verify --deep --strict "${{ steps.find_app.outputs.app_bundle }}"
- # ─── Create DMG ───────────────────────────────────────────
- name: Create DMG
id: dmg
run: |
@@ -193,6 +184,8 @@ jobs:
DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}"
STAGING="${RELEASE_DIR}/dmg-staging"
+ rm -rf "$STAGING"
+ rm -f "$DMG_OUTPUT"
mkdir -p "$STAGING"
cp -R "${{ steps.find_app.outputs.app_bundle }}" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
@@ -206,22 +199,18 @@ jobs:
"$DMG_OUTPUT"
rm -rf "$STAGING"
+ echo "dmg_path=$DMG_OUTPUT" >> "$GITHUB_OUTPUT"
- echo "dmg_path=$DMG_OUTPUT" >> $GITHUB_OUTPUT
- echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
-
- # ─── Sign DMG ─────────────────────────────────────────────
- name: Sign DMG
+ if: steps.signing.outputs.enabled == 'true'
run: |
codesign --force \
- --sign "Developer ID Application: Samir Patil (N26FZ4GW28)" \
+ --sign "${{ secrets.MAC_CSC_NAME }}" \
--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
+ if: steps.signing.outputs.enabled == 'true'
run: |
xcrun notarytool submit "${{ steps.dmg.outputs.dmg_path }}" \
--apple-id "${{ secrets.APPLE_ID }}" \
@@ -230,50 +219,47 @@ jobs:
--wait
timeout-minutes: 15
- # ─── Staple ───────────────────────────────────────────────
- name: Staple notarization ticket
+ if: steps.signing.outputs.enabled == 'true'
run: xcrun stapler staple "${{ steps.dmg.outputs.dmg_path }}"
- # ─── Validate ─────────────────────────────────────────────
- name: Validate stapled DMG
+ if: steps.signing.outputs.enabled == 'true'
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
+ - name: Upload macOS DMG
uses: actions/upload-artifact@v4
with:
name: openscreen-mac-${{ matrix.arch }}
path: ${{ steps.dmg.outputs.dmg_path }}
+ if-no-files-found: error
retention-days: 30
- # ─── Cleanup Keychain ─────────────────────────────────────
- name: Cleanup keychain
- if: always()
- run: security delete-keychain $RUNNER_TEMP/build.keychain-db || true
+ if: always() && steps.signing.outputs.enabled == 'true'
+ run: security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true
build-linux:
+ name: Linux packages
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
- node-version: '22'
+ node-version: 22
+ cache: npm
- 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
- # Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
- # (and to avoid 429s when the platform matrix builds hit it at once).
- name: Cache caption assets
uses: actions/cache@v4
with:
@@ -281,17 +267,84 @@ jobs:
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}
- name: Build Linux app
- run: npm run build:linux
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: npm run build:linux -- --publish never
- - name: Upload Linux build
+ - name: Upload Linux packages
uses: actions/upload-artifact@v4
with:
- name: linux-installer
+ name: openscreen-linux
path: |
release/**/*.AppImage
release/**/*.zsync
release/**/*.deb
release/**/*.pacman
+ if-no-files-found: error
retention-days: 30
+
+ publish-release:
+ name: Publish GitHub release
+ runs-on: ubuntu-latest
+ needs:
+ - build-windows
+ - build-macos
+ - build-linux
+ if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Resolve release tag
+ id: release
+ env:
+ INPUT_TAG: ${{ github.event.inputs.release_tag }}
+ run: |
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
+ TAG="${GITHUB_REF_NAME}"
+ else
+ TAG="${INPUT_TAG}"
+ fi
+
+ if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "::error::Release tag must look like v1.5.0; got '${TAG}'"
+ exit 1
+ fi
+
+ VERSION="${TAG#v}"
+ PACKAGE_VERSION="$(node -p 'require("./package.json").version')"
+ if [[ "$PACKAGE_VERSION" != "$VERSION" ]]; then
+ echo "::error::package.json version ${PACKAGE_VERSION} does not match release tag ${TAG}"
+ exit 1
+ fi
+
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - name: Download installers
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Publish release assets
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ steps.release.outputs.tag }}
+ run: |
+ mapfile -t FILES < <(find artifacts -type f | sort)
+ if [[ "${#FILES[@]}" -eq 0 ]]; then
+ echo "::error::No installer artifacts were downloaded"
+ exit 1
+ fi
+
+ if gh release view "$TAG" >/dev/null 2>&1; then
+ gh release upload "$TAG" "${FILES[@]}" --clobber
+ else
+ gh release create "$TAG" "${FILES[@]}" \
+ --target "$GITHUB_SHA" \
+ --title "$TAG" \
+ --generate-notes
+ fi
+
+ gh release edit "$TAG" \
+ --draft=false \
+ --latest \
+ --title "$TAG"
diff --git a/.github/workflows/bump-nix-package.yml b/.github/workflows/bump-nix-package.yml
index 5ff3c73e6..57e48e51a 100644
--- a/.github/workflows/bump-nix-package.yml
+++ b/.github/workflows/bump-nix-package.yml
@@ -111,7 +111,7 @@ jobs:
- \`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.
+ Merge this so Nix users (NixOS, Home Manager, \`nix run github:${{ github.repository }}\`) 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
diff --git a/.github/workflows/discord.yaml b/.github/workflows/discord.yaml
index 6da25d0d6..950abae3d 100644
--- a/.github/workflows/discord.yaml
+++ b/.github/workflows/discord.yaml
@@ -26,6 +26,8 @@ jobs:
steps:
- name: Sync PR activity to Discord forum thread
id: sync
+ # Discord sync is helpful automation, but it should not block PR validation.
+ continue-on-error: true
uses: actions/github-script@v7
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
@@ -210,15 +212,10 @@ jobs:
}
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);
- }
+ core.warning(msg);
return;
}
@@ -421,7 +418,7 @@ jobs:
core.info(`Posted update to Discord thread ${threadId}.`);
} catch (err) {
const msg = err && err.message ? err.message : String(err);
- core.setFailed(msg);
+ core.warning(`Discord sync failed, but this optional automation will not block PR validation: ${msg}`);
const alertWebhook = process.env.DISCORD_ALERT_WEBHOOK_URL;
if (alertWebhook) {
diff --git a/.github/workflows/merged-pr-bookkeeping.yml b/.github/workflows/merged-pr-bookkeeping.yml
new file mode 100644
index 000000000..8b0671325
--- /dev/null
+++ b/.github/workflows/merged-pr-bookkeeping.yml
@@ -0,0 +1,252 @@
+name: Merged PR issue bookkeeping
+
+on:
+ pull_request_target:
+ types: [closed]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: read
+
+jobs:
+ mark-linked-issues-fixed:
+ name: Mark linked issues fixed in main
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Update closing issues
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const pullRequest = context.payload.pull_request;
+ const pullNumber = pullRequest.number;
+
+ const fixedInMainLabel = {
+ name: "status: fixed in main",
+ color: "0E8A16",
+ description: "Work is merged into main but may not be in a downloadable release yet.",
+ };
+ const pendingReleaseLabel = {
+ name: "status: pending release",
+ color: "FBCA04",
+ description: "Merged change is waiting for a packaged desktop release.",
+ };
+ const labelsToRemove = ["status: in progress", "status: needs triage"];
+ const nextReleaseMilestoneTitle = "Next Release";
+
+ async function ensureLabel(label) {
+ try {
+ await github.rest.issues.getLabel({
+ owner,
+ repo,
+ name: label.name,
+ });
+ } catch (error) {
+ if (error.status !== 404) throw error;
+ try {
+ await github.rest.issues.createLabel({
+ owner,
+ repo,
+ name: label.name,
+ color: label.color,
+ description: label.description,
+ });
+ core.info(`Created label '${label.name}'.`);
+ } catch (createError) {
+ if (createError.status !== 422) throw createError;
+ core.info(`Label '${label.name}' already exists.`);
+ }
+ }
+ }
+
+ async function ensureMilestone(title) {
+ const milestones = await github.paginate(github.rest.issues.listMilestones, {
+ owner,
+ repo,
+ state: "all",
+ per_page: 100,
+ });
+ const existing = milestones.find((milestone) => milestone.title === title);
+ if (existing?.state === "open") return existing;
+ if (existing) {
+ const reopened = await github.rest.issues.updateMilestone({
+ owner,
+ repo,
+ milestone_number: existing.number,
+ state: "open",
+ });
+ core.info(`Reopened milestone '${title}'.`);
+ return reopened.data;
+ }
+
+ try {
+ const created = await github.rest.issues.createMilestone({
+ owner,
+ repo,
+ title,
+ description: "Merged changes queued for the next packaged desktop release.",
+ });
+ core.info(`Created milestone '${title}'.`);
+ return created.data;
+ } catch (error) {
+ if (error.status !== 422) throw error;
+ const refreshed = await github.paginate(github.rest.issues.listMilestones, {
+ owner,
+ repo,
+ state: "all",
+ per_page: 100,
+ });
+ const milestone = refreshed.find((item) => item.title === title);
+ if (!milestone) throw error;
+ return milestone;
+ }
+ }
+
+ async function getClosingIssueRefs() {
+ const query = `
+ query($owner: String!, $repo: String!, $pullNumber: Int!, $cursor: String) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $pullNumber) {
+ closingIssuesReferences(first: 100, after: $cursor) {
+ nodes {
+ number
+ repository {
+ name
+ owner {
+ login
+ }
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+ }
+ }
+ }
+ `;
+
+ const issueRefs = new Map();
+ let cursor = null;
+ let hasNextPage = true;
+
+ while (hasNextPage) {
+ const result = await github.graphql(query, {
+ owner,
+ repo,
+ pullNumber,
+ cursor,
+ });
+ const refs = result.repository.pullRequest.closingIssuesReferences;
+ for (const issue of refs.nodes) {
+ const issueOwner = issue.repository.owner.login;
+ const issueRepo = issue.repository.name;
+ issueRefs.set(`${issueOwner}/${issueRepo}#${issue.number}`, {
+ owner: issueOwner,
+ repo: issueRepo,
+ number: issue.number,
+ });
+ }
+ hasNextPage = refs.pageInfo.hasNextPage;
+ cursor = refs.pageInfo.endCursor;
+ }
+
+ return [...issueRefs.values()];
+ }
+
+ async function hasBookkeepingComment(issueNumber, marker) {
+ const comments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: issueNumber,
+ per_page: 100,
+ });
+ return comments.some((comment) => comment.body && comment.body.includes(marker));
+ }
+
+ await ensureLabel(fixedInMainLabel);
+ await ensureLabel(pendingReleaseLabel);
+ const fallbackMilestone = pullRequest.milestone || await ensureMilestone(nextReleaseMilestoneTitle);
+
+ const issueRefs = await getClosingIssueRefs();
+ if (issueRefs.length === 0) {
+ core.info(`PR #${pullNumber} did not declare closing issue references. Nothing to update.`);
+ return;
+ }
+
+ for (const issueRef of issueRefs) {
+ if (
+ issueRef.owner.toLowerCase() !== owner.toLowerCase() ||
+ issueRef.repo.toLowerCase() !== repo.toLowerCase()
+ ) {
+ core.warning(
+ `Skipping cross-repository closing reference ${issueRef.owner}/${issueRef.repo}#${issueRef.number}; ` +
+ `this workflow only updates issues in ${owner}/${repo}.`,
+ );
+ continue;
+ }
+
+ const issueNumber = issueRef.number;
+ const issueResponse = await github.rest.issues.get({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ });
+ const issue = issueResponse.data;
+ const existingLabels = issue.labels.map((label) =>
+ typeof label === "string" ? label : label.name,
+ );
+ const milestoneTitle = issue.milestone?.title || fallbackMilestone.title;
+ const milestoneNumber = issue.milestone?.number || fallbackMilestone.number;
+
+ await github.rest.issues.addLabels({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ labels: [fixedInMainLabel.name, pendingReleaseLabel.name],
+ });
+
+ for (const label of labelsToRemove) {
+ if (!existingLabels.includes(label)) continue;
+ try {
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ name: label,
+ });
+ } catch (error) {
+ if (error.status !== 404) throw error;
+ }
+ }
+
+ await github.rest.issues.update({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ milestone: milestoneNumber,
+ state: "closed",
+ state_reason: "completed",
+ });
+
+ const marker = ``;
+ if (!(await hasBookkeepingComment(issueNumber, marker))) {
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: [
+ marker,
+ `Fixed by #${pullNumber} and merged into \`main\`.`,
+ "",
+ `This change is assigned to the \`${milestoneTitle}\` release milestone and is not necessarily available in the latest downloadable desktop release yet. It is currently marked as \`${pendingReleaseLabel.name}\` until a packaged release containing it is published.`,
+ ].join("\n"),
+ });
+ }
+
+ core.info(`Updated issue #${issueNumber} for merged PR #${pullNumber}.`);
+ }
diff --git a/.github/workflows/publish-winget.yml b/.github/workflows/publish-winget.yml
index 62b4b7adf..12fd0404a 100644
--- a/.github/workflows/publish-winget.yml
+++ b/.github/workflows/publish-winget.yml
@@ -2,7 +2,7 @@ name: Publish release to WinGet
on:
release:
- types: [released]
+ types: [published]
workflow_dispatch:
inputs:
tag:
@@ -13,14 +13,13 @@ on:
jobs:
publish:
runs-on: windows-latest
- if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
+ if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.WINGET_IDENTIFIER != ''
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").
+ identifier: ${{ vars.WINGET_IDENTIFIER }}
+ # Matches the Windows installer asset attached to each release,
+ # e.g. "Openscreen.Setup.1.5.0.exe".
installers-regex: 'Setup\..*\.exe$'
release-tag: ${{ inputs.tag || github.event.release.tag_name }}
token: ${{ secrets.WINGET_ACC_TOKEN }}
diff --git a/.github/workflows/update-homebrew-cask.yml b/.github/workflows/update-homebrew-cask.yml
index 3d65cb0aa..030b893bd 100644
--- a/.github/workflows/update-homebrew-cask.yml
+++ b/.github/workflows/update-homebrew-cask.yml
@@ -16,11 +16,11 @@ permissions:
jobs:
update-cask:
runs-on: ubuntu-latest
- if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
+ if: (github.event_name == 'workflow_dispatch' || !github.event.release.prerelease) && vars.HOMEBREW_TAP_OWNER != '' && vars.HOMEBREW_TAP_REPO != ''
env:
- TAP_OWNER: siddharthvaddem
- TAP_REPO: homebrew-openscreen
- CASK_NAME: openscreen
+ TAP_OWNER: ${{ vars.HOMEBREW_TAP_OWNER }}
+ TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }}
+ CASK_NAME: ${{ vars.HOMEBREW_CASK_NAME || 'openscreen' }}
steps:
- name: Resolve tag and version
id: meta
@@ -143,10 +143,10 @@ jobs:
zap trash: [
"~/Library/Application Support/Openscreen",
- "~/Library/Caches/com.siddharthvaddem.openscreen",
+ "~/Library/Caches/com.etiennelescot.openscreen",
"~/Library/Logs/Openscreen",
- "~/Library/Preferences/com.siddharthvaddem.openscreen.plist",
- "~/Library/Saved Application State/com.siddharthvaddem.openscreen.savedState",
+ "~/Library/Preferences/com.etiennelescot.openscreen.plist",
+ "~/Library/Saved Application State/com.etiennelescot.openscreen.savedState",
]
end
EOF
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 651c80f3d..d4d82f7b5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,7 +43,33 @@ Thank you for considering contributing to this project! By contributing, you hel
## Reporting Issues
-If you encounter a bug or have a feature request, please open an issue in the [Issues](https://github.com/siddharthvaddem/openscreen/issues) section of this repository. Provide as much detail as possible to help us address the issue effectively.
+If you encounter a bug or have a feature request, please open an issue in the [Issues](https://github.com/EtienneLescot/openscreen/issues) section of this repository. Provide as much detail as possible to help us address the issue effectively.
+
+## Issue lifecycle
+
+Issues are closed when the corresponding fix or feature is merged into `main`.
+
+For desktop users, this does not always mean the change is already available in the latest downloadable release. When relevant, closed issues are marked as `status: fixed in main` and `status: pending release`.
+
+Once a GitHub Release containing the change is published, the issue can be marked as `status: released`.
+
+The next version number is not always known when a PR is merged. In that case, issues are assigned to the `Next Release` milestone. When preparing a release, this milestone can be renamed to the actual version, such as `v1.6.0` or `v2.0.0`, and a new `Next Release` milestone can be created.
+
+When a PR fully resolves an issue, link it with a GitHub closing keyword:
+
+```txt
+Fixes #123
+Closes #123
+Resolves #123
+```
+
+If a PR only partially addresses an issue, use a non-closing reference instead:
+
+```txt
+Refs #123
+Part of #123
+Related to #123
+```
## Style Guide
@@ -54,4 +80,4 @@ If you encounter a bug or have a feature request, please open an issue in the [I
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](./LICENSE).
-Thank you for your contributions!
\ No newline at end of file
+Thank you for your contributions!
diff --git a/README.md b/README.md
index 867f21634..13ef2abb7 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,31 @@
-> [!WARNING]
-> This started as a side project that blew up; not production grade and you'll hit bugs, but hopefully it covers what you need. **This project will soon be archived.**
+> [!NOTE]
+> This repository is an independent continuation of OpenScreen.
+>
+> OpenScreen was originally created by [Siddharth Vaddem](https://github.com/siddharthvaddem). The original repository was archived after v1.5.0 and remains available here: [siddharthvaddem/openscreen](https://github.com/siddharthvaddem/openscreen).
+>
+> This fork continues development under the OpenScreen name with the original author's approval, while remaining fully MIT open source.
+> [!WARNING]
+> OpenScreen is not production-grade software. You should expect bugs, rough edges, and occasional breaking changes.
-
-
-
-
-
-
#
OpenScreen
-
OpenScreen is your free, open-source alternative to Screen Studio.
+
OpenScreen is a free, open-source tool for creating polished screen recordings, product demos, and walkthroughs.
+
+OpenScreen was originally positioned as a free, open-source alternative to Screen Studio: something you can use to create quick, polished product demos and walkthroughs for X, Reddit, YouTube, documentation, landing pages, or internal demos.
-If you don't want to pay $29/month for Screen Studio but want a version that does what most people seem to need - quick, polished product demos and walkthroughs you'd post on X, Reddit or Youtube. OpenScreen does not offer every Screen Studio feature, but covers a lot of the core functionality.
+It is not a 1:1 clone of Screen Studio. Screen Studio is an excellent commercial product. OpenScreen focuses on covering the core open-source workflow: recording, zooms, cursor effects, webcam overlay, captions, editing, annotations, and export.
-Screen Studio is an awesome product and this is definitely not a 1:1 clone. If you just want something fully free and open source, this project should cover most of your needs.
+The goal of this continuation is to keep OpenScreen alive as a fully open-source project and progressively evolve it toward a broader recording and editing workflow.
-**100% free** for both **personal** and **commercial** use. Use it, modify it, distribute it. Please respect the License.
+**100% free** for both **personal** and **commercial** use. Use it, modify it, distribute it. Please respect the license.
> [!NOTE]
->Software should be accessible. OpenScreen has no paid tiers, premium features, upsells, or functionality locked behind a paywall.
+> Software should be accessible. OpenScreen has no paid tiers, premium features, upsells, or functionality locked behind a paywall.