Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ jobs:
# Tolerate only the expected case: it was never enabled. Swallowing
# permission, API or network failures would leave auto-merge on for a
# PR that is now a draft or points somewhere other than develop.
if printf '%s' "$err" | grep -qi 'not enabled'; then
# GitHub's wording is not stable: older payloads said "not enabled";
# current ones say "Can't disable auto-merge for this pull request."
if printf '%s' "$err" | grep -qiE 'not enabled|can.t disable auto-merge'; then
echo "Auto-merge was not enabled; nothing to disable."
exit 0
fi
Expand Down
124 changes: 114 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
name: CI

# Triggers (see docs/ci.md):
# - pull_request into develop or main — this is where required checks run
# - push to main — post-promotion record; Release does not wait on this workflow
# Push to develop is omitted so a squash-merge does not pay the suite twice.
on:
push:
branches: [main, develop]
branches: [main]
pull_request:
branches: [main, develop]

Expand All @@ -14,8 +18,46 @@ env:
NODE_VERSION: '22'

jobs:
# Release back-merges exist for ancestry (docs/RELEASE.md), not product
# changes. Skip the heavy suite but keep required check names green:
# lint, test, typecheck, e2e, security. Trusted PRs only: github-actions[bot]
# into develop. Branch prefix matches automerge.yml; title covers
# github-actions sync-develop PRs. Do not trust the prefix alone.
ci-gate:
runs-on: ubuntu-latest
outputs:
skip_heavy: ${{ steps.detect.outputs.skip_heavy }}
steps:
- id: detect
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_USER: ${{ github.event.pull_request.user.login }}
run: |
skip=false
if [ "$EVENT_NAME" = "pull_request" ] && \
[ "$PR_USER" = "github-actions[bot]" ] && \
[ "$BASE_REF" = "develop" ]; then
case "$HEAD_REF" in
chore/backmerge-*) skip=true ;;
esac
if [ "$PR_TITLE" = "chore(release): merge main into develop" ]; then
skip=true
fi
fi
echo "skip_heavy=${skip}" >> "$GITHUB_OUTPUT"
if [ "$skip" = "true" ]; then
echo "Release back-merge — heavy CI will report success without install/test/e2e."
else
echo "Full CI."
fi

# ── Shared install + cache ─────────────────────────
setup:
needs: ci-gate
if: needs.ci-gate.outputs.skip_heavy != 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down Expand Up @@ -54,16 +96,27 @@ jobs:

# ── Tier 1: Lint + Format ──────────────────────────
lint:
needs: setup
needs: [ci-gate, setup]
# always() is required so a skipped setup (back-merge) does not skip
# this job. needs.setup.result == 'success' still blocks a failed setup.
if: ${{ always() && !cancelled() && needs.ci-gate.result == 'success' && (needs.ci-gate.outputs.skip_heavy == 'true' || needs.setup.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Skip heavy CI for release back-merge
if: needs.ci-gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check lint reports success without the suite."

- uses: actions/checkout@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: pnpm/action-setup@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: actions/setup-node@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
with:
node-version: ${{ env.NODE_VERSION }}

- name: Restore node_modules
if: needs.ci-gate.outputs.skip_heavy != 'true'
uses: actions/cache/restore@v5
with:
path: |
Expand All @@ -73,9 +126,11 @@ jobs:
key: modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: ESLint
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm lint

- name: Prettier
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm format:check

# PR title commitlint moved to .github/workflows/pr-title.yml so it
Expand All @@ -84,16 +139,25 @@ jobs:

# ── Tier 1: Tests + Coverage ───────────────────────
test:
needs: setup
needs: [ci-gate, setup]
if: ${{ always() && !cancelled() && needs.ci-gate.result == 'success' && (needs.ci-gate.outputs.skip_heavy == 'true' || needs.setup.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Skip heavy CI for release back-merge
if: needs.ci-gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check test reports success without the suite."

- uses: actions/checkout@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: pnpm/action-setup@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: actions/setup-node@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
with:
node-version: ${{ env.NODE_VERSION }}

- name: Restore node_modules
if: needs.ci-gate.outputs.skip_heavy != 'true'
uses: actions/cache/restore@v5
with:
path: |
Expand All @@ -103,13 +167,15 @@ jobs:
key: modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Run tests
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm test

- name: Build packages
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm build

- name: Upload coverage
if: always()
if: ${{ always() && needs.ci-gate.outputs.skip_heavy != 'true' }}
uses: actions/upload-artifact@v5
with:
name: coverage-report
Expand All @@ -120,16 +186,25 @@ jobs:

# ── Tier 1: Typecheck ──────────────────────────────
typecheck:
needs: setup
needs: [ci-gate, setup]
if: ${{ always() && !cancelled() && needs.ci-gate.result == 'success' && (needs.ci-gate.outputs.skip_heavy == 'true' || needs.setup.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Skip heavy CI for release back-merge
if: needs.ci-gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check typecheck reports success without the suite."

- uses: actions/checkout@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: pnpm/action-setup@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: actions/setup-node@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
with:
node-version: ${{ env.NODE_VERSION }}

- name: Restore node_modules
if: needs.ci-gate.outputs.skip_heavy != 'true'
uses: actions/cache/restore@v5
with:
path: |
Expand All @@ -139,12 +214,15 @@ jobs:
key: modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Build packages first
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm build

- name: Typecheck all packages
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm -r typecheck

- name: Typecheck desktop
if: needs.ci-gate.outputs.skip_heavy != 'true'
working-directory: apps/desktop
run: pnpm typecheck

Expand All @@ -154,12 +232,20 @@ jobs:
# scripts + the two explicit materialize steps is what makes electron.launch
# work. This job is required (no continue-on-error).
e2e:
needs: setup
needs: [ci-gate, setup]
if: ${{ always() && !cancelled() && needs.ci-gate.result == 'success' && (needs.ci-gate.outputs.skip_heavy == 'true' || needs.setup.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Skip heavy CI for release back-merge
if: needs.ci-gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check e2e reports success without Playwright."

- uses: actions/checkout@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: pnpm/action-setup@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: actions/setup-node@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
Expand All @@ -170,33 +256,40 @@ jobs:
# (publickey)" — that is why Dependabot PRs fail e2e while develop
# (HTTPS tarball in the lockfile) stays green. See #544.
- name: Force HTTPS for GitHub git dependencies
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: git config --global 'url.https://github.com/.insteadOf' 'git@github.com:'

- name: Install dependencies (with postinstall scripts)
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm install --frozen-lockfile

- name: Materialize Electron binary
if: needs.ci-gate.outputs.skip_heavy != 'true'
working-directory: apps/desktop
run: node node_modules/electron/install.js

- name: Rebuild native modules for Electron
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm --filter @dripnex/desktop exec electron-builder install-app-deps

- name: Install Playwright system deps
if: needs.ci-gate.outputs.skip_heavy != 'true'
working-directory: apps/desktop
run: npx playwright install-deps chromium

- name: Build desktop bundle
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm --filter @dripnex/desktop build

- name: Run Playwright E2E (xvfb)
if: needs.ci-gate.outputs.skip_heavy != 'true'
working-directory: apps/desktop
run: xvfb-run --auto-servernum pnpm e2e
env:
CI: 'true'

- name: Upload Playwright report on failure
if: failure()
if: ${{ failure() && needs.ci-gate.outputs.skip_heavy != 'true' }}
uses: actions/upload-artifact@v5
with:
name: playwright-report
Expand All @@ -205,16 +298,25 @@ jobs:

# ── Tier 3: Security audit ─────────────────────────
security:
needs: setup
needs: [ci-gate, setup]
if: ${{ always() && !cancelled() && needs.ci-gate.result == 'success' && (needs.ci-gate.outputs.skip_heavy == 'true' || needs.setup.result == 'success') }}
runs-on: ubuntu-latest
steps:
- name: Skip heavy CI for release back-merge
if: needs.ci-gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check security reports success without the audit."

- uses: actions/checkout@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: pnpm/action-setup@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
- uses: actions/setup-node@v5
if: needs.ci-gate.outputs.skip_heavy != 'true'
with:
node-version: ${{ env.NODE_VERSION }}

- name: Restore node_modules
if: needs.ci-gate.outputs.skip_heavy != 'true'
uses: actions/cache/restore@v5
with:
path: |
Expand All @@ -224,17 +326,19 @@ jobs:
key: modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Audit dependencies
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: pnpm audit --prod --audit-level=high || true

- name: Check licenses
if: needs.ci-gate.outputs.skip_heavy != 'true'
run: |
npx license-checker --production --failOn "GPL-3.0;AGPL-3.0;SSPL-1.0" --summary || true

# ── Tier 2: Bundle size tracking ───────────────────
bundle-size:
needs: setup
needs: [ci-gate, setup]
if: github.event_name == 'pull_request' && needs.ci-gate.outputs.skip_heavy != 'true'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v5
Expand Down
52 changes: 45 additions & 7 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
name: CodeQL

# Single in-repo CodeQL source (advanced setup). GitHub default setup
# (Actions path dynamic/github-code-scanning/codeql, shown as
# "Code Quality: PR #N") must stay disabled — see docs/ci.md.
#
# Job name is "CodeQL" so the develop ruleset context of the same name
# still reports on release back-merges when the scan itself is skipped.
on:
push:
branches: [main, develop]
Expand All @@ -12,23 +18,55 @@ concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
security-events: write

jobs:
analyze:
name: Analyze (JavaScript/TypeScript)
name: CodeQL
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- id: gate
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_USER: ${{ github.event.pull_request.user.login }}
run: |
skip=false
if [ "$EVENT_NAME" = "pull_request" ] && \
[ "$PR_USER" = "github-actions[bot]" ] && \
[ "$BASE_REF" = "develop" ]; then
case "$HEAD_REF" in
chore/backmerge-*) skip=true ;;
esac
if [ "$PR_TITLE" = "chore(release): merge main into develop" ]; then
skip=true
fi
fi
echo "skip_heavy=${skip}" >> "$GITHUB_OUTPUT"
echo "skip_heavy=${skip}"

- name: Skip CodeQL for release back-merge
if: steps.gate.outputs.skip_heavy == 'true'
run: echo "Back-merge PR — required check CodeQL reports success without a scan."

- name: Checkout
uses: actions/checkout@v5
if: steps.gate.outputs.skip_heavy != 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
if: steps.gate.outputs.skip_heavy != 'true'
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
with:
languages: javascript-typescript

- name: Autobuild
uses: github/codeql-action/autobuild@v4
if: steps.gate.outputs.skip_heavy != 'true'
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
if: steps.gate.outputs.skip_heavy != 'true'
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
5 changes: 5 additions & 0 deletions apps/desktop/src/main/__mocks__/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export const safeStorage = {
encryptString: (value: string) => Buffer.from(value),
decryptString: (value: Buffer) => value.toString('utf8'),
};

/** Empty string = success, matching Electron's `shell.openPath`. */
export const shell = {
openPath: async (_filePath: string) => '',
};
Loading
Loading