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
21 changes: 17 additions & 4 deletions .github/workflows/macos-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ concurrency:
jobs:
package:
name: macOS ARM64 self-contained package
if: ${{ github.ref == 'refs/heads/main' && github.ref_protected && inputs.publish_authorized_artifact }}
environment: macos-release
runs-on: macos-26
timeout-minutes: 90
timeout-minutes: 150
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
with:
fetch-depth: 0
submodules: recursive
- name: Enforce Object-form artifact authorization
if: ${{ inputs.publish_authorized_artifact && vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED != 'true' }}
if: ${{ vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED != 'true' || vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED_SHA != github.sha }}
run: |
echo "Artifact upload requires CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED=true" >&2
echo "Artifact upload requires repository authorization for this exact commit" >&2
exit 1
- name: Verify ARM64 runner
run: test "$(uname -m)" = arm64
Expand Down Expand Up @@ -61,12 +63,23 @@ jobs:
cache-dependency-path: apps/frontend/package-lock.json
- name: Install pinned Tauri CLI
run: cargo install tauri-cli --version 2.8.4 --locked
- name: Configure Developer ID signing and notarization
env:
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
MAC_CERTS: ${{ secrets.MAC_CERTS }}
MAC_CERTS_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
run: ./scripts/configure-macos-signing.sh
- name: Build and verify self-contained package
run: make macos-package
- name: Remove ephemeral signing keychain
if: ${{ always() }}
run: ./scripts/cleanup-macos-signing.sh
- name: Add package manifest to summary
run: cat target/macos-package/BUILD-MANIFEST.txt >> "$GITHUB_STEP_SUMMARY"
- name: Upload authorized package
if: ${{ inputs.publish_authorized_artifact && vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED == 'true' }}
if: ${{ vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED == 'true' && vars.CHAT2DB_OBJECT_DISTRIBUTION_AUTHORIZED_SHA == github.sha }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: Chat2DB-Rust-macOS-arm64-${{ github.sha }}
Expand Down
2 changes: 1 addition & 1 deletion apps/chat2db-desktop/tauri.package.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"macOS": {
"minimumSystemVersion": "12.0",
"hardenedRuntime": false
"hardenedRuntime": true
}
}
}
166 changes: 163 additions & 3 deletions scripts/build-macos-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,24 @@ build_target="${CHAT2DB_MACOS_BUILD_TARGET:-${repository_root}/target/macos-pack
app_path="${build_target}/release/bundle/macos/Chat2DB Rust.app"
package_directory="${repository_root}/target/macos-package"
staging_directory=""
notary_directory=""
verification_directory=""
dmg_mounted=false

cleanup() {
if [[ "${dmg_mounted}" == true && -n "${verification_directory}" ]]; then
hdiutil detach "${verification_directory}" >/dev/null 2>&1 || true
dmg_mounted=false
fi
if [[ -n "${staging_directory}" && -d "${staging_directory}" ]]; then
rm -rf -- "${staging_directory}"
fi
if [[ -n "${notary_directory}" && -d "${notary_directory}" ]]; then
rm -rf -- "${notary_directory}"
fi
if [[ -n "${verification_directory}" && -d "${verification_directory}" ]]; then
rm -rf -- "${verification_directory}"
fi
}
trap cleanup EXIT

Expand Down Expand Up @@ -111,12 +124,119 @@ if [[ ! -d "${app_path}" || -L "${app_path}" ]]; then
fi

signing_identity="${APPLE_SIGNING_IDENTITY:--}"
signing_keychain="${CHAT2DB_SIGNING_KEYCHAIN:-}"
notary_profile="${CHAT2DB_NOTARY_KEYCHAIN_PROFILE:-}"
expected_team_id="${APPLE_TEAM_ID:-}"
notarization_enabled=false
notarization_status="not-submitted"
distribution_status="internal-test-only"

if [[ -n "${notary_profile}" ]]; then
if [[ "${signing_identity}" == "-" ]]; then
echo "notarization requires a Developer ID signing identity" >&2
exit 1
fi
if [[ -z "${signing_keychain}" || "${signing_keychain}" != /* || ! -f "${signing_keychain}" || -L "${signing_keychain}" ]]; then
echo "notarization requires a safe signing keychain" >&2
exit 1
fi
if [[ -z "${expected_team_id}" ]]; then
echo "notarization requires APPLE_TEAM_ID" >&2
exit 1
fi
notarization_enabled=true
fi

if [[ "${signing_identity}" == "-" ]]; then
codesign --force --deep --sign - --timestamp=none "${app_path}"
"${repository_root}/scripts/verify-macos-package.sh" "${app_path}"
else
codesign --force --deep --options runtime --timestamp --sign "${signing_identity}" "${app_path}"
# Tauri owns the only Developer ID signing pass so nested runtime
# entitlements and signatures are not destroyed by a deep re-sign.
CHAT2DB_REQUIRE_DEVELOPER_ID_SIGNATURE=true \
APPLE_TEAM_ID="${expected_team_id}" \
"${repository_root}/scripts/verify-macos-package.sh" "${app_path}"
distribution_status="developer-id-signed"
fi

notarize_artifact() {
local artifact_path="$1"
xcrun notarytool submit "${artifact_path}" \
--keychain-profile "${notary_profile}" \
--keychain "${signing_keychain}" \
--wait \
--timeout 45m
}

verify_developer_id_signature() {
local artifact_path="$1"
local artifact_kind="$2"
local signature_details
local signing_team_id
local signing_authority
local signing_timestamp
local designated_requirement

codesign --verify --strict --verbose=2 "${artifact_path}"
signature_details="$(codesign -dv --verbose=4 "${artifact_path}" 2>&1)"
signing_team_id="$(awk -F= '/^TeamIdentifier=/ { print $2; exit }' <<<"${signature_details}")"
signing_authority="$(awk -F= '/^Authority=/ { print $2; exit }' <<<"${signature_details}")"
signing_timestamp="$(awk -F= '/^Timestamp=/ { print $2; exit }' <<<"${signature_details}")"
designated_requirement="$(codesign -d -r- "${artifact_path}" 2>&1)"

if [[ "${signing_authority}" != Developer\ ID\ Application:* ]]; then
echo "${artifact_kind} is not signed by a Developer ID Application identity" >&2
exit 1
fi
if [[ -z "${signing_team_id}" || "${signing_team_id}" == "not set" ]]; then
echo "${artifact_kind} is missing a TeamIdentifier" >&2
exit 1
fi
if [[ -n "${expected_team_id}" && "${signing_team_id}" != "${expected_team_id}" ]]; then
echo "${artifact_kind} TeamIdentifier does not match APPLE_TEAM_ID" >&2
exit 1
fi
if [[ -z "${signing_timestamp}" || "${signing_timestamp}" == "none" ]]; then
echo "${artifact_kind} is missing a trusted timestamp" >&2
exit 1
fi
if [[ "${designated_requirement}" == *"cdhash"* ]]; then
echo "${artifact_kind} still has a build-specific cdhash requirement" >&2
exit 1
fi
}

verify_packaged_app() {
local packaged_app="$1"
if [[ "${signing_identity}" == "-" ]]; then
"${repository_root}/scripts/verify-macos-package.sh" "${packaged_app}"
else
CHAT2DB_REQUIRE_DEVELOPER_ID_SIGNATURE=true \
APPLE_TEAM_ID="${expected_team_id}" \
"${repository_root}/scripts/verify-macos-package.sh" "${packaged_app}"
fi
if [[ "${notarization_enabled}" == true ]]; then
xcrun stapler validate "${packaged_app}"
spctl --assess --type execute --verbose=4 "${packaged_app}"
fi
}

if [[ "${notarization_enabled}" == true ]]; then
notary_directory="$(mktemp -d "${target_root}/.chat2db-notary.XXXXXX")"
notary_app_zip="${notary_directory}/Chat2DB-Rust.app.zip"
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${notary_app_zip}"
notarize_artifact "${notary_app_zip}"
xcrun stapler staple "${app_path}"
xcrun stapler validate "${app_path}"
spctl --assess --type execute --verbose=4 "${app_path}"
rm -rf -- "${notary_directory}"
notary_directory=""
CHAT2DB_REQUIRE_DEVELOPER_ID_SIGNATURE=true \
APPLE_TEAM_ID="${expected_team_id}" \
"${repository_root}/scripts/verify-macos-package.sh" "${app_path}"
notarization_status="accepted"
distribution_status="developer-id-notarized"
fi
"${repository_root}/scripts/verify-macos-package.sh" "${app_path}"

version="$(awk '
/^\[workspace.package\]$/ { in_package = 1; next }
Expand Down Expand Up @@ -148,6 +268,12 @@ zip_path="${package_directory}/${artifact_base}.app.zip"
dmg_path="${package_directory}/${artifact_base}.dmg"
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${zip_path}"

verification_directory="$(mktemp -d "${target_root}/.chat2db-zip-verify.XXXXXX")"
ditto -x -k "${zip_path}" "${verification_directory}"
verify_packaged_app "${verification_directory}/Chat2DB Rust.app"
rm -rf -- "${verification_directory}"
verification_directory=""

staging_directory="$(mktemp -d "${repository_root}/target/.macos-dmg.staging.XXXXXX")"
ditto "${app_path}" "${staging_directory}/Chat2DB Rust.app"
ln -s /Applications "${staging_directory}/Applications"
Expand All @@ -160,13 +286,44 @@ hdiutil create \
rm -rf -- "${staging_directory}"
staging_directory=""

if [[ "${signing_identity}" != "-" ]]; then
if [[ -n "${signing_keychain}" ]]; then
codesign --force --sign "${signing_identity}" --keychain "${signing_keychain}" --timestamp "${dmg_path}"
else
codesign --force --sign "${signing_identity}" --timestamp "${dmg_path}"
fi
verify_developer_id_signature "${dmg_path}" "macOS DMG"
fi

if [[ "${notarization_enabled}" == true ]]; then
notarize_artifact "${dmg_path}"
xcrun stapler staple "${dmg_path}"
xcrun stapler validate "${dmg_path}"
spctl --assess --type open --context context:primary-signature --verbose=4 "${dmg_path}"
fi
hdiutil verify "${dmg_path}"

verification_directory="$(mktemp -d "${target_root}/.chat2db-dmg-verify.XXXXXX")"
hdiutil attach -readonly -nobrowse -mountpoint "${verification_directory}" "${dmg_path}"
dmg_mounted=true
verify_packaged_app "${verification_directory}/Chat2DB Rust.app"
hdiutil detach "${verification_directory}"
dmg_mounted=false
rm -rf -- "${verification_directory}"
verification_directory=""

(
cd "${package_directory}"
shasum -a 256 -- "$(basename "${zip_path}")" "$(basename "${dmg_path}")" > SHA256SUMS
)
git_commit="$(git -C "${repository_root}" rev-parse HEAD)"
community_commit="$(git -C "${repository_root}/third_party/chat2db-community" rev-parse HEAD)"
app_kib="$(du -sk "${app_path}" | awk '{ print $1 }')"
signature_details="$(codesign -dv --verbose=4 "${app_path}" 2>&1)"
signing_team_id="$(awk -F= '/^TeamIdentifier=/ { print $2; exit }' <<<"${signature_details}")"
signing_authority="$(awk -F= '/^Authority=/ { print $2; exit }' <<<"${signature_details}")"
signing_team_id="${signing_team_id:-none}"
signing_authority="${signing_authority:-adhoc}"
cat > "${package_directory}/BUILD-MANIFEST.txt" <<EOF
Chat2DB Rust macOS test package
version=${version}
Expand All @@ -175,7 +332,10 @@ git_commit=${git_commit}
community_commit=${community_commit}
app_size_kib=${app_kib}
signing_identity=${signing_identity}
distribution_status=internal-test-only
signing_authority=${signing_authority}
signing_team_id=${signing_team_id}
notarization_status=${notarization_status}
distribution_status=${distribution_status}
EOF

echo "Built self-contained macOS app: ${app_path}"
Expand Down
35 changes: 35 additions & 0 deletions scripts/cleanup-macos-signing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail

keychain_path="${CHAT2DB_SIGNING_KEYCHAIN:-}"
if [[ -z "${keychain_path}" ]]; then
exit 0
fi

if [[ -z "${RUNNER_TEMP:-}" || "${RUNNER_TEMP}" != /* || ! -d "${RUNNER_TEMP}" || -L "${RUNNER_TEMP}" ]]; then
echo "refusing to clean a signing keychain without a safe runner temp directory" >&2
exit 1
fi
case "${keychain_path}" in
"${RUNNER_TEMP}"/chat2db-signing-*.keychain-db) ;;
*)
echo "refusing to clean unexpected signing keychain: ${keychain_path}" >&2
exit 1
;;
esac

remaining_keychains=()
while IFS= read -r existing_keychain; do
existing_keychain="${existing_keychain//\"/}"
existing_keychain="${existing_keychain#"${existing_keychain%%[![:space:]]*}"}"
existing_keychain="${existing_keychain%"${existing_keychain##*[![:space:]]}"}"
if [[ -n "${existing_keychain}" && "${existing_keychain}" != "${keychain_path}" ]]; then
remaining_keychains+=("${existing_keychain}")
fi
done < <(security list-keychains -d user)
if [[ "${#remaining_keychains[@]}" -gt 0 ]]; then
security list-keychains -d user -s "${remaining_keychains[@]}"
fi
security delete-keychain "${keychain_path}" >/dev/null 2>&1 || true
rm -f -- "${keychain_path}"
echo "Removed ephemeral macOS signing keychain"
4 changes: 2 additions & 2 deletions scripts/community-frontend.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"repository": "https://github.com/OtterMind/Chat2DB.git",
"submodulePath": "third_party/chat2db-community",
"sourcePath": "chat2db-community-client",
"commit": "1c650f0e8a61d80b6b570e2cdcfc9c1b01f2a4e4",
"tree": "b08643418e5dd71a1ba4c955f16278a96e604939",
"commit": "390109c5bcf2fd249617fe62733ead43ccf85611",
"tree": "91d424a75687cf11ff5c4bda1297f6c2d7cd0a40",
"packageManager": "yarn@1.22.22"
}
5 changes: 5 additions & 0 deletions scripts/community-frontend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ function test() {
runYarn(['test:ai-model-select'], worktree);
runYarn(['test:export-connections'], worktree);
runYarn(['test:host-file-transfer'], worktree);
runYarn(['test:canvas-lifecycle'], worktree);
runYarn(['test:result-error-boundary'], worktree);
runYarn(['test:result-resource-activity'], worktree);
runYarn(['test:result-table-lifecycle'], worktree);
runYarn(['test:workspace-resource-activity'], worktree);
}

function build() {
Expand Down
Loading
Loading