diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 16e174dc..ec754d21 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,9 @@ ## Summary -- Describe the user-facing outcome. +- Describe the user-facing outcome and any compatibility or migration impact. +- If the change is internal-only, say so. ## Validation -- List the commands or checks run. - -## Changelog - -- [ ] Added `changes/..md` -- [ ] This is dependency maintenance, or it is internal-only and the PR - explains why a maintainer should apply `skip-changelog`: +- List the exact commands or checks run and the real boundaries exercised. +- Do not replace results with “tests passed.” diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 8b48c9b1..0afcf304 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -12,6 +12,11 @@ on: required: false default: 14 type: number + sign: + description: Sign and notarize the macOS build (requires Apple secrets). + required: false + default: false + type: boolean workflow_dispatch: inputs: checkout_ref: @@ -24,6 +29,11 @@ on: required: false default: 14 type: number + sign: + description: Sign and notarize the macOS build (requires Apple secrets). + required: false + default: false + type: boolean permissions: contents: read @@ -116,9 +126,54 @@ jobs: - name: Test the locked desktop crate run: cargo test --release --locked --manifest-path desktop/src-tauri/Cargo.toml - - name: Build the unsigned desktop installer + - name: Prepare the Apple notarization key + id: apple_key + if: runner.os == 'macOS' && inputs.sign + shell: bash + env: + APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} + run: | + if [[ -z "$APPLE_API_KEY_P8" ]]; then + echo "::error::inputs.sign is true but Apple signing secrets are missing." >&2 + exit 1 + fi + key_path="$RUNNER_TEMP/apple_api_key.p8" + printf '%s' "$APPLE_API_KEY_P8" | openssl base64 -d -A > "$key_path" + echo "path=$key_path" >> "$GITHUB_OUTPUT" + + - name: Build the desktop installer + working-directory: desktop + env: + VIDXP_DESKTOP_SIGN: ${{ (runner.os == 'macOS' && inputs.sign) && '1' || '' }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_PATH: ${{ steps.apple_key.outputs.path }} run: npm run desktop:build + + - name: Verify signing and notarization + if: runner.os == 'macOS' && inputs.sign working-directory: desktop + shell: bash + run: | + dmg="$(find src-tauri/target/release/bundle/dmg -maxdepth 1 -name '*.dmg' | head -n1)" + [[ -n "$dmg" ]] + # Tauri removes the intermediate .app after packaging the DMG. + codesign --verify --strict --verbose=2 "$dmg" + mountpoint="$(mktemp -d "$RUNNER_TEMP/vidxp-dmg.XXXXXX")" + trap 'hdiutil detach "$mountpoint" -quiet >/dev/null 2>&1 || true; rmdir "$mountpoint" >/dev/null 2>&1 || true' EXIT + hdiutil attach "$dmg" -readonly -nobrowse -mountpoint "$mountpoint" + app="$(find "$mountpoint" -maxdepth 1 -name '*.app' | head -n1)" + [[ -n "$app" ]] + # The .app is signed (Developer ID), hardened-runtime, notarized + stapled. + codesign --verify --strict --verbose=2 "$app" + codesign -dvv "$app" 2>&1 | grep -i 'Authority=Developer ID Application' + codesign -dvv "$app" 2>&1 | grep -iE 'flags=.*runtime' + xcrun stapler validate "$app" + spctl -a -t exec -vv "$app" - name: Verify the Windows app uses the GUI subsystem if: runner.os == 'Windows' diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 835ea17f..d8bccb93 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -111,6 +111,8 @@ jobs: with: artifact_retention_days: 30 checkout_ref: ${{ inputs.head_sha }} + sign: true + secrets: inherit containers: needs: contract diff --git a/desktop/scripts/build-desktop.mjs b/desktop/scripts/build-desktop.mjs index 9de40716..9ae495b8 100644 --- a/desktop/scripts/build-desktop.mjs +++ b/desktop/scripts/build-desktop.mjs @@ -39,11 +39,21 @@ const executable = resolve( ".bin", process.platform === "win32" ? "tauri.cmd" : "tauri", ); -const result = spawnSync( - executable, - ["build", "--bundles", bundleSpec.bundle, "--ci", "--no-sign", "--", "--locked"], - { shell: process.platform === "win32", stdio: "inherit" }, -); + +// Sign + notarize only when explicitly requested on macOS (release builds). +const signMacos = + process.platform === "darwin" && process.env.VIDXP_DESKTOP_SIGN === "1"; + +const buildArgs = ["build", "--bundles", bundleSpec.bundle, "--ci"]; +if (!signMacos) { + buildArgs.push("--no-sign"); +} +buildArgs.push("--", "--locked"); + +const result = spawnSync(executable, buildArgs, { + shell: process.platform === "win32", + stdio: "inherit", +}); if (result.error) { throw result.error; diff --git a/desktop/src-tauri/entitlements.plist b/desktop/src-tauri/entitlements.plist new file mode 100644 index 00000000..f2eb2ecb --- /dev/null +++ b/desktop/src-tauri/entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/desktop/src-tauri/src/target_profiles.rs b/desktop/src-tauri/src/target_profiles.rs index b9c367ba..183f9857 100644 --- a/desktop/src-tauri/src/target_profiles.rs +++ b/desktop/src-tauri/src/target_profiles.rs @@ -470,7 +470,7 @@ fn collect_probe_output( ) } -#[cfg(test)] +#[cfg(all(test, windows))] fn collect_version_output(executable: &Path) -> Result { collect_command_output(executable, &["--version"], "The VidXP version check", None) } @@ -707,7 +707,7 @@ pub fn validate_executable( validate_executable_with(path, desktop_version, collect_probe_output) } -#[cfg(test)] +#[cfg(all(test, windows))] fn inspect_executable(path: &Path, desktop_version: &str) -> Result { inspect_executable_with( path, diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 35f42c55..55ce26f9 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -55,7 +55,8 @@ } }, "macOS": { - "minimumSystemVersion": "13.0" + "minimumSystemVersion": "13.0", + "entitlements": "entitlements.plist" } } } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 9a0c23f8..803f2f94 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -207,14 +207,12 @@ boundary, say so. The repository's [pull request template](../.github/pull_request_template.md) is the required starting point. GitHub loads it automatically for new pull requests. Complete -all three sections: +both sections: - **Summary:** describe the user-facing outcome and important compatibility or - migration behavior. + migration behavior. Say explicitly when a change is internal-only. - **Validation:** list the exact commands and real boundaries exercised. Do not replace results with “tests passed.” -- **Changelog:** add the correct fragment, or explain why the change is - dependency-only/internal and should receive `skip-changelog`. Keep the description compact, but include: @@ -225,29 +223,56 @@ Keep the description compact, but include: - validation actually performed; and - screenshots or sample output for user-interface changes. -Use Conventional Commit prefixes for commits, for example: +Use Conventional Commit prefixes for PR titles and commits that will land on a +release channel, for example: ```text feat(mcp): add clip artifact discovery fix(frontend): keep search form submittable docs: clarify local installation profiles +ci(release): verify signed macOS installers ``` ## Release notes and versions -Release Please derives versions and changelog entries from Conventional -Commits. Make the squash-merge commit or the commits retained by a regular -merge describe the public change accurately: +Both release channels use Release Please's Python strategy. Release Please +reads the Conventional Commits that land on `main` or `release`. `main` +produces prereleases and `release` produces stable releases; both use the same +commit-type and changelog rules. -- `feat` creates a feature release. -- `fix` and `perf` create a patch release. -- `!` or a `BREAKING CHANGE:` footer creates a breaking release. -- `docs`, `test`, `ci`, `build`, `refactor`, and `chore` do not publish by - themselves. +| Landed commit | Changelog | Version effect | +|---|---|---| +| `feat(scope): ...` | Included | Minor | +| `fix`, `perf`, `deps`, `revert`, or `docs` | Included | Patch | +| `(scope)!: ...` or a `BREAKING CHANGE:` footer | Included as breaking | Major | +| `style`, `chore`, `refactor`, `test`, `build`, or `ci` | Hidden | None by itself | +| Unrecognized type | Omitted | None | + +For example, a breaking API change uses: + +```text +feat(api)!: replace the legacy search response +``` + +Use a hidden type only for work with no user-visible release impact. A public +fix implemented in CI or packaging still needs `fix` or `feat`, not `ci` or +`build`. + +For squash merges, the final squash subject—normally the PR title—must be the +intended Conventional Commit. To keep an internal-facing PR title or describe +multiple release entries, add this block to the PR description: + +```text +BEGIN_COMMIT_OVERRIDE +feat(desktop): sign and notarize macOS installers + +fix(desktop): preserve the signed installer artifact +END_COMMIT_OVERRIDE +``` + +Release Please uses the block instead of the squash commit message. It does not +rename the PR, and the override does not work with plain merges. -Write user-visible `feat`, `fix`, and `perf` subjects for users, not for the -implementation history. Internal corrections to an unreleased feature should -remain part of that feature rather than appear as fictional public bug fixes. Release Please prepares the combined version and changelog in a pull request; do not edit released changelog sections by hand.