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
141 changes: 141 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# The coverage floor every app has to clear. vitest.shared.ts carries the same
# number so the failure happens locally first; scripts/ci/check-coverage.mjs
# re-reads it here so a package cannot configure its own gate away.
COVERAGE_THRESHOLD: "76"

jobs:
# One matrix entry per workspace package that has tests. Runs on Linux because
# it only reads package.json files — nothing is installed or built here.
discover:
name: Discover workspace
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.scan.outputs.packages }}
any: ${{ steps.scan.outputs.any }}
rust: ${{ steps.scan.outputs.rust }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- id: scan
run: node scripts/ci/workspace-packages.mjs >> "$GITHUB_OUTPUT"

# Windows because that is the only platform the product supports.
test:
name: test · ${{ matrix.package.path }}
needs: discover
if: needs.discover.outputs.any == 'true'
runs-on: windows-latest
strategy:
# One package failing must not hide the state of the others.
fail-fast: false
matrix:
package: ${{ fromJSON(needs.discover.outputs.packages) }}
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Test with coverage
run: pnpm --filter "${{ matrix.package.name }}" run ${{ matrix.package.script }}

- name: Enforce the coverage floor
run: node scripts/ci/check-coverage.mjs "${{ matrix.package.path }}"

- name: Upload the coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.package.slug }}
path: ${{ matrix.package.path }}/coverage
if-no-files-found: ignore
retention-days: 7

checks:
name: typecheck & lint
needs: discover
if: needs.discover.outputs.any == 'true'
runs-on: windows-latest
Comment on lines +80 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

checks job never runs while no package declares a test script.

if: needs.discover.outputs.any == 'true' gates the typecheck & lint job on whether any workspace package has a test/test:coverage script (per scripts/ci/workspace-packages.mjs). Per the PR's own status, no packages exist yet, so any is false today. This means the checks job — which runs pnpm run typecheck and pnpm run lint at the root, validating the very tsconfig.base.json/tsconfig.json/vitest.shared.ts files this PR adds — is skipped entirely, and the ci aggregator job (lines 124-134) treats a skipped job as passing. The root typecheck/lint commands already tolerate an empty workspace (--if-present), so this job doesn't need to depend on test-script presence at all.

🐛 Proposed fix
   checks:
     name: typecheck & lint
     needs: discover
-    if: needs.discover.outputs.any == 'true'
     runs-on: windows-latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checks:
name: typecheck & lint
needs: discover
if: needs.discover.outputs.any == 'true'
runs-on: windows-latest
checks:
name: typecheck & lint
needs: discover
runs-on: windows-latest
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 80 - 84, Update the checks job
condition in the checks workflow job so typecheck and lint run independently of
needs.discover.outputs.any or test-script discovery. Preserve the existing
discover dependency only if required for workflow ordering, and ensure the ci
aggregator continues to include the checks result.

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm run typecheck

- run: pnpm run lint

# Skipped until crates/ exists. No coverage gate here: the recorder is driven
# through its JSON-RPC boundary, and a line count over WASAPI glue would
# measure the wrong thing.
rust:
name: rust
needs: discover
if: needs.discover.outputs.rust == 'true'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- uses: Swatinem/rust-cache@v2

- run: cargo fmt --all --check

- run: cargo clippy --all-targets --all-features -- -D warnings

- run: cargo test --locked --all-features

# The single check to require on the branch. Skipped jobs are fine — an empty
# workspace has nothing to test — but a failed or cancelled one is not.
ci:
name: CI
if: always()
needs: [discover, test, checks, rust]
runs-on: ubuntu-latest
steps:
- name: Fail if any job failed or was cancelled
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1

- name: Report
run: |
echo "discover: ${{ needs.discover.result }}"
echo "test: ${{ needs.test.result }}"
echo "checks: ${{ needs.checks.result }}"
echo "rust: ${{ needs.rust.result }}"
115 changes: 115 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:

concurrency:
# Never two runs publishing the same tag.
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write

jobs:
release:
name: Build and publish the installer
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/release.yml
printf '%s\n' '--- package manifests and scripts ---'
git ls-files '*package.json' '*pnpm-lock.yaml' '*npmrc' '*yarnrc*' | head -80
rg -n --glob 'package.json' '"(scripts|packageManager|engines|prepare|preinstall|install|postinstall)"' .
printf '%s\n' '--- checkout and credential references ---'
rg -n 'actions/checkout|persist-credentials|github\.token|GITHUB_TOKEN|\.git/config|pnpm install|electron-builder|nsis|makensis' .github package.json pnpm-workspace.yaml 2>/dev/null || true

Repository: protonspy/open-wiki

Length of output: 6183


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- tracked workflow files ---'
git ls-files '.github/workflows/*'
printf '%s\n' '--- release-related files ---'
git ls-files | rg '(^|/)(package\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|.*release.*|.*package.*|.*electron.*|.*nsis.*)$' | head -120
printf '%s\n' '--- repository configuration ---'
git ls-files | rg '(^|/)(\.npmrc|\.pnpmfile\.cjs|\.gitignore|CODEOWNERS|README.*|CONTRIBUTING.*)$' | head -120

Repository: protonspy/open-wiki

Length of output: 468


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- root package.json ---'
cat -n package.json
printf '%s\n' '--- workspace definition ---'
cat -n pnpm-workspace.yaml
printf '%s\n' '--- tracked desktop files ---'
git ls-files 'apps/desktop/*' | head -80
printf '%s\n' '--- distribution ADR ---'
cat -n docs/adr/0009-distribution-through-github-releases.md

Repository: protonspy/open-wiki

Length of output: 4839


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/release.yml").read_text()
checkout = workflow.index("actions/checkout@v4")
install = workflow.index("pnpm install --frozen-lockfile")
package = workflow.index('pnpm --filter "`@project-wiki/desktop`" run package')

assert checkout < install < package
assert "persist-credentials: false" not in workflow
assert "contents: write" in workflow
assert "GH_TOKEN: ${{ github.token }}" in workflow
print("checkout precedes install and packaging")
print("persist-credentials override: absent")
print("job permission: contents: write")
print("release check token: github.token")
PY

Repository: protonspy/open-wiki

Length of output: 300


Disable checkout credential persistence.

This workflow grants contents: write. Installation and packaging run after checkout, so compromised code could use the persisted token from .git/config.

Set persist-credentials: false.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 21-24: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 21, Update the actions/checkout@v4
step in the release workflow to set persist-credentials to false, preventing the
write-capable token from being retained in the repository’s Git configuration.

Source: Linters/SAST tools


# Until task 10.1 lands there is nothing to package. Saying so beats a
# pnpm error about a filter matching no project.
- name: Check the desktop app exists
shell: pwsh
run: |
if (-not (Test-Path "apps/desktop/package.json")) {
Write-Error "apps/desktop does not exist yet — there is nothing to release (plan task 10.1)."
exit 1
}

- name: Check the tag matches the app version
if: startsWith(github.ref, 'refs/tags/v')
shell: pwsh
run: |
$tag = "${{ github.ref_name }}".TrimStart("v")
$version = (Get-Content "apps/desktop/package.json" -Raw | ConvertFrom-Json).version
if ($tag -ne $version) {
Write-Error "tag v$tag does not match apps/desktop/package.json version $version"
Comment on lines +37 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/release.yml'

printf '%s\n' '--- relevant workflow sections ---'
sed -n '1,90p' .github/workflows/release.yml

printf '%s\n' '--- all github.ref_name uses ---'
rg -n -C 4 'github\.ref_name|RELEASE_TAG|ConvertFrom-Json|TrimStart' .github/workflows/release.yml

Repository: protonspy/open-wiki

Length of output: 4892


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import shutil
import subprocess

candidates = {
    "tag-injection": 'v"; Write-Output "TAG_INJECTION',
    "tag-injection-with-version-prefix": 'v1.2.3"; Write-Output "TAG_INJECTION',
    "release-command-injection": 'v"; Write-Output "RELEASE_INJECTION',
}

workflow_lines = {
    "version-check": '$tag = "${TAG}".TrimStart("v")',
    "release-view": 'gh release view "${TAG}" 2>$null',
    "existing-release-error": 'Write-Error "release ${TAG} already exists"',
    "no-release-output": 'Write-Host "no release for ${TAG} yet"',
}

for label, tag in candidates.items():
    ref = f"refs/tags/{tag}"
    result = subprocess.run(
        ["git", "check-ref-format", ref],
        text=True,
        capture_output=True,
    )
    print(f"{label}: git_ref_name_allowed={result.returncode == 0}")
    if result.returncode == 0:
        for name, template in workflow_lines.items():
            print(f"  {name}: {template.replace('${TAG}', tag)}")

print("pwsh_available:", bool(shutil.which("pwsh")))
print("powershell_available:", bool(shutil.which("powershell")))
PY

Repository: protonspy/open-wiki

Length of output: 364


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import subprocess

tags = [
    'v$(Write-Output TAG_INJECTION)',
    'v1.2.3$(Write-Output TAG_INJECTION)',
    'v`$(Write-Output TAG_INJECTION)',
    'v$(Get-Date)',
    'v${HOME}',
]

templates = {
    "version-check": '$tag = "${TAG}".TrimStart("v"); Write-Output "VERSION_DONE"',
    "release-view": 'Write-Output "before"; Write-Output "${TAG}"; Write-Output "after"',
    "existing-release-error": 'Write-Error "release ${TAG} already exists" 2>$null',
    "no-release-output": 'Write-Host "no release for ${TAG} yet"',
}

for tag in tags:
    allowed = subprocess.run(
        ["git", "check-ref-format", f"refs/tags/{tag}"],
        capture_output=True,
    ).returncode == 0
    print(f"\nTAG {tag!r} allowed={allowed}")
    if not allowed:
        continue
    for name, template in templates.items():
        script = template.replace("${TAG}", tag)
        result = subprocess.run(
            ["pwsh", "-NoProfile", "-NonInteractive", "-Command", script],
            text=True,
            capture_output=True,
        )
        print(f"{name}: exit={result.returncode}")
        print("  stdout:", repr(result.stdout.strip()))
        print("  stderr:", repr(result.stderr.strip()))
PY

Repository: protonspy/open-wiki

Length of output: 951


🏁 Script executed:

#!/bin/bash
set -eu

pwsh -NoProfile -NonInteractive -Command @'
$tag = 'v$(Get-Date)'
function gh { param([Parameter(ValueFromRemainingArguments=$true)]$Args); Write-Output ("GH_ARGS=" + ($Args -join '|')) }

Write-Output '--- interpolated workflow source ---'
$tag = "v$(Get-Date)".TrimStart("v")
Write-Output ("TAG_VALUE=" + $tag)
gh release view "v$(Get-Date)" 2>$null
Write-Error "release v$(Get-Date) already exists" 2>$null
Write-Host "no release for v$(Get-Date) yet"

Write-Output '--- environment value ---'
$env:RELEASE_TAG = 'v$(Get-Date)'
$tag = ($env:RELEASE_TAG).TrimStart("v")
Write-Output ("TAG_VALUE=" + $tag)
gh release view "$env:RELEASE_TAG" 2>$null
Write-Error "release $env:RELEASE_TAG already exists" 2>$null
Write-Host "no release for $env:RELEASE_TAG yet"
'@

Repository: protonspy/open-wiki

Length of output: 389


🏁 Script executed:

#!/bin/bash
set -eu

pwsh -NoProfile -NonInteractive -Command - <<'PS'
$tag = 'v$(Get-Date)'
function gh {
  param([Parameter(ValueFromRemainingArguments=$true)]$Args)
  Write-Output ("GH_ARGS=" + ($Args -join '|'))
}

Write-Output '--- interpolated workflow source ---'
$tag = "v$(Get-Date)".TrimStart("v")
Write-Output ("TAG_VALUE=" + $tag)
gh release view "v$(Get-Date)" 2>$null
Write-Error "release v$(Get-Date) already exists" 2>$null
Write-Host "no release for v$(Get-Date) yet"

Write-Output '--- environment value ---'
$env:RELEASE_TAG = 'v$(Get-Date)'
$tag = ($env:RELEASE_TAG).TrimStart("v")
Write-Output ("TAG_VALUE=" + $tag)
gh release view "$env:RELEASE_TAG" 2>$null
Write-Error "release $env:RELEASE_TAG already exists" 2>$null
Write-Host "no release for $env:RELEASE_TAG yet"
PS

Repository: protonspy/open-wiki

Length of output: 420


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import json
import pathlib
import subprocess

package = pathlib.Path("apps/desktop/package.json")
version = json.loads(package.read_text())["version"] if package.is_file() else "1.2.3"
tag = f"v$(Write-Output('{version}'))"

allowed = subprocess.run(
    ["git", "check-ref-format", f"refs/tags/{tag}"],
    capture_output=True,
).returncode == 0
print("package_version:", version)
print("tag:", tag)
print("git_ref_name_allowed:", allowed)
PY

pwsh -NoProfile -NonInteractive -Command - <<'PS'
$version = '1.2.3'
$tag = "v$(Write-Output('1.2.3'))".TrimStart("v")
Write-Output ("tag=" + $tag)
Write-Output ("matches=" + ($tag -eq $version))
PS

Repository: protonspy/open-wiki

Length of output: 261


Pass github.ref_name through env before using it in PowerShell.

A valid tag can execute a PowerShell subexpression during source interpolation and still produce a version-matching $tag. Set RELEASE_TAG: ${{ github.ref_name }} in both affected steps, then use $env:RELEASE_TAG for the version check, gh command, and messages.

🧰 Tools
🪛 zizmor (1.28.0)

[error] 37-37: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 37 - 40, Update both affected
release workflow steps to define RELEASE_TAG in their env blocks from
github.ref_name, then replace direct github.ref_name interpolation with
$env:RELEASE_TAG for the PowerShell version comparison, gh command, and related
messages.

Source: Linters/SAST tools

exit 1
}
Write-Host "releasing $version"

- name: Refuse to republish an existing release
if: startsWith(github.ref, 'refs/tags/v')
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
# A published release has been downloaded; deleting it does not undo that.
gh release view "${{ github.ref_name }}" 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Error "release ${{ github.ref_name }} already exists — bump the version and tag again"
exit 1
}
Write-Host "no release for ${{ github.ref_name }} yet"
Comment on lines +45 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow context ---'
sed -n '1,100p' .github/workflows/release.yml
printf '%s\n' '--- ADR and release-related references ---'
rg -n -C 3 'ADR.?0009|exactly.?once|gh release|release create|publish' .github README.md docs . 2>/dev/null | head -250

Repository: protonspy/open-wiki

Length of output: 12761


🌐 Web query:

GitHub CLI gh release view exit codes 404 authentication rate limit documentation

💡 Result:

The GitHub CLI (gh) follows standard exit code conventions, where 0 indicates success and 1 indicates a general failure [1]. While some legacy man pages may list specific exit codes such as 4 for authentication [2][3], the current official GitHub CLI documentation defines its exit codes as follows [1]: 0: Successful execution. 1: Command failed for any reason. 2: Command was running but was cancelled. Regarding your specific queries: Exit Code 1: If a command like gh release view fails (for instance, due to a 404 "Not Found" error when a release does not exist), the CLI exits with code 1 [4][5]. It is common practice in automation scripts to check for these failures [5]. Authentication and Rate Limits: There is no distinct exit code for authentication errors or API rate limit exceeded errors in the current documentation; these are treated as command failures and result in an exit code of 1 [1]. Note on documentation: While you may encounter older or community-generated documentation suggesting an exit code of 4 for authentication [2][3], the official CLI help currently directs users to the standard convention where 1 represents all failures [1]. When writing scripts, you should generally rely on exit code 0 for success and non-zero (typically 1) for any failure, rather than relying on granular exit code differentiation [4].

Citations:


🌐 Web query:

softprops/action-gh-release v2 existing release overwrite behavior update release

💡 Result:

When using softprops/action-gh-release (v2), the action is designed to automatically detect if a GitHub release already exists for the specified tag [1][2]. If an existing release is found, the action will update it rather than creating a new one [1][3]. Regarding the behavior of release data and files: 1. Release Info: If release metadata fields (such as name, body, or prerelease status) are not explicitly defined in the action configuration, the existing release will retain its original information [1][2]. 2. File Overwriting: The action includes an overwrite_files input, which defaults to true [1][4]. When this is enabled, the action will overwrite existing release assets that share the same filename [1][4]. 3. Body Content: By default, the action overwrites the release body. If you wish to preserve existing content, you can set the append_body input to true [1][5]. 4. Draft Status: If the action reuses an existing draft release, you should set draft: true in your configuration to ensure it remains a draft; otherwise, the action will attempt to publish the release after uploading assets [1][2]. If you encounter unexpected behavior, such as release creation errors, ensure that your configuration and permissions are correct, as issues related to release updates are often tracked in the project's issue repository [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- ADR details ---'
sed -n '55,70p' docs/adr/0009-distribution-through-github-releases.md
printf '%s\n' '--- workflow publish configuration ---'
sed -n '88,120p' .github/workflows/release.yml
printf '%s\n' '--- repository references to action-gh-release inputs ---'
rg -n -C 3 'softprops/action-gh-release|overwrite_files|fail_on_unmatched_files|tag_name|draft|make_latest|overwrite' .github docs plans README.md . 2>/dev/null | head -250

Repository: protonspy/open-wiki

Length of output: 6824


Fail closed on release lookup errors. Continue only for an explicit 404. Authentication, rate-limit, and network failures must stop the workflow. softprops/action-gh-release@v2 updates existing releases and overwrites matching assets by default, so use a create-only publication operation to preserve the exactly-once rule.

🧰 Tools
🪛 zizmor (1.28.0)

[error] 52-52: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 54-54: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 57-57: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 45 - 57, Update the “Refuse to
republish an existing release” step to continue only when gh release view
returns an explicit 404; fail the workflow for authentication, rate-limit,
network, or any other lookup error. Then configure the subsequent
softprops/action-gh-release@v2 publication step to use create-only behavior and
prevent updating existing releases or overwriting matching assets.


- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Build the installer
shell: pwsh
env:
# electron-builder picks these up on its own and signs when they are
# set. Absent, it builds unsigned — see adr:0009.
CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
# Publishing is this workflow's job, not electron-builder's.
GH_TOKEN: ""
run: pnpm --filter "@project-wiki/desktop" run package

- name: Collect the artifacts and their checksums
id: collect
shell: pwsh
run: |
$dir = "apps/desktop/release"
$installers = @(Get-ChildItem -Path $dir -Filter *.exe -File -ErrorAction SilentlyContinue)
if ($installers.Count -eq 0) {
Write-Error "no .exe found in $dir — the package script produced no installer"
exit 1
}
$installers | ForEach-Object { Write-Host " $($_.Name) $([math]::Round($_.Length / 1MB, 1)) MB" }
Get-FileHash -Algorithm SHA256 $installers.FullName |
ForEach-Object { "$($_.Hash.ToLower()) $(Split-Path $_.Path -Leaf)" } |
Out-File -FilePath "$dir/SHA256SUMS.txt" -Encoding utf8
Comment on lines +84 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/release.yml' 'ADR*' '*ADR*'
printf '%s\n' '--- release workflow ---'
cat -n .github/workflows/release.yml | sed -n '1,125p'
printf '%s\n' '--- ADR 0009 references ---'
rg -n -i -C 4 '0009|single NSIS|NSIS|installer|electron-builder' . --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: protonspy/open-wiki

Length of output: 13756


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- package and build configuration references ---'
rg -n -i -C 5 'electron-builder|nsis|win\.target|portable|uninstaller|artifactName|publish|package script' . --glob '!node_modules' --glob '!dist' --glob '!build'
printf '%s\n' '--- workflow expressions and artifact publication ---'
rg -n -C 5 'installers|SHA256SUMS|upload-artifact|Get-ChildItem|package|artifact|\.exe' .github/workflows/release.yml

Repository: protonspy/open-wiki

Length of output: 13245


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/release.yml")
lines = p.read_text().splitlines()
for start, end in [(70, 110)]:
    for n in range(start, min(end, len(lines)) + 1):
        print(f"{n:4}: {lines[n-1]}")
PY
printf '%s\n' '--- candidate ADR files ---'
find . -type f \( -iname '*adr*' -o -iname '*decision*' \) -not -path '*/node_modules/*' -not -path '*/dist/*' -not -path '*/build/*' -print

Repository: protonspy/open-wiki

Length of output: 2368


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- desktop package files ---'
git ls-files 'apps/desktop/*' 'package.json' 'pnpm-workspace.yaml' | sed -n '1,160p'
printf '%s\n' '--- package scripts and electron-builder configuration ---'
rg -n -i -C 8 '"package"|"build"|electron-builder|nsis|win[[:space:]]*:' \
  --glob 'package.json' --glob '*.json' --glob '*.yaml' --glob '*.yml' \
  apps package.json pnpm-workspace.yaml 2>/dev/null || true
printf '%s\n' '--- release references ---'
rg -n -C 4 'apps/desktop/release|SHA256SUMS|single NSIS|expected exactly one' \
  . --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: protonspy/open-wiki

Length of output: 2773


Require exactly one NSIS installer.

The workflow treats every .exe in apps/desktop/release/ as the installer and publishes every match. Fail unless the expected installer set contains exactly one file. Publish that explicit path and generate its checksum. Validate the electron-builder NSIS target instead of relying on the .exe extension.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 84 - 92, Update the installer
validation and publishing flow around the PowerShell installer discovery to
require exactly one expected electron-builder NSIS installer, rather than
accepting every .exe in the release directory. Fail when the NSIS target is
missing or multiple installers are found, then publish only that explicit
installer path and generate its SHA256 checksum.

Get-Content "$dir/SHA256SUMS.txt"

- name: Publish the release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
apps/desktop/release/*.exe
apps/desktop/release/SHA256SUMS.txt
generate_release_notes: true
# A tag carrying a suffix — v0.1.0-beta.1 — is not a stable release.
prerelease: ${{ contains(github.ref_name, '-') }}
fail_on_unmatched_files: true

# workflow_dispatch builds without a tag: useful for checking the packaging
# still works without publishing anything.
- name: Upload the installer as a build artifact
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v4
with:
name: installer-${{ github.sha }}
path: apps/desktop/release/*
retention-days: 7
43 changes: 43 additions & 0 deletions docs/adr/0001-no-backend-byok.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
status: accepted
---

# 0001 · No backend: BYOK, no accounts, no telemetry

## Context

The application handles two categories of sensitive data: meeting audio and a project's
internal documentation. It needs transcription, which is a third-party service. The
question is who talks to that service — a backend of ours, or the user's machine.

A backend would bring real convenience: a single credential, aggregated billing,
configuration changes without a release. It would also bring the position of data
processor under the LGPD, infrastructure cost proportional to usage, and the obligation
to answer what happens to the audio of a confidential meeting that passed through our
servers.

## Decision

There is no backend. The user supplies their own transcription credential, the
application talks to the provider directly, and there is no account, no authentication
of our own and no telemetry of any kind — including anonymous crash telemetry.
Comment on lines +21 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use one authentication vocabulary across the ADRs.

The statements in docs/adr/0001-no-backend-byok.md and docs/adr/0003-mcp-as-the-only-bridge-to-the-llm.md omit the local MCP token defined by docs/adr/0007-plaintext-credentials-in-the-config.md.

  • docs/adr/0001-no-backend-byok.md#L21-L23: limit “no authentication” to hosted accounts and user authentication.
  • docs/adr/0003-mcp-as-the-only-bridge-to-the-llm.md#L38-L38: state that the application stores the transcription credential and local MCP token, but no LLM credential.
📍 Affects 2 files
  • docs/adr/0001-no-backend-byok.md#L21-L23 (this comment)
  • docs/adr/0003-mcp-as-the-only-bridge-to-the-llm.md#L38-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0001-no-backend-byok.md` around lines 21 - 23, The ADRs use
inconsistent authentication terminology. In docs/adr/0001-no-backend-byok.md
lines 21-23, revise the no-authentication statement to limit it to hosted
accounts and user authentication. In
docs/adr/0003-mcp-as-the-only-bridge-to-the-llm.md line 38, state that the
application stores the transcription credential and local MCP token, but no LLM
credential.


## Consequences

The audio and the documents never pass through a server of ours, which leaves us in the
position of a software vendor rather than a data processor. That simplifies the LGPD
position considerably — and stops being true the instant any hosted component is added.
This is why it is an ADR and not a line in a README: the cost is not adding the
component, it is losing the position.

The cost is onboarding: the user has to create an account somewhere else and paste a
credential before the first recording works. The application validates the credential on
the spot precisely because a wrong key discovered after an hour of recording is the worst
possible way to discover it.

Without telemetry, we do not know what breaks on anyone's machine. Diagnosis depends on
the local log and on what the user reports.

There is a way out for anyone who does not want even that: transcribe locally, with no
credential at all. It exists because this ADR is only convincing if the privacy argument
has a path that depends on trusting no third party whatsoever.
43 changes: 0 additions & 43 deletions docs/adr/0001-sem-backend-byok.md

This file was deleted.

Loading
Loading