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
26 changes: 25 additions & 1 deletion .github/workflows/desktop-release-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ jobs:
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install --yes cpio dbus-x11 fakeroot gnome-keyring libsecret-1-0 rpm zip
sudo apt-get install --yes cpio dbus-x11 desktop-file-utils fakeroot gnome-keyring libglib2.0-bin libsecret-1-0 rpm unzip xdg-utils xvfb zip

- name: Package desktop app from clean checkout
shell: bash
Expand Down Expand Up @@ -337,6 +337,30 @@ jobs:
--make-directory apps/desktop/out/make \
--output "desktop-release-${{ matrix.platform }}-${{ matrix.arch }}"

- name: Exercise staged native install, deep-link, relaunch, and removal lifecycle
if: matrix.platform == 'linux' || matrix.platform == 'darwin'
shell: bash
run: |
if [ "${{ matrix.platform }}" = linux ]; then
keyring_root="$(mktemp -d)"
trap 'rm -rf -- "$keyring_root"' EXIT
dbus-run-session -- bash -euo pipefail -c '
export XDG_DATA_HOME="$1"
eval "$(printf "%s\n" "propr-native-lifecycle" | gnome-keyring-daemon --unlock --components=secrets)"
xvfb-run --auto-servernum \
node apps/desktop/scripts/test-native-artifact-lifecycle.mjs \
--version "$2" \
--platform linux \
--arch "$3" \
--artifact-directory "$4"
' bash "$keyring_root" "$PROPR_DESKTOP_VERSION" "${{ matrix.arch }}" \
"desktop-release-${{ matrix.platform }}-${{ matrix.arch }}"
else
bash apps/desktop/scripts/run-packaged-darwin-connect-smoke.sh \
"${{ matrix.arch }}" native-lifecycle "$PROPR_DESKTOP_VERSION" \
"desktop-release-${{ matrix.platform }}-${{ matrix.arch }}"
fi

- name: Upload unsigned validation target
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
Expand Down
75 changes: 75 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,81 @@ PROPR_DESKTOP_ENABLE_RPM=1 \
npm run make -w @propr/desktop -- --arch="$(node -p process.arch)"
```

### Unsigned internal-RC install and removal (macOS/Linux)

Choose the artifact whose `x64` or `arm64` suffix matches the machine. These are internal validation builds: they do
not claim signing, notarization, or Gatekeeper approval, and the commands below do not weaken quarantine or trust
policy. An unsigned macOS build may therefore be rejected on a normal end-user machine.

Debian/Ubuntu DEB installation and native removal:

```sh
ARCH=x64 # use arm64 on an ARM64 Linux machine
VERSION=0.8.15
sudo apt install "./ProPR-Desktop-${VERSION}-linux-${ARCH}.deb"
propr-desktop
xdg-open 'propr://connect?api=http%3A%2F%2Flocalhost%3A4000'
xdg-open 'propr://connect?api=https%3A%2F%2Ft-your-tunnel.propr.dev'
sudo apt remove propr-desktop
```

Fedora/RHEL-family RPM installation, followed by the package-manager-independent ZIP flow:

```sh
ARCH=x64 # use arm64 on an ARM64 Linux machine
VERSION=0.8.15
sudo rpm --install "ProPR-Desktop-${VERSION}-linux-${ARCH}.rpm"
propr-desktop
sudo rpm --erase propr-desktop

install_root="$(mktemp -d)"
unzip "ProPR-Desktop-${VERSION}-linux-${ARCH}.zip" -d "$install_root"
"$install_root/propr-desktop-linux-${ARCH}/propr-desktop"
rm -r "$install_root"
```

On Intel (`x64`) or Apple Silicon (`arm64`) macOS, mount and copy the DMG or extract the ZIP. Quit the app before
removing it:

```sh
ARCH=arm64 # use x64 on an Intel Mac
VERSION=0.8.15
mount_point="$(mktemp -d)"
hdiutil attach -readonly -nobrowse -mountpoint "$mount_point" \
"ProPR-Desktop-${VERSION}-macos-${ARCH}.dmg"
sudo ditto "$mount_point/propr-desktop.app" '/Applications/propr-desktop.app'
hdiutil detach "$mount_point"
rmdir "$mount_point"
open '/Applications/propr-desktop.app'
open 'propr://connect?api=http%3A%2F%2Flocalhost%3A4000'
open 'propr://connect?api=https%3A%2F%2Ft-your-tunnel.propr.dev'
osascript -e 'tell application id "dev.propr.desktop" to quit'
sudo rm -r '/Applications/propr-desktop.app'

install_root="$(mktemp -d)"
ditto -x -k "ProPR-Desktop-${VERSION}-macos-${ARCH}.zip" "$install_root"
open "$install_root/propr-desktop.app"
osascript -e 'tell application id "dev.propr.desktop" to quit'
rm -r "$install_root"
```

The pull-request native gate runs DEB/RPM/ZIP on `ubuntu-24.04` and `ubuntu-24.04-arm`, and DMG/ZIP on
`macos-15-intel` and `macos-15`. Every staged format is extracted or mounted and copied, launched, shut down,
relaunched with the same isolated profile, and removed. It verifies the staged hash remains unchanged, executable
architecture and native launcher registration, profile permissions and state preservation, warm OS protocol dispatch,
renderer exactly-once acknowledgement, explicit confirmation of untrusted Connect candidates, and cleanup of owned
processes, mounts, LaunchServices registration, profiles, and install roots.

For copied macOS test apps only, CI reuses the packaged-Connect harness to generate one disposable, non-production
code-signing identity in an isolated keychain. It signs the copied app (never the staged DMG/ZIP), verifies the same
designated requirement before and after both launches, and restores the runner's original keychain list/default before
deleting the identity and temporary keychain. This stabilizes the Safe Storage application identity without changing
trust settings and is not evidence of Developer ID signing, notarization, Gatekeeper approval, or end-user launchability.
Linux runs each artifact against one isolated, unlocked D-Bus/libsecret session and proves credential round-trip and
deletion without permitting plaintext/basic-text fallback. Cold launches are direct argv; Linux package warm dispatch uses an
isolated XDG MIME database and `gio`, ZIP warm dispatch is direct because ZIP has no registered launcher, and macOS
warm dispatch uses LaunchServices against the exact copied bundle.

### CI preflight, signing, and notarization configuration

Repository-ruleset inspection uses a dedicated GitHub App installed only on this repository. Configure the App with
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ const config: ForgeConfig = {
productName: 'ProPR Desktop',
version: releaseVersion,
bin: DESKTOP_EXECUTABLE_NAME,
mimeType: ['x-scheme-handler/propr'],
},
})]
: []),
Expand All @@ -204,6 +205,7 @@ const config: ForgeConfig = {
productName: 'ProPR Desktop',
version: releaseVersion,
bin: DESKTOP_EXECUTABLE_NAME,
mimeType: ['x-scheme-handler/propr'],
},
})]
: []),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/scripts/packaged-connect-platform.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('packaged Connect target-native credential setup', () => {
'APPLICATION_SIGNING',
'INITIAL_SIGNATURE_VERIFICATION',
'PAIR_REPROBE_JOURNEY',
'NATIVE_ARTIFACT_LIFECYCLE',
'STABLE_SIGNATURE_VERIFICATION',
'KEYCHAIN_RESTORATION_DELETION',
'TEMPORARY_FILE_CLEANUP',
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/scripts/packaged-smoke-support.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const createSmokeChildEnvironment = async ({
profileApiUrl,
parentEnvironment = process.env,
inspectPath = lstat,
preserveMacosKeychainContext = false,
}) => {
if (!profile || !createdProfiles.has(profile)) {
throw new Error('Packaged smoke child environment rejected an unknown profile');
Expand Down Expand Up @@ -330,8 +331,21 @@ export const createSmokeChildEnvironment = async ({
});
}
if (platform === 'darwin') {
let home = profile.home;
if (preserveMacosKeychainContext) {
const runnerHome = parentEnvironment.HOME;
if (typeof runnerHome !== 'string' || runnerHome.length > 4096
|| !isAbsolute(runnerHome) || resolve(runnerHome) !== runnerHome) {
throw new Error('Packaged smoke macOS Keychain home is invalid');
}
const homeStats = await inspectPath(runnerHome);
if (!homeStats.isDirectory() || homeStats.isSymbolicLink()) {
throw new Error('Packaged smoke macOS Keychain home is invalid');
}
home = runnerHome;
}
return Object.freeze({
HOME: profile.home,
HOME: home,
...triggers,
TEMP: profile.temporary,
TMP: profile.temporary,
Expand Down
37 changes: 36 additions & 1 deletion apps/desktop/scripts/packaged-smoke-support.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'node:assert/strict';
import { chmod, readFile, writeFile } from 'node:fs/promises';
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, join, relative } from 'node:path';
import { describe, test } from 'node:test';
Expand Down Expand Up @@ -109,6 +109,41 @@ describe('packaged smoke native window layout', () => {
});

describe('packaged smoke child environment', () => {
test('preserves a validated runner HOME only for an explicit native macOS Keychain context', async () => {
const profile = await createPrivateSmokeProfile(tmpdir());
const runnerHome = await mkdtemp(join(tmpdir(), 'propr-runner-home-'));
try {
const isolated = await createSmokeChildEnvironment({
platform: 'darwin',
profile,
profileApiUrl: 'http://127.0.0.1:43123',
parentEnvironment: { HOME: runnerHome },
});
assert.equal(isolated.HOME, profile.home);

const keychainEnabled = await createSmokeChildEnvironment({
platform: 'darwin',
profile,
profileApiUrl: 'http://127.0.0.1:43123',
parentEnvironment: { HOME: runnerHome },
preserveMacosKeychainContext: true,
});
assert.equal(keychainEnabled.HOME, runnerHome);
assert.equal(keychainEnabled.TMPDIR, profile.temporary);

await assert.rejects(createSmokeChildEnvironment({
platform: 'darwin',
profile,
profileApiUrl: 'http://127.0.0.1:43123',
parentEnvironment: { HOME: 'relative-home' },
preserveMacosKeychainContext: true,
}), /Keychain home is invalid/);
} finally {
await removePrivateSmokeProfile(profile);
await rm(runnerHome, { recursive: true, force: true });
}
});

test('defines four isolated launches with exact per-mode environment, argv, and marker contracts', () => {
const firstOrigin = 'http://127.0.0.1:41001';
const secondOrigin = 'http://127.0.0.1:41002';
Expand Down
39 changes: 33 additions & 6 deletions apps/desktop/scripts/run-packaged-darwin-connect-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ if [[ "$architecture" != 'arm64' && "$architecture" != 'x64' ]]; then
echo 'Packaged Darwin Connect acceptance requires an explicit supported architecture.' >&2
exit 1
fi
mode="${2:-connect}"
if [[ "$mode" != 'connect' && "$mode" != 'native-lifecycle' ]]; then
echo 'Packaged Darwin acceptance mode is invalid.' >&2
exit 1
fi

script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
repository_root="$(cd "$script_directory/../../.." && pwd -P)"
application="$repository_root/apps/desktop/out/propr-desktop-darwin-$architecture/propr-desktop.app"
signature_verifier="$script_directory/verify-darwin-packaged-connect-signature.mjs"
application_signer="$script_directory/sign-darwin-packaged-connect.mjs"
bounded_runner="$script_directory/run-bounded-darwin-command.mjs"
if [[ ! -d "$application" || ! -f "$signature_verifier" || ! -f "$application_signer"
native_lifecycle="$script_directory/test-native-artifact-lifecycle.mjs"
if [[ ( "$mode" == 'connect' && ! -d "$application" )
|| ( "$mode" == 'native-lifecycle' && ! -f "$native_lifecycle" )
|| ! -f "$signature_verifier" || ! -f "$application_signer"
|| ! -f "$bounded_runner" ]]; then
echo 'Packaged Darwin Connect acceptance artifact is missing.' >&2
exit 1
Expand All @@ -30,14 +38,15 @@ readonly COMMAND_TIMEOUT_MS=30000
readonly CLEANUP_TIMEOUT_MS=10000
readonly SIGNING_TIMEOUT_MS=180000
readonly JOURNEY_TIMEOUT_MS=240000
readonly NATIVE_LIFECYCLE_TIMEOUT_MS=1200000
readonly TERMINATION_GRACE_MS=5000
readonly MAX_OUTPUT_BYTES=262144

stage_marker() {
local stage="$1"
local code="$2"
case "$stage" in
KEY_CERTIFICATE_GENERATION|KEYCHAIN_CREATION_SELECTION|IDENTITY_IMPORT|PARTITION_LIST_UPDATE|APPLICATION_SIGNING|INITIAL_SIGNATURE_VERIFICATION|PAIR_REPROBE_JOURNEY|STABLE_SIGNATURE_VERIFICATION|KEYCHAIN_RESTORATION_DELETION|TEMPORARY_FILE_CLEANUP) ;;
KEY_CERTIFICATE_GENERATION|KEYCHAIN_CREATION_SELECTION|IDENTITY_IMPORT|PARTITION_LIST_UPDATE|APPLICATION_SIGNING|INITIAL_SIGNATURE_VERIFICATION|PAIR_REPROBE_JOURNEY|NATIVE_ARTIFACT_LIFECYCLE|STABLE_SIGNATURE_VERIFICATION|KEYCHAIN_RESTORATION_DELETION|TEMPORARY_FILE_CLEANUP) ;;
*) return 1 ;;
esac
case "$code" in
Expand Down Expand Up @@ -249,12 +258,30 @@ verify_stable_signature() {
"$application" "$identity_sha1" "$requirement_proof" "$keychain_path"
}

run_native_artifact_lifecycle() {
local version="${1:-}"
local artifact_directory="${2:-}"
if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ || ! -d "$artifact_directory" ]]; then
echo 'Native Darwin lifecycle arguments are invalid.' >&2
return 1
fi
PROPR_DESKTOP_NATIVE_SIGNING_KEYCHAIN="$keychain_path" \
PROPR_DESKTOP_NATIVE_SIGNING_CERTIFICATE_SHA1="$identity_sha1" \
run_bounded_forward "$NATIVE_LIFECYCLE_TIMEOUT_MS" node "$native_lifecycle" \
--version "$version" --platform darwin --arch "$architecture" \
--artifact-directory "$artifact_directory"
}

run_stage KEY_CERTIFICATE_GENERATION generate_key_and_certificates
run_stage KEYCHAIN_CREATION_SELECTION create_and_select_keychain
run_stage IDENTITY_IMPORT import_identity
run_stage PARTITION_LIST_UPDATE update_partition_list
unset keychain_password identity_password
run_stage APPLICATION_SIGNING sign_application
run_stage INITIAL_SIGNATURE_VERIFICATION verify_initial_signature
run_stage PAIR_REPROBE_JOURNEY run_pair_and_reprobe
run_stage STABLE_SIGNATURE_VERIFICATION verify_stable_signature
if [[ "$mode" == 'connect' ]]; then
run_stage APPLICATION_SIGNING sign_application
run_stage INITIAL_SIGNATURE_VERIFICATION verify_initial_signature
run_stage PAIR_REPROBE_JOURNEY run_pair_and_reprobe
run_stage STABLE_SIGNATURE_VERIFICATION verify_stable_signature
else
run_stage NATIVE_ARTIFACT_LIFECYCLE run_native_artifact_lifecycle "${3:-}" "${4:-}"
fi
Loading
Loading