Skip to repository content1475 lines · 62.4 KB · text
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T00:31:07.820Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
bundle-omega-rc
1#!/usr/bin/env bash
2# Owned Omega RC macOS package entry point (OMEGA-BRAND-05).
3#
4# Produces (or dry-runs the contract for):
5# Omega-v0.2.0-rc3-macos-arm64.dmg
6#
7# Does not claim notarization success. Notarization runs only when owner
8# notarization secrets are present; otherwise the release record records that
9# notarization was not attempted.
10
11set -euo pipefail
12
13ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14cd "$ROOT"
15
16readonly OMEGA_RC_VERSION="0.2.0-rc18"
17readonly OMEGA_RC_SEMVER="0.2.0"
18readonly OMEGA_RC_CHANNEL="preview"
19readonly OMEGA_RC_DISPLAY_CHANNEL="rc"
20readonly OMEGA_TEAM_ID="HQWSG26L43"
21readonly OMEGA_SIGNING_IDENTITY="Developer ID Application: OpenAgents, Inc. (${OMEGA_TEAM_ID})"
22readonly OMEGA_BUNDLE_ID="com.openagents.omega.rc"
23readonly OMEGA_VOLUME_NAME="Omega RC"
24readonly OMEGA_ARTIFACT_NAME="Omega-v${OMEGA_RC_VERSION}-macos-arm64.dmg"
25readonly OMEGA_RELEASE_RECORD_SCHEMA_VERSION=1
26readonly OMEGA_BUILD_LOCK_SNAPSHOT_NAME="Cargo.lock.omega-v${OMEGA_RC_VERSION}"
27readonly OMEGA_SOURCE_VERIFIER_PATH="script/verify-omega-identity"
28readonly OMEGA_IDENTITY_BOUNDARY_LIST="BUZZ_PRIVATE_KEY,identity.key,get_nsec"
29readonly OMEGA_BRAND_VERIFIER_PATH="script/verify-omega-brand"
30readonly OMEGA_BRAND_POLICY_PATH="script/omega-brand-gate.json"
31readonly OMEGA_EFFECTD_RELEASE_TAG="omega-effectd-v0.1.0-rc.10"
32readonly OMEGA_EFFECTD_ASSET_NAME="omega-effectd-v0.1.0-macos-arm64.tar.gz"
33readonly OMEGA_EFFECTD_ASSET_SHA256="e3ab92ffe0baa0cb3082ea4bc83720d1677b633f3f0725b7c7571930ae19f7ac"
34readonly OMEGA_EFFECTD_MANIFEST_ASSET_SHA256="d797f749f57d2ab88cdad74f5ff81e882bc6052951a026c028a84fb7b022b5eb"
35readonly OMEGA_EFFECTD_SOURCE_COMMIT="e3280b4795efe189474e12b4c6cd06123cfda359"
36readonly OMEGA_EFFECTD_SOURCE_TREE="af28ecd0a57523640a96b6f3ed7411f07a7da228"
37readonly OMEGA_EFFECTD_DOWNLOAD_URL="https://github.com/OpenAgentsInc/openagents/releases/download/${OMEGA_EFFECTD_RELEASE_TAG}/${OMEGA_EFFECTD_ASSET_NAME}"
38
39DRY_RUN=false
40SKIP_BUILD=false
41SELF_TEST_NOTARIZATION_KEY=false
42TARGET_TRIPLE="aarch64-apple-darwin"
43OUTPUT_DIR="${ROOT}/target/omega-rc"
44RELEASE_RECORD_PATH=""
45BUILD_LOCK_SNAPSHOT_PATH=""
46BUILD_LOCK_SNAPSHOT_NAME=""
47BUILD_LOCK_SNAPSHOT_DIGEST=""
48OMEGA_BINARY_DIGEST=""
49CLI_BINARY_DIGEST=""
50IDENTITY_PROOF_BINARY_DIGEST=""
51SOURCE_VERIFIER_DIGEST=""
52SOURCE_VERIFIER_PASSED=false
53PACKAGED_BOUNDARY_SCAN_PASSED=false
54BRAND_VERIFIER_DIGEST=""
55BRAND_POLICY_DIGEST=""
56BRAND_SOURCE_SCAN_PASSED=false
57BRAND_PACKAGED_SCAN_PASSED=false
58OMEGA_EFFECTD_ARCHIVE_PATH="${ROOT}/target/omega-effectd-cache/${OMEGA_EFFECTD_RELEASE_TAG}/${OMEGA_EFFECTD_ASSET_NAME}"
59OMEGA_EFFECTD_MANIFEST_SHA256=""
60OMEGA_EFFECTD_WRAPPER_SHA256=""
61OMEGA_EFFECTD_BUNDLE_SHA256=""
62OMEGA_EFFECTD_SOURCE_NODE_SHA256=""
63OMEGA_EFFECTD_PACKAGED_NODE_SHA256=""
64NOTARIZATION_KEY_FILE=""
65NOTARIZATION_KEY_FILE_IS_TEMPORARY=false
66PACKAGED_BINDINGS_JSON=""
67
68usage() {
69 cat <<EOF
70Usage: ${0##*/} [--dry-run] [--skip-build] [--output-dir DIR] [--release-record PATH]
71 [--self-test-notarization-key]
72
73Owned Omega RC packaging for macOS arm64.
74
75Options:
76 --dry-run Validate prerequisites and release-record schema without
77 compiling, signing, or notarizing.
78 --skip-build Reuse an existing DMG at the expected output path and
79 validate its existing release record without modifying
80 or relabeling either file.
81 --output-dir DIR Directory for the DMG and release record
82 (default: target/omega-rc).
83 --release-record PATH Explicit release-record JSON path.
84 --self-test-notarization-key
85 Verify path and inline PEM input normalization without
86 compiling, signing, or notarizing.
87 -h, --help Show this help.
88
89Environment (owner secrets; never commit):
90 MACOS_CERTIFICATE / MACOS_CERTIFICATE_PASSWORD
91 Optional keychain import path (same shape as script/bundle-mac).
92 APPLE_NOTARIZATION_KEY / APPLE_NOTARIZATION_KEY_ID / APPLE_NOTARIZATION_ISSUER_ID
93 Required only when notarization should be attempted after signing. The
94 key may be inline PEM contents or a path to a readable PEM file.
95EOF
96}
97
98while [[ $# -gt 0 ]]; do
99 case "$1" in
100 --dry-run) DRY_RUN=true ;;
101 --skip-build) SKIP_BUILD=true ;;
102 --output-dir)
103 OUTPUT_DIR="$2"
104 shift
105 ;;
106 --release-record)
107 RELEASE_RECORD_PATH="$2"
108 shift
109 ;;
110 --self-test-notarization-key) SELF_TEST_NOTARIZATION_KEY=true ;;
111 -h|--help)
112 usage
113 exit 0
114 ;;
115 *)
116 echo "error: unknown argument: $1" >&2
117 usage >&2
118 exit 2
119 ;;
120 esac
121 shift
122done
123
124if [[ "${DRY_RUN}" == "true" && "${SKIP_BUILD}" == "true" ]]; then
125 echo "error: --dry-run and --skip-build are mutually exclusive." >&2
126 exit 2
127fi
128
129if [[ "${SELF_TEST_NOTARIZATION_KEY}" == "true" && ( "${DRY_RUN}" == "true" || "${SKIP_BUILD}" == "true" ) ]]; then
130 echo "error: --self-test-notarization-key cannot be combined with build modes." >&2
131 exit 2
132fi
133
134mkdir -p "${OUTPUT_DIR}"
135BUILD_LOCK_SNAPSHOT_PATH="${OUTPUT_DIR}/${OMEGA_BUILD_LOCK_SNAPSHOT_NAME}"
136if [[ -z "${RELEASE_RECORD_PATH}" ]]; then
137 RELEASE_RECORD_PATH="${OUTPUT_DIR}/omega-v${OMEGA_RC_VERSION}-macos-arm64.release.json"
138fi
139
140sha256_file() {
141 local path="$1"
142 if [[ ! -f "${path}" ]]; then
143 echo ""
144 return 0
145 fi
146 shasum -a 256 "${path}" | awk '{print $1}'
147}
148
149prepare_notarization_key_file() {
150 local key_input="$1"
151
152 NOTARIZATION_KEY_FILE=""
153 NOTARIZATION_KEY_FILE_IS_TEMPORARY=false
154
155 if [[ -f "${key_input}" && -r "${key_input}" ]]; then
156 NOTARIZATION_KEY_FILE="${key_input}"
157 return 0
158 fi
159
160 if [[ "${key_input}" != *"-----BEGIN PRIVATE KEY-----"* ]]; then
161 echo "error: APPLE_NOTARIZATION_KEY must be inline PEM contents or a readable file path." >&2
162 return 1
163 fi
164
165 NOTARIZATION_KEY_FILE="$(mktemp)"
166 NOTARIZATION_KEY_FILE_IS_TEMPORARY=true
167 chmod 600 "${NOTARIZATION_KEY_FILE}"
168 printf '%s\n' "${key_input}" > "${NOTARIZATION_KEY_FILE}"
169}
170
171cleanup_notarization_key_file() {
172 if [[ "${NOTARIZATION_KEY_FILE_IS_TEMPORARY}" == "true" && -n "${NOTARIZATION_KEY_FILE}" ]]; then
173 rm -f "${NOTARIZATION_KEY_FILE}"
174 fi
175 NOTARIZATION_KEY_FILE=""
176 NOTARIZATION_KEY_FILE_IS_TEMPORARY=false
177}
178
179self_test_notarization_key() {
180 local test_directory
181 local path_key
182 local inline_key
183 local expected_digest
184 local observed_digest
185
186 test_directory="$(mktemp -d)"
187 path_key="${test_directory}/AuthKey_TEST.p8"
188 inline_key=$'-----BEGIN PRIVATE KEY-----\ndeterministic-test-value\n-----END PRIVATE KEY-----'
189 printf '%s\n' "${inline_key}" > "${path_key}"
190
191 prepare_notarization_key_file "${path_key}"
192 if [[ "${NOTARIZATION_KEY_FILE}" != "${path_key}" || "${NOTARIZATION_KEY_FILE_IS_TEMPORARY}" != "false" ]]; then
193 echo "error: notarization key path normalization self-test failed." >&2
194 rm -rf "${test_directory}"
195 return 1
196 fi
197
198 prepare_notarization_key_file "${inline_key}"
199 expected_digest="$(printf '%s\n' "${inline_key}" | shasum -a 256 | awk '{print $1}')"
200 observed_digest="$(sha256_file "${NOTARIZATION_KEY_FILE}")"
201 if [[ "${expected_digest}" != "${observed_digest}" || "${NOTARIZATION_KEY_FILE_IS_TEMPORARY}" != "true" ]]; then
202 echo "error: inline notarization key normalization self-test failed." >&2
203 cleanup_notarization_key_file
204 rm -rf "${test_directory}"
205 return 1
206 fi
207
208 cleanup_notarization_key_file
209 rm -rf "${test_directory}"
210 echo "notarization key normalization self-test passed."
211}
212
213prepare_omega_effectd_archive() {
214 mkdir -p "$(dirname "${OMEGA_EFFECTD_ARCHIVE_PATH}")"
215 if [[ ! -f "${OMEGA_EFFECTD_ARCHIVE_PATH}" ]]; then
216 local download_path="${OMEGA_EFFECTD_ARCHIVE_PATH}.download"
217 curl --fail --location --silent --show-error \
218 "${OMEGA_EFFECTD_DOWNLOAD_URL}" --output "${download_path}"
219 mv "${download_path}" "${OMEGA_EFFECTD_ARCHIVE_PATH}"
220 fi
221
222 local archive_digest
223 archive_digest="$(sha256_file "${OMEGA_EFFECTD_ARCHIVE_PATH}")"
224 if [[ "${archive_digest}" != "${OMEGA_EFFECTD_ASSET_SHA256}" ]]; then
225 echo "error: cached omega-effectd archive digest does not match the pinned release asset." >&2
226 echo " expected: ${OMEGA_EFFECTD_ASSET_SHA256}" >&2
227 echo " observed: ${archive_digest}" >&2
228 exit 1
229 fi
230
231 local component_json
232 component_json="$(python3 - "${OMEGA_EFFECTD_ARCHIVE_PATH}" "${OMEGA_EFFECTD_SOURCE_COMMIT}" "${OMEGA_EFFECTD_SOURCE_TREE}" <<'PY'
233import hashlib
234import json
235import pathlib
236import sys
237import tarfile
238
239archive_path = pathlib.Path(sys.argv[1])
240expected_source_commit = sys.argv[2]
241expected_source_tree = sys.argv[3]
242expected_files = {
243 "omega-effectd/bin/omega-effectd",
244 "omega-effectd/component-manifest.json",
245 "omega-effectd/dist/omega-effectd.mjs",
246 "omega-effectd/licenses/Node-LICENSE.txt",
247 "omega-effectd/licenses/OpenAgents-Apache-2.0.txt",
248 "omega-effectd/licenses/THIRD_PARTY_LICENSES.txt",
249 "omega-effectd/licenses/THIRD_PARTY_NOTICES.json",
250 "omega-effectd/runtime/bin/node",
251}
252
253with tarfile.open(archive_path, "r:gz") as archive:
254 members = archive.getmembers()
255 names = [member.name for member in members]
256 if len(names) != len(set(names)):
257 raise SystemExit("omega-effectd archive contains duplicate paths")
258 for member in members:
259 path = pathlib.PurePosixPath(member.name)
260 if path.is_absolute() or ".." in path.parts:
261 raise SystemExit(f"omega-effectd archive contains unsafe path: {member.name}")
262 if not (member.isdir() or member.isfile()):
263 raise SystemExit(f"omega-effectd archive contains unsupported entry: {member.name}")
264 regular_files = {member.name for member in members if member.isfile()}
265 if regular_files != expected_files:
266 missing = sorted(expected_files - regular_files)
267 unexpected = sorted(regular_files - expected_files)
268 raise SystemExit(
269 f"omega-effectd archive inventory mismatch; missing={missing}, unexpected={unexpected}"
270 )
271
272 def bytes_for(name: str) -> bytes:
273 extracted = archive.extractfile(name)
274 if extracted is None:
275 raise SystemExit(f"omega-effectd archive entry is unreadable: {name}")
276 return extracted.read()
277
278 manifest_bytes = bytes_for("omega-effectd/component-manifest.json")
279 manifest = json.loads(manifest_bytes)
280 expected_manifest = {
281 "schema": "openagents.omega.effectd.component.v1",
282 "package": "@openagentsinc/omega-effectd",
283 "serviceVersion": "0.1.0",
284 "protocol": "openagents.omega.effectd.v1",
285 "platform": "darwin-arm64",
286 }
287 for key, expected in expected_manifest.items():
288 if manifest.get(key) != expected:
289 raise SystemExit(f"omega-effectd manifest {key} does not match the pin")
290 if manifest.get("source", {}).get("commit") != expected_source_commit:
291 raise SystemExit("omega-effectd manifest source commit does not match the pin")
292 if manifest.get("source", {}).get("tree") != expected_source_tree:
293 raise SystemExit("omega-effectd manifest source tree does not match the pin")
294
295 for relative, expected_digest in manifest.get("files", {}).items():
296 name = f"omega-effectd/{relative}"
297 if name not in regular_files:
298 raise SystemExit(f"omega-effectd manifest names missing file: {relative}")
299 observed = hashlib.sha256(bytes_for(name)).hexdigest()
300 if observed != expected_digest:
301 raise SystemExit(f"omega-effectd manifest digest mismatch: {relative}")
302
303 node_bytes = bytes_for("omega-effectd/runtime/bin/node")
304 source_node_digest = hashlib.sha256(node_bytes).hexdigest()
305 if source_node_digest != manifest.get("node", {}).get("binarySha256"):
306 raise SystemExit("omega-effectd Node binary digest does not match the manifest")
307 if manifest.get("node", {}).get("version") != "24.13.1":
308 raise SystemExit("omega-effectd Node version does not match the pin")
309
310 print(json.dumps({
311 "manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
312 "wrapper_sha256": hashlib.sha256(bytes_for("omega-effectd/bin/omega-effectd")).hexdigest(),
313 "bundle_sha256": hashlib.sha256(bytes_for("omega-effectd/dist/omega-effectd.mjs")).hexdigest(),
314 "source_node_sha256": source_node_digest,
315 }))
316PY
317)"
318
319 OMEGA_EFFECTD_MANIFEST_SHA256="$(jq -r '.manifest_sha256' <<<"${component_json}")"
320 OMEGA_EFFECTD_WRAPPER_SHA256="$(jq -r '.wrapper_sha256' <<<"${component_json}")"
321 OMEGA_EFFECTD_BUNDLE_SHA256="$(jq -r '.bundle_sha256' <<<"${component_json}")"
322 OMEGA_EFFECTD_SOURCE_NODE_SHA256="$(jq -r '.source_node_sha256' <<<"${component_json}")"
323 if [[ "${OMEGA_EFFECTD_MANIFEST_SHA256}" != "${OMEGA_EFFECTD_MANIFEST_ASSET_SHA256}" ]]; then
324 echo "error: omega-effectd component manifest digest does not match the pinned release asset." >&2
325 exit 1
326 fi
327}
328
329stage_omega_effectd_component() {
330 local app_path="$1"
331 local staging_directory
332 staging_directory="$(mktemp -d)"
333 tar -xzf "${OMEGA_EFFECTD_ARCHIVE_PATH}" -C "${staging_directory}"
334 local component_source="${staging_directory}/omega-effectd"
335 local component_target="${app_path}/Contents/Resources/omega-effectd"
336 if [[ ! -x "${component_source}/bin/omega-effectd" || ! -x "${component_source}/runtime/bin/node" ]]; then
337 echo "error: omega-effectd archive executables are not executable." >&2
338 rm -rf "${staging_directory}"
339 exit 1
340 fi
341 rm -rf "${component_target}"
342 ditto "${component_source}" "${component_target}"
343 rm -rf "${staging_directory}"
344
345 if [[ "$(sha256_file "${component_target}/component-manifest.json")" != "${OMEGA_EFFECTD_MANIFEST_SHA256}" || \
346 "$(sha256_file "${component_target}/bin/omega-effectd")" != "${OMEGA_EFFECTD_WRAPPER_SHA256}" || \
347 "$(sha256_file "${component_target}/dist/omega-effectd.mjs")" != "${OMEGA_EFFECTD_BUNDLE_SHA256}" || \
348 "$(sha256_file "${component_target}/runtime/bin/node")" != "${OMEGA_EFFECTD_SOURCE_NODE_SHA256}" ]]; then
349 echo "error: staged omega-effectd component differs from the verified release archive." >&2
350 exit 1
351 fi
352}
353
354require_clean_checkout() {
355 if [[ -n "$(git status --porcelain)" ]]; then
356 echo "error: working tree is dirty; refuse to build an Omega RC candidate from an unrecorded tree." >&2
357 git status --short >&2
358 exit 1
359 fi
360}
361
362require_arm64_host_or_target() {
363 if [[ "${TARGET_TRIPLE}" != "aarch64-apple-darwin" ]]; then
364 echo "error: this RC command only produces macos-arm64 artifacts (got ${TARGET_TRIPLE})." >&2
365 exit 1
366 fi
367}
368
369find_openagents_signing_identity() {
370 local identities
371 identities="$(security find-identity -v -p codesigning 2>/dev/null || true)"
372 if ! grep -Fq "${OMEGA_SIGNING_IDENTITY}" <<<"${identities}"; then
373 echo "error: required OpenAgents signing identity is missing:" >&2
374 echo " ${OMEGA_SIGNING_IDENTITY}" >&2
375 echo "Team ID ${OMEGA_TEAM_ID} must be available in the login keychain before an RC package can be signed." >&2
376 echo "Available codesigning identities:" >&2
377 echo "${identities:- (none)}" >&2
378 exit 1
379 fi
380}
381
382verify_packaged_identity_boundaries() {
383 local app_path="$1"
384 local forbidden_literal
385 local matches
386 local -a forbidden_literals=("BUZZ_PRIVATE_KEY" "identity.key" "get_nsec")
387 for forbidden_literal in "${forbidden_literals[@]}"; do
388 matches="$(LC_ALL=C grep -aRFl -- "${forbidden_literal}" "${app_path}" || true)"
389 if [[ -n "${matches}" ]]; then
390 echo "error: packaged Omega contains forbidden identity boundary ${forbidden_literal}:" >&2
391 echo "${matches}" >&2
392 exit 1
393 fi
394 done
395 PACKAGED_BOUNDARY_SCAN_PASSED=true
396}
397
398verify_source_identity_boundaries() {
399 "${OMEGA_SOURCE_VERIFIER_PATH}"
400 SOURCE_VERIFIER_DIGEST="$(sha256_file "${OMEGA_SOURCE_VERIFIER_PATH}")"
401 if [[ -z "${SOURCE_VERIFIER_DIGEST}" ]]; then
402 echo "error: source identity verifier digest is unavailable." >&2
403 exit 1
404 fi
405 SOURCE_VERIFIER_PASSED=true
406}
407
408# OMEGA-DELTA-0017 and OMEGA-DELTA-0018, source side.
409verify_source_brand() {
410 "${OMEGA_BRAND_VERIFIER_PATH}"
411 BRAND_VERIFIER_DIGEST="$(sha256_file "${OMEGA_BRAND_VERIFIER_PATH}")"
412 BRAND_POLICY_DIGEST="$(sha256_file "${OMEGA_BRAND_POLICY_PATH}")"
413 if [[ -z "${BRAND_VERIFIER_DIGEST}" || -z "${BRAND_POLICY_DIGEST}" ]]; then
414 echo "error: brand gate digests are unavailable." >&2
415 exit 1
416 fi
417 BRAND_SOURCE_SCAN_PASSED=true
418}
419
420# OMEGA-DELTA-0017 and OMEGA-DELTA-0018, packaged side.
421#
422# 0.2.0-rc10 shipped thirteen Zed strings in its signed Info.plist and Zed logo
423# marks on its status bar while every brand check on omega#16 passed, because
424# those checks read the executable for three identity literals. Nothing had
425# ever read Info.plist, and no string scan can see a logo. This runs against
426# the built bundle, before signing, so a packaging regression fails here rather
427# than in a macOS permission dialog on the owner's machine.
428verify_packaged_brand() {
429 local app_path="$1"
430 "${OMEGA_BRAND_VERIFIER_PATH}" --app "${app_path}"
431 BRAND_PACKAGED_SCAN_PASSED=true
432}
433
434validate_release_record_schema() {
435 local path="$1"
436 python3 - \
437 "${path}" \
438 "${OMEGA_RELEASE_RECORD_SCHEMA_VERSION}" \
439 "${OMEGA_RC_VERSION}" \
440 "${OMEGA_ARTIFACT_NAME}" \
441 "${OMEGA_BUILD_LOCK_SNAPSHOT_NAME}" <<'PY'
442import json
443import re
444import sys
445
446path = sys.argv[1]
447expected_schema = int(sys.argv[2])
448expected_version, expected_artifact, expected_build_lock = sys.argv[3:]
449with open(path, encoding="utf-8") as handle:
450 record = json.load(handle)
451
452def valid_sha256(value):
453 return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None
454
455required = [
456 "schema_version",
457 "product",
458 "channel",
459 "version",
460 "bundle_version",
461 "artifact_name",
462 "volume_name",
463 "bundle_identifier",
464 "team_id",
465 "signing_identity",
466 "source",
467 "toolchains",
468 "execution",
469 "build_inputs",
470 "components",
471 "digests",
472 "identity_boundaries",
473 "brand_gate",
474 "notarization",
475 "legal",
476 "publication",
477]
478missing = [key for key in required if key not in record]
479if missing:
480 raise SystemExit(f"release-record missing keys: {', '.join(missing)}")
481
482if record["schema_version"] != expected_schema:
483 raise SystemExit(
484 f"schema_version {record['schema_version']} != expected {expected_schema}"
485 )
486if record["product"] != "Omega":
487 raise SystemExit("product must be Omega")
488if record["channel"] != "rc":
489 raise SystemExit("channel must be rc")
490if record["version"] != expected_version:
491 raise SystemExit(f"version must be {expected_version}")
492if record["artifact_name"] != expected_artifact:
493 raise SystemExit(f"artifact_name must be {expected_artifact}")
494if record["bundle_version"] != "0.2.0":
495 raise SystemExit("bundle_version must be 0.2.0")
496if record["publication"].get("repository") != "OpenAgentsInc/omega":
497 raise SystemExit("publication.repository must be OpenAgentsInc/omega")
498if record["publication"].get("tag") != f"v{expected_version}":
499 raise SystemExit(f"publication.tag must be v{expected_version}")
500if record["publication"].get("prerelease") is not True:
501 raise SystemExit("publication.prerelease must be true")
502if record["publication"].get("latest") is not False:
503 raise SystemExit("publication.latest must be false")
504if record["legal"].get("commercial_terms_attached") is not False:
505 raise SystemExit("legal.commercial_terms_attached must be false")
506legal_notices = record["legal"].get("notices")
507if not isinstance(legal_notices, dict) or not legal_notices:
508 raise SystemExit("legal.notices must bind packaged paths to SHA-256 digests")
509if not all(
510 isinstance(name, str) and name and valid_sha256(digest)
511 for name, digest in legal_notices.items()
512):
513 raise SystemExit("every legal notice must have a path and SHA-256 digest")
514if "commit" not in record["source"]:
515 raise SystemExit("source.commit is required")
516digests = record["digests"]
517for key in ("cargo_lock_sha256", "icon_family_manifest_sha256"):
518 if key not in digests or not valid_sha256(digests[key]):
519 raise SystemExit(f"digests.{key} must be a SHA-256 digest")
520if "package_sha256" not in digests:
521 raise SystemExit("digests.package_sha256 is required (null when absent)")
522for key in ("omega_binary_sha256", "cli_binary_sha256"):
523 if key not in digests:
524 raise SystemExit(f"digests.{key} is required (null when absent)")
525packaged_icons = digests.get("packaged_icon_outputs")
526if not isinstance(packaged_icons, dict) or set(packaged_icons) != {
527 "Contents/Resources/openagents-icon.icns",
528 "Contents/Resources/Document.icns",
529}:
530 raise SystemExit("digests.packaged_icon_outputs has the wrong inventory")
531if not all(valid_sha256(digest) for digest in packaged_icons.values()):
532 raise SystemExit("every packaged icon output must have a SHA-256 digest")
533
534execution = record["execution"]
535if not isinstance(execution.get("dry_run"), bool):
536 raise SystemExit("execution.dry_run must be a boolean")
537
538owner_blockers = record.get("owner_blockers")
539if not isinstance(owner_blockers, list) or not all(
540 isinstance(blocker, str) and blocker for blocker in owner_blockers
541):
542 raise SystemExit("owner_blockers must be a list of non-empty strings")
543notarization = record["notarization"]
544# OMEGA-DELTA-0023. A stapled disk image whose application carries no ticket is
545# the 0.2.0-rc11 shape: Gatekeeper acceptance of the installed product would
546# rest on an online lookup and offline first start could not be proven.
547if "app_stapled" not in notarization:
548 raise SystemExit("notarization.app_stapled is required")
549if notarization.get("stapled") is True and notarization.get("app_stapled") is not True:
550 raise SystemExit(
551 "the disk image is stapled but Omega.app is not; offline first start "
552 "cannot be proven from this candidate"
553 )
554if notarization.get("stapled") is True and any(
555 "notarization" in blocker.lower() for blocker in owner_blockers
556):
557 raise SystemExit("stapled releases must not retain a notarization blocker")
558if execution["dry_run"] is False and any(
559 "developer id" in blocker.lower() for blocker in owner_blockers
560):
561 raise SystemExit("built releases must not retain a Developer ID blocker")
562if not any("publication" in blocker.lower() for blocker in owner_blockers):
563 raise SystemExit("release record must retain the external publication blocker")
564
565build_lock = record["build_inputs"].get("cargo_lock_snapshot", {})
566for key in ("name", "sha256"):
567 if key not in build_lock:
568 raise SystemExit(f"build_inputs.cargo_lock_snapshot.{key} is required")
569
570effectd = record["components"].get("omega_effectd", {})
571expected_effectd = {
572 "package": "@openagentsinc/omega-effectd",
573 "service_version": "0.1.0",
574 "protocol": "openagents.omega.effectd.v1",
575 "platform": "darwin-arm64",
576 "release_tag": "omega-effectd-v0.1.0-rc.10",
577 "asset_name": "omega-effectd-v0.1.0-macos-arm64.tar.gz",
578 "archive_sha256": "e3ab92ffe0baa0cb3082ea4bc83720d1677b633f3f0725b7c7571930ae19f7ac",
579 "manifest_sha256": "d797f749f57d2ab88cdad74f5ff81e882bc6052951a026c028a84fb7b022b5eb",
580 "source_commit": "e3280b4795efe189474e12b4c6cd06123cfda359",
581 "source_tree": "af28ecd0a57523640a96b6f3ed7411f07a7da228",
582}
583for key, expected in expected_effectd.items():
584 if effectd.get(key) != expected:
585 raise SystemExit(f"components.omega_effectd.{key} does not match the pin")
586for key in ("wrapper_sha256", "bundle_sha256", "source_node_sha256"):
587 if not valid_sha256(effectd.get(key)):
588 raise SystemExit(f"components.omega_effectd.{key} must be a SHA-256 digest")
589if effectd.get("packaged_node_sha256") is not None and not valid_sha256(effectd["packaged_node_sha256"]):
590 raise SystemExit("components.omega_effectd.packaged_node_sha256 must be null or a SHA-256 digest")
591
592identity_proof = record["components"].get("identity_proof_driver", {})
593expected_identity_proof = {
594 "path": "Contents/MacOS/omega-identity-proof",
595 "protocol": "openagents.omega.identity-proof.v1",
596 "keyring_service": "com.openagents.omega.identity-proof.v1",
597 "keyring_account": "disposable-proof-only",
598}
599for key, expected in expected_identity_proof.items():
600 if identity_proof.get(key) != expected:
601 raise SystemExit(f"components.identity_proof_driver.{key} is invalid")
602if identity_proof.get("binary_sha256") is not None and not valid_sha256(identity_proof["binary_sha256"]):
603 raise SystemExit("components.identity_proof_driver.binary_sha256 must be null or a SHA-256 digest")
604
605boundaries = record["identity_boundaries"]
606source_verifier = boundaries.get("source_verifier", {})
607if source_verifier.get("path") != "script/verify-omega-identity":
608 raise SystemExit("identity_boundaries.source_verifier.path is invalid")
609if source_verifier.get("passed") is not True:
610 raise SystemExit("identity_boundaries.source_verifier.passed must be true")
611if not valid_sha256(source_verifier.get("sha256")):
612 raise SystemExit("identity_boundaries.source_verifier.sha256 must be a SHA-256 digest")
613packaged_scan = boundaries.get("packaged_scan", {})
614if packaged_scan.get("forbidden_literals") != [
615 "BUZZ_PRIVATE_KEY",
616 "identity.key",
617 "get_nsec",
618]:
619 raise SystemExit("identity_boundaries.packaged_scan.forbidden_literals is invalid")
620
621# OMEGA-DELTA-0017 and OMEGA-DELTA-0018. A release record that cannot say the
622# brand gate ran is not evidence that it ran.
623brand = record["brand_gate"]
624brand_source = brand.get("source_verifier", {})
625if brand_source.get("path") != "script/verify-omega-brand":
626 raise SystemExit("brand_gate.source_verifier.path is invalid")
627if brand_source.get("passed") is not True:
628 raise SystemExit("brand_gate.source_verifier.passed must be true")
629if not valid_sha256(brand_source.get("sha256")):
630 raise SystemExit("brand_gate.source_verifier.sha256 must be a SHA-256 digest")
631brand_policy = brand.get("policy", {})
632if brand_policy.get("path") != "script/omega-brand-gate.json":
633 raise SystemExit("brand_gate.policy.path is invalid")
634if not valid_sha256(brand_policy.get("sha256")):
635 raise SystemExit("brand_gate.policy.sha256 must be a SHA-256 digest")
636brand_packaged = brand.get("packaged_scan", {})
637if brand_packaged.get("surfaces") != ["Contents/Info.plist", "embedded icon assets"]:
638 raise SystemExit("brand_gate.packaged_scan.surfaces is invalid")
639
640if execution["dry_run"]:
641 if any(build_lock.get(key) is not None for key in ("name", "sha256")):
642 raise SystemExit("dry-run must not claim a build Cargo.lock snapshot")
643 if any(
644 digests.get(key) is not None
645 for key in ("package_sha256", "omega_binary_sha256", "cli_binary_sha256")
646 ):
647 raise SystemExit("dry-run must not claim package or binary digests")
648 if packaged_scan.get("passed") is not False:
649 raise SystemExit("dry-run packaged boundary scan must be false")
650 if brand_packaged.get("passed") is not False:
651 raise SystemExit("dry-run packaged brand scan must be false")
652 if effectd.get("packaged_node_sha256") is not None:
653 raise SystemExit("dry-run must not claim a signed packaged omega-effectd Node digest")
654 if identity_proof.get("binary_sha256") is not None:
655 raise SystemExit("dry-run must not claim an identity proof driver digest")
656else:
657 if set(legal_notices) != {"LICENSE-GPL", "LICENSE-APACHE", "licenses.md"}:
658 raise SystemExit("built release legal notice inventory is incomplete")
659 for key in ("name", "sha256"):
660 if not isinstance(build_lock.get(key), str) or not build_lock[key]:
661 raise SystemExit(f"build_inputs.cargo_lock_snapshot.{key} must be non-empty")
662 if build_lock["name"] != expected_build_lock:
663 raise SystemExit("build_inputs.cargo_lock_snapshot.name is invalid")
664 if not valid_sha256(build_lock["sha256"]):
665 raise SystemExit("build_inputs.cargo_lock_snapshot.sha256 must be a SHA-256 digest")
666 if digests["cargo_lock_sha256"] != build_lock["sha256"]:
667 raise SystemExit("build Cargo.lock snapshot digest must match cargo_lock_sha256")
668 for key in ("package_sha256", "omega_binary_sha256", "cli_binary_sha256"):
669 if not valid_sha256(digests.get(key)):
670 raise SystemExit(f"digests.{key} must be a SHA-256 digest")
671 if packaged_scan.get("passed") is not True:
672 raise SystemExit("built package boundary scan must pass")
673 if brand_packaged.get("passed") is not True:
674 raise SystemExit("built package brand scan must pass")
675 if not valid_sha256(effectd.get("packaged_node_sha256")):
676 raise SystemExit("built package must bind the signed omega-effectd Node digest")
677 if not valid_sha256(identity_proof.get("binary_sha256")):
678 raise SystemExit("built package must bind the signed identity proof driver digest")
679print(f"release-record schema ok: {path}")
680PY
681}
682
683collect_icon_digests_json() {
684 python3 - <<'PY'
685import json
686from pathlib import Path
687
688manifest_path = Path("crates/zed/resources/icon_family/manifest.json")
689manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
690outputs = {
691 relative: {"sha256": record["sha256"], "format": record["format"]}
692 for relative, record in manifest.get("outputs", {}).items()
693}
694print(json.dumps({
695 "manifest_sha256": __import__("hashlib").sha256(manifest_path.read_bytes()).hexdigest(),
696 "pinned_png_sha256": manifest["pinned_source"]["png_sha256"],
697 "pinned_icns_sha256": manifest["pinned_source"]["icns_sha256"],
698 "outputs": outputs,
699}))
700PY
701}
702
703collect_source_packaged_bindings_json() {
704 python3 - <<'PY'
705import hashlib
706import json
707from pathlib import Path
708
709legal = {}
710for source, packaged in (
711 (Path("LICENSE-GPL"), "LICENSE-GPL"),
712 (Path("LICENSE-APACHE"), "LICENSE-APACHE"),
713 (Path("assets/licenses.md"), "licenses.md"),
714):
715 if source.is_file():
716 legal[packaged] = hashlib.sha256(source.read_bytes()).hexdigest()
717
718icons = {}
719for source, packaged in (
720 (
721 Path("crates/zed/resources/icon_family/source/openagents-icon.icns"),
722 "Contents/Resources/openagents-icon.icns",
723 ),
724 (
725 Path("crates/zed/resources/Document.icns"),
726 "Contents/Resources/Document.icns",
727 ),
728):
729 if not source.is_file():
730 raise SystemExit(f"missing packaged icon source: {source}")
731 icons[packaged] = hashlib.sha256(source.read_bytes()).hexdigest()
732
733print(json.dumps({"legal_notices": legal, "icon_outputs": icons}, sort_keys=True))
734PY
735}
736
737collect_staged_packaged_bindings_json() {
738 local staged_root="$1"
739 python3 - "${staged_root}" <<'PY'
740import hashlib
741import json
742import plistlib
743import sys
744from pathlib import Path
745
746root = Path(sys.argv[1])
747app = root / "Omega.app"
748if not app.is_dir():
749 raise SystemExit(f"staged Omega app is missing: {app}")
750
751legal = {}
752for name in ("LICENSE-GPL", "LICENSE-APACHE", "licenses.md"):
753 path = root / name
754 if path.is_file():
755 legal[name] = hashlib.sha256(path.read_bytes()).hexdigest()
756if set(legal) != {"LICENSE-GPL", "LICENSE-APACHE", "licenses.md"}:
757 raise SystemExit("staged package legal notice inventory is incomplete")
758
759info = plistlib.loads((app / "Contents/Info.plist").read_bytes())
760icon_name = info.get("CFBundleIconFile")
761if not isinstance(icon_name, str) or not icon_name:
762 raise SystemExit("staged Omega Info.plist has no CFBundleIconFile")
763if not icon_name.endswith(".icns"):
764 icon_name += ".icns"
765icon_paths = [
766 Path("Contents/Resources") / icon_name,
767 Path("Contents/Resources/Document.icns"),
768]
769icons = {}
770for relative in icon_paths:
771 path = app / relative
772 if not path.is_file():
773 raise SystemExit(f"staged packaged icon is missing: {relative}")
774 icons[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
775
776print(json.dumps({"legal_notices": legal, "icon_outputs": icons}, sort_keys=True))
777PY
778}
779
780write_release_record() {
781 local package_path="$1"
782 local notarization_status="$2"
783 local notarization_attempted="$3"
784 local notarization_stapled="$4"
785 local app_stapled="$5"
786 local package_digest
787 if [[ "${DRY_RUN}" == "true" ]]; then
788 package_digest=""
789 else
790 package_digest="$(sha256_file "${package_path}")"
791 fi
792 if [[ -z "${package_digest}" ]]; then
793 package_digest="null"
794 fi
795
796 local commit upstream_commit upstream_remote rustc_version host_triple cargo_lock_digest icon_json packaged_bindings_json
797 commit="$(git rev-parse HEAD)"
798 # The upstream Zed remote is named `zed`. `upstream` is the retired name and
799 # is accepted only until every checkout has been renamed. This value is
800 # release provenance, so an absent remote must fail loudly rather than
801 # silently record a null upstream commit.
802 for upstream_remote in zed upstream; do
803 upstream_commit="$(git rev-parse "${upstream_remote}/main" 2>/dev/null || true)"
804 if [[ -z "${upstream_commit}" ]]; then
805 upstream_commit="$(git merge-base HEAD "${upstream_remote}/main" 2>/dev/null || true)"
806 fi
807 [[ -n "${upstream_commit}" ]] && break
808 done
809 if [[ -z "${upstream_commit}" ]]; then
810 echo "error: no upstream Zed remote found (tried zed/main and upstream/main)." >&2
811 echo " add it with: git remote add zed https://github.com/zed-industries/zed.git" >&2
812 echo " then: git remote set-url --push zed DISABLED && git fetch zed main" >&2
813 exit 1
814 fi
815 rustc_version="$(rustc --version)"
816 host_triple="$(rustc -vV | awk '/^host:/{print $2}')"
817 cargo_lock_digest="$(sha256_file Cargo.lock)"
818 icon_json="$(collect_icon_digests_json)"
819 packaged_bindings_json="${PACKAGED_BINDINGS_JSON:-$(collect_source_packaged_bindings_json)}"
820
821 OMEGA_RC_RECORD_OUT="${RELEASE_RECORD_PATH}" \
822 OMEGA_RC_SCHEMA_VERSION="${OMEGA_RELEASE_RECORD_SCHEMA_VERSION}" \
823 OMEGA_RC_VERSION_VALUE="${OMEGA_RC_VERSION}" \
824 OMEGA_RC_BUNDLE_VERSION_VALUE="${OMEGA_RC_SEMVER}" \
825 OMEGA_RC_ARTIFACT_NAME="${OMEGA_ARTIFACT_NAME}" \
826 OMEGA_RC_VOLUME_NAME="${OMEGA_VOLUME_NAME}" \
827 OMEGA_RC_BUNDLE_ID="${OMEGA_BUNDLE_ID}" \
828 OMEGA_RC_TEAM_ID="${OMEGA_TEAM_ID}" \
829 OMEGA_RC_SIGNING_IDENTITY="${OMEGA_SIGNING_IDENTITY}" \
830 OMEGA_RC_COMMIT="${commit}" \
831 OMEGA_RC_UPSTREAM_COMMIT="${upstream_commit}" \
832 OMEGA_RC_RUSTC="${rustc_version}" \
833 OMEGA_RC_HOST="${host_triple}" \
834 OMEGA_RC_CARGO_LOCK_DIGEST="${cargo_lock_digest}" \
835 OMEGA_RC_BUILD_LOCK_SNAPSHOT_NAME="${BUILD_LOCK_SNAPSHOT_NAME:-}" \
836 OMEGA_RC_BUILD_LOCK_SNAPSHOT_DIGEST="${BUILD_LOCK_SNAPSHOT_DIGEST}" \
837 OMEGA_RC_ICON_JSON="${icon_json}" \
838 OMEGA_RC_PACKAGED_BINDINGS_JSON="${packaged_bindings_json}" \
839 OMEGA_RC_PACKAGE_DIGEST="${package_digest}" \
840 OMEGA_RC_OMEGA_BINARY_DIGEST="${OMEGA_BINARY_DIGEST}" \
841 OMEGA_RC_CLI_BINARY_DIGEST="${CLI_BINARY_DIGEST}" \
842 OMEGA_RC_IDENTITY_PROOF_BINARY_DIGEST="${IDENTITY_PROOF_BINARY_DIGEST}" \
843 OMEGA_RC_EFFECTD_RELEASE_TAG="${OMEGA_EFFECTD_RELEASE_TAG}" \
844 OMEGA_RC_EFFECTD_ASSET_NAME="${OMEGA_EFFECTD_ASSET_NAME}" \
845 OMEGA_RC_EFFECTD_ARCHIVE_DIGEST="${OMEGA_EFFECTD_ASSET_SHA256}" \
846 OMEGA_RC_EFFECTD_SOURCE_COMMIT="${OMEGA_EFFECTD_SOURCE_COMMIT}" \
847 OMEGA_RC_EFFECTD_SOURCE_TREE="${OMEGA_EFFECTD_SOURCE_TREE}" \
848 OMEGA_RC_EFFECTD_MANIFEST_DIGEST="${OMEGA_EFFECTD_MANIFEST_SHA256}" \
849 OMEGA_RC_EFFECTD_WRAPPER_DIGEST="${OMEGA_EFFECTD_WRAPPER_SHA256}" \
850 OMEGA_RC_EFFECTD_BUNDLE_DIGEST="${OMEGA_EFFECTD_BUNDLE_SHA256}" \
851 OMEGA_RC_EFFECTD_SOURCE_NODE_DIGEST="${OMEGA_EFFECTD_SOURCE_NODE_SHA256}" \
852 OMEGA_RC_EFFECTD_PACKAGED_NODE_DIGEST="${OMEGA_EFFECTD_PACKAGED_NODE_SHA256}" \
853 OMEGA_RC_SOURCE_VERIFIER_PATH="${OMEGA_SOURCE_VERIFIER_PATH}" \
854 OMEGA_RC_SOURCE_VERIFIER_DIGEST="${SOURCE_VERIFIER_DIGEST}" \
855 OMEGA_RC_SOURCE_VERIFIER_PASSED="${SOURCE_VERIFIER_PASSED}" \
856 OMEGA_RC_PACKAGED_SCAN_PASSED="${PACKAGED_BOUNDARY_SCAN_PASSED}" \
857 OMEGA_RC_IDENTITY_BOUNDARIES="${OMEGA_IDENTITY_BOUNDARY_LIST}" \
858 OMEGA_RC_BRAND_VERIFIER_PATH="${OMEGA_BRAND_VERIFIER_PATH}" \
859 OMEGA_RC_BRAND_VERIFIER_DIGEST="${BRAND_VERIFIER_DIGEST}" \
860 OMEGA_RC_BRAND_POLICY_PATH="${OMEGA_BRAND_POLICY_PATH}" \
861 OMEGA_RC_BRAND_POLICY_DIGEST="${BRAND_POLICY_DIGEST}" \
862 OMEGA_RC_BRAND_SOURCE_PASSED="${BRAND_SOURCE_SCAN_PASSED}" \
863 OMEGA_RC_BRAND_PACKAGED_PASSED="${BRAND_PACKAGED_SCAN_PASSED}" \
864 OMEGA_RC_DRY_RUN="${DRY_RUN}" \
865 OMEGA_RC_NOTARIZATION_STATUS="${notarization_status}" \
866 OMEGA_RC_NOTARIZATION_ATTEMPTED="${notarization_attempted}" \
867 OMEGA_RC_NOTARIZATION_STAPLED="${notarization_stapled}" \
868 OMEGA_RC_NOTARIZATION_APP_STAPLED="${app_stapled}" \
869 OMEGA_RC_PACKAGE_PATH="${package_path}" \
870 python3 <<'PY'
871import json
872import os
873import subprocess
874from pathlib import Path
875
876
877def worktree_state() -> tuple[bool, list[str]]:
878 """Read whether the tree this candidate was built from was recorded.
879
880 `require_clean_checkout` refuses to *start* from a dirty tree, so anything
881 seen here appeared during the build. This runs after packaging, and the
882 build generates `assets/licenses.md` through `script/generate-licenses`,
883 which is untracked — so every candidate recorded `dirty: true` on the
884 strength of its own output.
885
886 That reads as "the commit does not determine these bytes", which is the
887 opposite of true, and an independent reviewer correctly treated it as
888 weakening the source binding. A bare boolean cannot tell a reviewer which
889 reading applies, so the entries are recorded beside it: a tracked
890 modification really does break the commit-to-bytes binding, an untracked
891 build output does not.
892 """
893 status = subprocess.run(
894 ["git", "status", "--porcelain"],
895 capture_output=True,
896 text=True,
897 check=True,
898 )
899 entries = [line for line in status.stdout.splitlines() if line.strip()]
900 tracked = [line for line in entries if not line.startswith("??")]
901 return bool(tracked), entries
902
903
904source_is_dirty, worktree_entries = worktree_state()
905icons = json.loads(os.environ["OMEGA_RC_ICON_JSON"])
906packaged_bindings = json.loads(os.environ["OMEGA_RC_PACKAGED_BINDINGS_JSON"])
907upstream = os.environ.get("OMEGA_RC_UPSTREAM_COMMIT") or None
908package_digest = os.environ["OMEGA_RC_PACKAGE_DIGEST"]
909package_sha256 = None if package_digest == "null" else package_digest
910package_path = os.environ["OMEGA_RC_PACKAGE_PATH"]
911version = os.environ["OMEGA_RC_VERSION_VALUE"]
912build_lock_name = os.environ.get("OMEGA_RC_BUILD_LOCK_SNAPSHOT_NAME") or None
913build_lock_digest = os.environ.get("OMEGA_RC_BUILD_LOCK_SNAPSHOT_DIGEST") or None
914omega_binary_digest = os.environ.get("OMEGA_RC_OMEGA_BINARY_DIGEST") or None
915cli_binary_digest = os.environ.get("OMEGA_RC_CLI_BINARY_DIGEST") or None
916identity_proof_binary_digest = os.environ.get("OMEGA_RC_IDENTITY_PROOF_BINARY_DIGEST") or None
917dry_run = os.environ["OMEGA_RC_DRY_RUN"] == "true"
918notarization_attempted = os.environ["OMEGA_RC_NOTARIZATION_ATTEMPTED"] == "true"
919notarization_stapled = os.environ["OMEGA_RC_NOTARIZATION_STAPLED"] == "true"
920
921owner_blockers = []
922if dry_run:
923 owner_blockers.append(
924 "Developer ID signing must complete before this dry-run record can become a release candidate"
925 )
926if not notarization_stapled:
927 owner_blockers.append(
928 "Owner notarization must complete and staple the candidate before distribution"
929 )
930owner_blockers.append("Human publication of the GitHub prerelease (never latest)")
931
932record = {
933 "schema_version": int(os.environ["OMEGA_RC_SCHEMA_VERSION"]),
934 "product": "Omega",
935 "channel": "rc",
936 "version": version,
937 "bundle_version": os.environ["OMEGA_RC_BUNDLE_VERSION_VALUE"],
938 "artifact_name": os.environ["OMEGA_RC_ARTIFACT_NAME"],
939 "volume_name": os.environ["OMEGA_RC_VOLUME_NAME"],
940 "bundle_identifier": os.environ["OMEGA_RC_BUNDLE_ID"],
941 "team_id": os.environ["OMEGA_RC_TEAM_ID"],
942 "signing_identity": os.environ["OMEGA_RC_SIGNING_IDENTITY"],
943 "source": {
944 "commit": os.environ["OMEGA_RC_COMMIT"],
945 "upstream_commit": upstream,
946 # True only for a *tracked* modification, which is what actually
947 # breaks the commit-to-bytes binding. The entries are recorded beside
948 # it so a reader never has to guess which kind they are looking at.
949 "dirty": source_is_dirty,
950 "worktree_entries": worktree_entries,
951 "repository": "https://github.com/OpenAgentsInc/omega",
952 },
953 "toolchains": {
954 "rustc": os.environ["OMEGA_RC_RUSTC"],
955 "host": os.environ["OMEGA_RC_HOST"],
956 "target": "aarch64-apple-darwin",
957 },
958 "execution": {
959 "dry_run": dry_run,
960 },
961 "build_inputs": {
962 "cargo_lock_snapshot": {
963 "name": build_lock_name,
964 "sha256": build_lock_digest,
965 },
966 },
967 "digests": {
968 "cargo_lock_sha256": os.environ["OMEGA_RC_CARGO_LOCK_DIGEST"],
969 "icon_family_manifest_sha256": icons["manifest_sha256"],
970 "pinned_png_sha256": icons["pinned_png_sha256"],
971 "pinned_icns_sha256": icons["pinned_icns_sha256"],
972 "icon_outputs": icons["outputs"],
973 "packaged_icon_outputs": packaged_bindings["icon_outputs"],
974 "package_sha256": package_sha256,
975 "omega_binary_sha256": omega_binary_digest,
976 "cli_binary_sha256": cli_binary_digest,
977 },
978 "components": {
979 "omega_effectd": {
980 "package": "@openagentsinc/omega-effectd",
981 "service_version": "0.1.0",
982 "protocol": "openagents.omega.effectd.v1",
983 "platform": "darwin-arm64",
984 "release_tag": os.environ["OMEGA_RC_EFFECTD_RELEASE_TAG"],
985 "asset_name": os.environ["OMEGA_RC_EFFECTD_ASSET_NAME"],
986 "archive_sha256": os.environ["OMEGA_RC_EFFECTD_ARCHIVE_DIGEST"],
987 "source_commit": os.environ["OMEGA_RC_EFFECTD_SOURCE_COMMIT"],
988 "source_tree": os.environ["OMEGA_RC_EFFECTD_SOURCE_TREE"],
989 "manifest_sha256": os.environ["OMEGA_RC_EFFECTD_MANIFEST_DIGEST"],
990 "wrapper_sha256": os.environ["OMEGA_RC_EFFECTD_WRAPPER_DIGEST"],
991 "bundle_sha256": os.environ["OMEGA_RC_EFFECTD_BUNDLE_DIGEST"],
992 "source_node_sha256": os.environ["OMEGA_RC_EFFECTD_SOURCE_NODE_DIGEST"],
993 "packaged_node_sha256": (
994 os.environ.get("OMEGA_RC_EFFECTD_PACKAGED_NODE_DIGEST") or None
995 ),
996 },
997 "identity_proof_driver": {
998 "path": "Contents/MacOS/omega-identity-proof",
999 "binary_sha256": identity_proof_binary_digest,
1000 "protocol": "openagents.omega.identity-proof.v1",
1001 "keyring_service": "com.openagents.omega.identity-proof.v1",
1002 "keyring_account": "disposable-proof-only",
1003 },
1004 },
1005 "identity_boundaries": {
1006 "source_verifier": {
1007 "path": os.environ["OMEGA_RC_SOURCE_VERIFIER_PATH"],
1008 "sha256": os.environ["OMEGA_RC_SOURCE_VERIFIER_DIGEST"],
1009 "passed": os.environ["OMEGA_RC_SOURCE_VERIFIER_PASSED"] == "true",
1010 },
1011 "packaged_scan": {
1012 "passed": os.environ["OMEGA_RC_PACKAGED_SCAN_PASSED"] == "true",
1013 "forbidden_literals": os.environ["OMEGA_RC_IDENTITY_BOUNDARIES"].split(","),
1014 },
1015 },
1016 "brand_gate": {
1017 "source_verifier": {
1018 "path": os.environ["OMEGA_RC_BRAND_VERIFIER_PATH"],
1019 "sha256": os.environ["OMEGA_RC_BRAND_VERIFIER_DIGEST"],
1020 "passed": os.environ["OMEGA_RC_BRAND_SOURCE_PASSED"] == "true",
1021 },
1022 "policy": {
1023 "path": os.environ["OMEGA_RC_BRAND_POLICY_PATH"],
1024 "sha256": os.environ["OMEGA_RC_BRAND_POLICY_DIGEST"],
1025 },
1026 "packaged_scan": {
1027 "passed": os.environ["OMEGA_RC_BRAND_PACKAGED_PASSED"] == "true",
1028 "surfaces": ["Contents/Info.plist", "embedded icon assets"],
1029 },
1030 },
1031 "notarization": {
1032 "attempted": notarization_attempted,
1033 "stapled": notarization_stapled,
1034 "app_stapled": os.environ["OMEGA_RC_NOTARIZATION_APP_STAPLED"] == "true",
1035 "status": os.environ["OMEGA_RC_NOTARIZATION_STATUS"],
1036 },
1037 "legal": {
1038 "commercial_terms_attached": False,
1039 "notices": packaged_bindings["legal_notices"],
1040 },
1041 "publication": {
1042 "repository": "OpenAgentsInc/omega",
1043 "prerelease": True,
1044 "latest": False,
1045 "tag": f"v{version}",
1046 },
1047 "package_path": package_path if not dry_run and Path(package_path).is_file() else None,
1048 "owner_blockers": owner_blockers,
1049}
1050
1051out_path = Path(os.environ["OMEGA_RC_RECORD_OUT"])
1052out_path.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
1053print(f"wrote release record: {out_path}")
1054PY
1055
1056 validate_release_record_schema "${RELEASE_RECORD_PATH}"
1057}
1058
1059validate_existing_release_record() {
1060 local package_path="$1"
1061 if [[ ! -f "${RELEASE_RECORD_PATH}" ]]; then
1062 echo "error: --skip-build requires existing release record at ${RELEASE_RECORD_PATH}" >&2
1063 exit 1
1064 fi
1065 validate_release_record_schema "${RELEASE_RECORD_PATH}"
1066
1067 local commit package_digest verifier_digest
1068 commit="$(git rev-parse HEAD)"
1069 package_digest="$(sha256_file "${package_path}")"
1070 verifier_digest="$(sha256_file "${OMEGA_SOURCE_VERIFIER_PATH}")"
1071 python3 - \
1072 "${RELEASE_RECORD_PATH}" \
1073 "${OUTPUT_DIR}" \
1074 "${commit}" \
1075 "${OMEGA_ARTIFACT_NAME}" \
1076 "${package_digest}" \
1077 "${verifier_digest}" <<'PY'
1078import hashlib
1079import json
1080import sys
1081from pathlib import Path
1082
1083record_path, output_dir, commit, artifact_name, package_digest, verifier_digest = (
1084 sys.argv[1:]
1085)
1086record = json.loads(Path(record_path).read_text(encoding="utf-8"))
1087
1088if record["execution"]["dry_run"]:
1089 raise SystemExit("--skip-build cannot reuse a dry-run release record")
1090if record["source"].get("commit") != commit:
1091 raise SystemExit("--skip-build release record commit does not match HEAD")
1092if record.get("artifact_name") != artifact_name:
1093 raise SystemExit("--skip-build release record artifact name is stale")
1094if record["digests"].get("package_sha256") != package_digest:
1095 raise SystemExit("--skip-build package digest does not match release record")
1096if record["identity_boundaries"]["source_verifier"].get("sha256") != verifier_digest:
1097 raise SystemExit("--skip-build source verifier digest does not match release record")
1098
1099snapshot = record["build_inputs"]["cargo_lock_snapshot"]
1100snapshot_name = snapshot["name"]
1101if Path(snapshot_name).name != snapshot_name:
1102 raise SystemExit("--skip-build Cargo.lock snapshot name must be a basename")
1103snapshot_path = Path(output_dir) / snapshot_name
1104if not snapshot_path.is_file():
1105 raise SystemExit(f"--skip-build Cargo.lock snapshot is missing: {snapshot_path}")
1106snapshot_digest = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
1107if snapshot_digest != snapshot["sha256"]:
1108 raise SystemExit("--skip-build Cargo.lock snapshot digest does not match release record")
1109
1110print(f"validated existing Omega RC artifact and release record: {record_path}")
1111PY
1112}
1113
1114restore_channel_and_version() {
1115 (
1116 cd "${ROOT}"
1117 if [[ -f "${_OMEGA_RC_CHANNEL_BACKUP_FILE:-}" ]]; then
1118 cp "${_OMEGA_RC_CHANNEL_BACKUP_FILE}" crates/zed/RELEASE_CHANNEL
1119 rm -f "${_OMEGA_RC_CHANNEL_BACKUP_FILE}"
1120 fi
1121 if [[ -f "${_OMEGA_RC_CARGO_BACKUP_FILE:-}" ]]; then
1122 cp "${_OMEGA_RC_CARGO_BACKUP_FILE}" crates/zed/Cargo.toml
1123 rm -f "${_OMEGA_RC_CARGO_BACKUP_FILE}"
1124 fi
1125 if [[ -f "${_OMEGA_RC_LOCK_BACKUP_FILE:-}" ]]; then
1126 cp "${_OMEGA_RC_LOCK_BACKUP_FILE}" Cargo.lock
1127 rm -f "${_OMEGA_RC_LOCK_BACKUP_FILE}"
1128 fi
1129 rm -f crates/zed/Cargo.toml.backup
1130 )
1131}
1132
1133set_channel_and_version() {
1134 _OMEGA_RC_CHANNEL_BACKUP_FILE="$(mktemp)"
1135 _OMEGA_RC_CARGO_BACKUP_FILE="$(mktemp)"
1136 _OMEGA_RC_LOCK_BACKUP_FILE="$(mktemp)"
1137 cp crates/zed/RELEASE_CHANNEL "${_OMEGA_RC_CHANNEL_BACKUP_FILE}"
1138 cp crates/zed/Cargo.toml "${_OMEGA_RC_CARGO_BACKUP_FILE}"
1139 cp Cargo.lock "${_OMEGA_RC_LOCK_BACKUP_FILE}"
1140
1141 printf '%s\n' "${OMEGA_RC_CHANNEL}" > crates/zed/RELEASE_CHANNEL
1142 python3 - <<PY
1143import re
1144from pathlib import Path
1145cargo = Path("crates/zed/Cargo.toml")
1146text = cargo.read_text(encoding="utf-8")
1147text2, count = re.subn(
1148 r'(?m)^version = "[^"]+"',
1149 'version = "${OMEGA_RC_SEMVER}"',
1150 text,
1151 count=1,
1152)
1153if count != 1:
1154 raise SystemExit("failed to set crates/zed/Cargo.toml version")
1155cargo.write_text(text2, encoding="utf-8")
1156PY
1157 export ZED_RELEASE_CHANNEL="${OMEGA_RC_CHANNEL}"
1158 export ZED_APP_VERSION="${OMEGA_RC_SEMVER}"
1159 export RELEASE_CHANNEL="${OMEGA_RC_CHANNEL}"
1160}
1161
1162maybe_import_certificate() {
1163 if [[ -z "${MACOS_CERTIFICATE:-}" || -z "${MACOS_CERTIFICATE_PASSWORD:-}" ]]; then
1164 return 0
1165 fi
1166 echo "Importing MACOS_CERTIFICATE into a temporary keychain..."
1167 security create-keychain -p "${MACOS_CERTIFICATE_PASSWORD}" omega-rc.keychain || true
1168 security default-keychain -s omega-rc.keychain
1169 security unlock-keychain -p "${MACOS_CERTIFICATE_PASSWORD}" omega-rc.keychain
1170 security set-keychain-settings omega-rc.keychain
1171 echo "${MACOS_CERTIFICATE}" | base64 --decode > /tmp/omega-rc-certificate.p12
1172 security import /tmp/omega-rc-certificate.p12 -k omega-rc.keychain -P "${MACOS_CERTIFICATE_PASSWORD}" -T /usr/bin/codesign
1173 rm -f /tmp/omega-rc-certificate.p12
1174 security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${MACOS_CERTIFICATE_PASSWORD}" omega-rc.keychain
1175 _OMEGA_RC_KEYCHAIN_CREATED=1
1176}
1177
1178cleanup() {
1179 restore_channel_and_version || true
1180 cleanup_notarization_key_file
1181 if [[ "${_OMEGA_RC_KEYCHAIN_CREATED:-0}" == "1" ]]; then
1182 security default-keychain -s login.keychain || true
1183 security delete-keychain omega-rc.keychain || true
1184 fi
1185}
1186
1187trap cleanup EXIT
1188
1189if [[ "${SELF_TEST_NOTARIZATION_KEY}" == "true" ]]; then
1190 self_test_notarization_key
1191 exit 0
1192fi
1193
1194require_arm64_host_or_target
1195require_clean_checkout
1196verify_source_identity_boundaries
1197verify_source_brand
1198prepare_omega_effectd_archive
1199
1200DMG_PATH="${OUTPUT_DIR}/${OMEGA_ARTIFACT_NAME}"
1201
1202if [[ "${SKIP_BUILD}" == "true" ]]; then
1203 if [[ ! -f "${DMG_PATH}" ]]; then
1204 echo "error: --skip-build requires existing package at ${DMG_PATH}" >&2
1205 exit 1
1206 fi
1207 validate_existing_release_record "${DMG_PATH}"
1208 exit 0
1209fi
1210
1211if [[ "${DRY_RUN}" == "true" ]]; then
1212 echo "Omega RC dry-run"
1213 echo " version: ${OMEGA_RC_VERSION}"
1214 echo " channel file value: ${OMEGA_RC_CHANNEL} (maps to ${OMEGA_RC_DISPLAY_CHANNEL})"
1215 echo " bundle id: ${OMEGA_BUNDLE_ID}"
1216 echo " volume: ${OMEGA_VOLUME_NAME}"
1217 echo " artifact: ${OMEGA_ARTIFACT_NAME}"
1218 echo " team: ${OMEGA_TEAM_ID}"
1219 echo " signing identity: ${OMEGA_SIGNING_IDENTITY}"
1220 find_openagents_signing_identity
1221 # Five arguments, not four: `app_stapled` arrived with the app-level staple
1222 # check and this call was not widened with it, so every dry run since has
1223 # died on `$5: unbound variable` after printing the plan. A dry run is the
1224 # affordance an independent reviewer is told to use before trusting a
1225 # candidate, so it has to reach the end.
1226 write_release_record "${DMG_PATH}" "not_attempted" "false" "false" "false"
1227 echo "dry-run complete; no compile, codesign, or notarization was performed."
1228 exit 0
1229fi
1230
1231find_openagents_signing_identity
1232maybe_import_certificate
1233find_openagents_signing_identity
1234
1235set_channel_and_version
1236
1237echo "Preparing Omega RC ${OMEGA_RC_VERSION} (channel=${OMEGA_RC_CHANNEL})"
1238echo "Updating temporary Cargo.lock for zed ${OMEGA_RC_SEMVER}"
1239cargo update --package zed --precise "${OMEGA_RC_SEMVER}"
1240cp Cargo.lock "${BUILD_LOCK_SNAPSHOT_PATH}"
1241BUILD_LOCK_SNAPSHOT_NAME="${OMEGA_BUILD_LOCK_SNAPSHOT_NAME}"
1242BUILD_LOCK_SNAPSHOT_DIGEST="$(sha256_file "${BUILD_LOCK_SNAPSHOT_PATH}")"
1243if [[ -z "${BUILD_LOCK_SNAPSHOT_DIGEST}" ]]; then
1244 echo "error: failed to capture build Cargo.lock snapshot." >&2
1245 exit 1
1246fi
1247
1248script/generate-licenses
1249
1250rustup target add "${TARGET_TRIPLE}"
1251
1252cargo_bundle_version="$(cargo -q bundle --help 2>&1 | head -n 1 || echo "")"
1253if [[ "${cargo_bundle_version}" != "cargo-bundle v0.6.1-zed" ]]; then
1254 cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy
1255fi
1256
1257export CXXFLAGS="-stdlib=libc++"
1258export ZED_BUNDLE=true
1259export ZED_COMMIT_SHA="$(git rev-parse HEAD)"
1260
1261echo "Compiling omega binaries for ${TARGET_TRIPLE}"
1262cargo build --locked --release --package zed --package cli --package omega_identity --target "${TARGET_TRIPLE}"
1263cargo build --locked --release --package remote_server --target "${TARGET_TRIPLE}"
1264
1265echo "Creating application bundle"
1266pushd crates/zed >/dev/null
1267cp Cargo.toml Cargo.toml.backup
1268sed \
1269 -i.backup \
1270 "s/package.metadata.bundle-${OMEGA_RC_CHANNEL}/package.metadata.bundle/" \
1271 Cargo.toml
1272app_path="$(TERM=xterm-256color CARGO_TERM_COLOR=never cargo bundle --release --target "${TARGET_TRIPLE}" --select-workspace-root | xargs)"
1273if [[ "$(sha256_file "${ROOT}/Cargo.lock")" != "${BUILD_LOCK_SNAPSHOT_DIGEST}" ]]; then
1274 echo "error: cargo-bundle changed Cargo.lock after the locked release build." >&2
1275 exit 1
1276fi
1277mv Cargo.toml.backup Cargo.toml
1278rm -f Cargo.toml.backup
1279popd >/dev/null
1280
1281document_icon_source="crates/zed/resources/Document.icns"
1282document_icon_target="${app_path}/Contents/Resources/Document.icns"
1283if [[ -f "${document_icon_source}" ]]; then
1284 mkdir -p "$(dirname "${document_icon_target}")"
1285 cp "${document_icon_source}" "${document_icon_target}"
1286fi
1287
1288# Do not embed the inherited Zed preview provisioning profile (team MQ55VZLNZQ).
1289rm -f "${app_path}/Contents/embedded.provisionprofile"
1290
1291cp "target/${TARGET_TRIPLE}/release/omega" "${app_path}/Contents/MacOS/omega"
1292cp "target/${TARGET_TRIPLE}/release/cli" "${app_path}/Contents/MacOS/cli"
1293cp "target/${TARGET_TRIPLE}/release/omega-identity-proof" "${app_path}/Contents/MacOS/omega-identity-proof"
1294stage_omega_effectd_component "${app_path}"
1295verify_packaged_identity_boundaries "${app_path}"
1296verify_packaged_brand "${app_path}"
1297
1298# Strip associated-domains entitlement: it requires an Omega-owned profile.
1299entitlements_path="${OUTPUT_DIR}/omega-rc.entitlements"
1300sed '/com.apple.developer.associated-domains/,+1d' \
1301 crates/zed/resources/zed.entitlements > "${entitlements_path}"
1302effectd_node_entitlements_path="${OUTPUT_DIR}/omega-effectd-node.entitlements"
1303plutil -create xml1 "${effectd_node_entitlements_path}"
1304plutil -insert 'com\.apple\.security\.cs\.allow-jit' -bool true \
1305 "${effectd_node_entitlements_path}"
1306plutil -insert 'com\.apple\.security\.cs\.allow-unsigned-executable-memory' -bool true \
1307 "${effectd_node_entitlements_path}"
1308
1309echo "Code signing with ${OMEGA_SIGNING_IDENTITY}"
1310/usr/bin/codesign --force --timestamp --options runtime \
1311 --entitlements "${effectd_node_entitlements_path}" \
1312 --sign "${OMEGA_SIGNING_IDENTITY}" \
1313 "${app_path}/Contents/Resources/omega-effectd/runtime/bin/node" -v
1314OMEGA_EFFECTD_PACKAGED_NODE_SHA256="$(sha256_file "${app_path}/Contents/Resources/omega-effectd/runtime/bin/node")"
1315/usr/bin/codesign --deep --force --timestamp --options runtime \
1316 --sign "${OMEGA_SIGNING_IDENTITY}" "${app_path}/Contents/MacOS/cli" -v
1317/usr/bin/codesign --deep --force --timestamp --options runtime \
1318 --sign "${OMEGA_SIGNING_IDENTITY}" "${app_path}/Contents/MacOS/omega-identity-proof" -v
1319/usr/bin/codesign --deep --force --timestamp --options runtime \
1320 --entitlements "${entitlements_path}" \
1321 --sign "${OMEGA_SIGNING_IDENTITY}" "${app_path}/Contents/MacOS/omega" -v
1322/usr/bin/codesign --force --timestamp --options runtime \
1323 --entitlements "${entitlements_path}" \
1324 --sign "${OMEGA_SIGNING_IDENTITY}" "${app_path}" -v
1325OMEGA_BINARY_DIGEST="$(sha256_file "${app_path}/Contents/MacOS/omega")"
1326CLI_BINARY_DIGEST="$(sha256_file "${app_path}/Contents/MacOS/cli")"
1327IDENTITY_PROOF_BINARY_DIGEST="$(sha256_file "${app_path}/Contents/MacOS/omega-identity-proof")"
1328if [[ -z "${OMEGA_BINARY_DIGEST}" || -z "${CLI_BINARY_DIGEST}" || -z "${IDENTITY_PROOF_BINARY_DIGEST}" ]]; then
1329 echo "error: signed Omega binary digests are unavailable." >&2
1330 exit 1
1331fi
1332
1333# OMEGA-DELTA-0023. Staple the APPLICATION, before the disk image is built.
1334#
1335# This script used to staple the DMG only. A DMG ticket covers the disk image
1336# the owner throws away; it does not travel with the application that ends up
1337# in /Applications, so `stapler validate /Applications/Omega.app` reported no
1338# ticket and Gatekeeper acceptance of the installed product could rest on an
1339# online lookup with Apple. omega#16 requires OFFLINE first start, which that
1340# cannot prove. The app is submitted and stapled here so the disk image below
1341# is assembled from an already-stapled application.
1342app_stapled="false"
1343if [[ -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
1344 echo "Submitting Omega.app for notarization (owner secrets present)..."
1345 prepare_notarization_key_file "${APPLE_NOTARIZATION_KEY}"
1346 xcode_bin_dir_path="$(xcode-select -p)/usr/bin"
1347 app_notarization_zip="${OUTPUT_DIR}/omega-app-notarization.zip"
1348 rm -f "${app_notarization_zip}"
1349 # notarytool takes an archive, not a bundle directory. ditto --keepParent
1350 # preserves the .app wrapper and its extended attributes.
1351 /usr/bin/ditto -c -k --keepParent "${app_path}" "${app_notarization_zip}"
1352 if "${xcode_bin_dir_path}/notarytool" submit --wait \
1353 --key "${NOTARIZATION_KEY_FILE}" \
1354 --key-id "${APPLE_NOTARIZATION_KEY_ID}" \
1355 --issuer "${APPLE_NOTARIZATION_ISSUER_ID}" \
1356 "${app_notarization_zip}"; then
1357 "${xcode_bin_dir_path}/stapler" staple "${app_path}"
1358 if "${xcode_bin_dir_path}/stapler" validate "${app_path}"; then
1359 app_stapled="true"
1360 echo "Stapled and validated ${app_path}"
1361 else
1362 echo "error: ${app_path} has no valid notarization ticket after stapling." >&2
1363 exit 1
1364 fi
1365 else
1366 echo "warning: app notarization submit failed; Omega.app remains signed but unstapled." >&2
1367 echo " offline first start cannot be proven from this candidate." >&2
1368 fi
1369 rm -f "${app_notarization_zip}"
1370 cleanup_notarization_key_file
1371else
1372 echo "Notarization secrets absent; Omega.app is signed but not stapled."
1373 echo "Offline first start cannot be proven from this candidate."
1374fi
1375
1376dmg_source_directory="${OUTPUT_DIR}/dmg-src"
1377rm -rf "${dmg_source_directory}"
1378mkdir -p "${dmg_source_directory}"
1379mv "${app_path}" "${dmg_source_directory}/"
1380stapled_app_path="${dmg_source_directory}/$(basename "${app_path}")"
1381ln -s /Applications "${dmg_source_directory}/Applications"
1382cp LICENSE-GPL LICENSE-APACHE "${dmg_source_directory}/"
1383if [[ -f assets/licenses.md ]]; then
1384 cp assets/licenses.md "${dmg_source_directory}/"
1385fi
1386PACKAGED_BINDINGS_JSON="$(collect_staged_packaged_bindings_json "${dmg_source_directory}")"
1387
1388echo "Creating DMG ${DMG_PATH}"
1389rm -f "${DMG_PATH}"
1390
1391# `hdiutil create -srcfolder` mounts its staging volume under /Volumes and
1392# copies there. On this host that copy began failing with
1393#
1394# could not access /Volumes/Omega RC/Omega.app - Operation not permitted
1395#
1396# after 0.2.0-rc16 had built the same way from the same directory. The source
1397# tree is readable, carries no special flags, and /Volumes holds no stale
1398# mount; the refusal is on the destination, which is the shape of a privacy
1399# restriction on mounted volumes rather than anything about the artifact.
1400#
1401# A release must not depend on whether this machine will let a tool write
1402# under /Volumes, so the image is built at a mountpoint we choose. Same
1403# contents, same compressed format, no /Volumes involvement.
1404dmg_scratch_image="${OUTPUT_DIR}/.dmg-scratch.dmg"
1405dmg_scratch_mount="${OUTPUT_DIR}/.dmg-mnt"
1406rm -f "${dmg_scratch_image}"
1407rm -rf "${dmg_scratch_mount}"
1408mkdir -p "${dmg_scratch_mount}"
1409
1410dmg_staged_megabytes="$(du -sm "${dmg_source_directory}" | cut -f1)"
1411hdiutil create -size "$(( dmg_staged_megabytes + 120 ))m" -fs HFS+ \
1412 -volname "${OMEGA_VOLUME_NAME}" -layout NONE "${dmg_scratch_image}" >/dev/null
1413hdiutil attach "${dmg_scratch_image}" -nobrowse -quiet -mountpoint "${dmg_scratch_mount}"
1414cp -R "${dmg_source_directory}/." "${dmg_scratch_mount}/"
1415hdiutil detach "${dmg_scratch_mount}" -quiet
1416hdiutil convert "${dmg_scratch_image}" -format UDZO -o "${DMG_PATH}" >/dev/null
1417rm -f "${dmg_scratch_image}"
1418rmdir "${dmg_scratch_mount}" 2>/dev/null || true
1419
1420rm -f "${dmg_source_directory}/Applications"
1421
1422/usr/bin/codesign --deep --force --timestamp --options runtime \
1423 --sign "${OMEGA_SIGNING_IDENTITY}" "${DMG_PATH}" -v
1424
1425notarization_status="not_attempted"
1426notarization_attempted="false"
1427notarization_stapled="false"
1428
1429if [[ -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
1430 notarization_attempted="true"
1431 echo "Submitting DMG for notarization (owner secrets present)..."
1432 prepare_notarization_key_file "${APPLE_NOTARIZATION_KEY}"
1433 xcode_bin_dir_path="$(xcode-select -p)/usr/bin"
1434 if "${xcode_bin_dir_path}/notarytool" submit --wait \
1435 --key "${NOTARIZATION_KEY_FILE}" \
1436 --key-id "${APPLE_NOTARIZATION_KEY_ID}" \
1437 --issuer "${APPLE_NOTARIZATION_ISSUER_ID}" \
1438 "${DMG_PATH}"; then
1439 "${xcode_bin_dir_path}/stapler" staple "${DMG_PATH}"
1440 if ! "${xcode_bin_dir_path}/stapler" validate "${DMG_PATH}"; then
1441 echo "error: ${DMG_PATH} has no valid notarization ticket after stapling." >&2
1442 exit 1
1443 fi
1444 notarization_status="submitted_and_stapled"
1445 notarization_stapled="true"
1446 # OMEGA-DELTA-0023. Both artifacts are validated, and the app ticket is
1447 # re-validated through its staged path so a stapled bundle cannot be
1448 # reported on the strength of the DMG's ticket alone.
1449 if ! "${xcode_bin_dir_path}/stapler" validate "${stapled_app_path}"; then
1450 echo "error: the staged Omega.app inside the disk image has no ticket." >&2
1451 exit 1
1452 fi
1453 echo "Stapled and validated ${DMG_PATH} and ${stapled_app_path}"
1454 else
1455 notarization_status="submit_failed"
1456 echo "warning: notarization submit failed; DMG remains signed but unstapled." >&2
1457 fi
1458 cleanup_notarization_key_file
1459else
1460 echo "Notarization secrets absent; leaving DMG signed but not notarized."
1461 echo "Release record will set notarization.status=not_attempted."
1462fi
1463
1464write_release_record "${DMG_PATH}" "${notarization_status}" "${notarization_attempted}" "${notarization_stapled}" "${app_stapled}"
1465
1466echo "Omega RC package ready:"
1467echo " ${DMG_PATH}"
1468echo " ${RELEASE_RECORD_PATH}"
1469if [[ "${notarization_stapled}" != "true" ]]; then
1470 echo "Note: notarization was not completed in this run."
1471fi
1472if [[ "${app_stapled}" != "true" ]]; then
1473 echo "Note: Omega.app is not stapled, so offline first start is not proven."
1474fi
1475