chore: realign module version to 0.1.0 pre-release baseline #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| on: | |
| push: | |
| tags: | |
| - "[0-9]+.[0-9]+.[0-9]+" | |
| permissions: {} | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| verify: | |
| name: Verify release contract | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden runner | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| agent.api.stepsecurity.io:443 | |
| api.github.com:443 | |
| github.com:443 | |
| codeload.github.com:443 | |
| objects.githubusercontent.com:443 | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify public contracts | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| RELEASE_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| python3 -I - <<'PY' | |
| import json | |
| import os | |
| import pathlib | |
| import py_compile | |
| import re | |
| import runpy | |
| semver = re.compile( | |
| r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)" | |
| r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" | |
| r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" | |
| ) | |
| version = pathlib.Path("VERSION").read_text(encoding="ascii").strip() | |
| version_data = json.loads(pathlib.Path("build/version.json").read_text()) | |
| manifest = json.loads(pathlib.Path("build/manifest.json").read_text()) | |
| contract = json.loads(pathlib.Path("config/nddev-contract.json").read_text()) | |
| baseline = json.loads( | |
| pathlib.Path("references/codex-baseline.json").read_text() | |
| ) | |
| versions = { | |
| os.environ["RELEASE_TAG"], | |
| version, | |
| version_data["build_version"], | |
| manifest["build_version"], | |
| } | |
| if len(versions) != 1 or semver.fullmatch(version) is None: | |
| raise SystemExit(f"release version mismatch: {sorted(versions)}") | |
| if version_data["schema_version"] != 2: | |
| raise SystemExit("unsupported build version schema") | |
| if manifest["schema_version"] != 3: | |
| raise SystemExit("unsupported manifest schema") | |
| if contract["contract_version"] != 3: | |
| raise SystemExit("unsupported public contract version") | |
| if baseline["schema_version"] != 2: | |
| raise SystemExit("unsupported runtime baseline schema") | |
| permission_profiles_since = version_data[ | |
| "codex_permission_profiles_since" | |
| ] | |
| tested = version_data["codex_cli_tested"] | |
| if semver.fullmatch(permission_profiles_since) is None: | |
| raise SystemExit( | |
| "invalid Codex permission-profile compatibility floor: " | |
| f"{permission_profiles_since}" | |
| ) | |
| if semver.fullmatch(tested) is None: | |
| raise SystemExit(f"invalid Codex tested version: {tested}") | |
| minimum_tuple = tuple( | |
| int(part) for part in permission_profiles_since.split(".") | |
| ) | |
| tested_tuple = tuple(int(part) for part in tested.split(".")) | |
| if tested_tuple < minimum_tuple: | |
| raise SystemExit( | |
| "Codex tested version is below the permission-profile floor" | |
| ) | |
| baseline_ref = "references/codex-baseline.json" | |
| if version_data["runtime_baseline_ref"] != baseline_ref: | |
| raise SystemExit("build version baseline reference mismatch") | |
| if manifest["runtime_compatibility"]["baseline_ref"] != baseline_ref: | |
| raise SystemExit("manifest baseline reference mismatch") | |
| if contract["runtime_compatibility"]["baseline_ref"] != baseline_ref: | |
| raise SystemExit("contract baseline reference mismatch") | |
| expected_version_refs = { | |
| "tested_version_ref": "build/version.json:codex_cli_tested" | |
| } | |
| if baseline["codex_cli"] != expected_version_refs: | |
| raise SystemExit("Codex baseline version references mismatch") | |
| if baseline["permission_profiles"] != { | |
| "configuration_key": "default_permissions", | |
| "custom_profiles_surface": "[permissions.<name>]", | |
| "custom_profiles_used": False, | |
| "managed_enforcement": False, | |
| "minimum_codex_version_ref": ( | |
| "build/version.json:codex_permission_profiles_since" | |
| ), | |
| "status": "beta", | |
| }: | |
| raise SystemExit("Codex permission-profile baseline mismatch") | |
| if baseline["evidence"]["official_permissions_documentation"] != ( | |
| "https://learn.chatgpt.com/docs/permissions" | |
| ): | |
| raise SystemExit("Codex baseline documentation mismatch") | |
| if baseline["evidence"]["official_agents_documentation"] != ( | |
| "https://learn.chatgpt.com/docs/agent-configuration/agents-md" | |
| "#how-codex-discovers-guidance" | |
| ): | |
| raise SystemExit("Codex instruction baseline documentation mismatch") | |
| expected_permission_defaults = { | |
| "admin_enforcement": False, | |
| "built_in_profiles_only": True, | |
| "configuration_precedence_applies": True, | |
| "custom_permissions_table_used": False, | |
| } | |
| if contract["permission_defaults"] != expected_permission_defaults: | |
| raise SystemExit("public permission-default contract mismatch") | |
| transaction = manifest["transaction_policy"] | |
| if { | |
| "agents_override_blocked_commands": transaction[ | |
| "agents_override_blocked_commands" | |
| ], | |
| "managed_file_mode": transaction["managed_file_mode"], | |
| "new_target_mode": transaction["new_target_mode"], | |
| "preserve_existing_target_mode": transaction[ | |
| "preserve_existing_target_mode" | |
| ], | |
| "reject_agents_override": transaction["reject_agents_override"], | |
| } != { | |
| "agents_override_blocked_commands": [ | |
| "plan", | |
| "apply", | |
| "switch", | |
| "restore", | |
| "launch", | |
| ], | |
| "managed_file_mode": "0600", | |
| "new_target_mode": "0700", | |
| "preserve_existing_target_mode": True, | |
| "reject_agents_override": True, | |
| }: | |
| raise SystemExit("manifest lifecycle hardening contract mismatch") | |
| expected_launch = { | |
| "command": "<absolute-target>/bin/codex", | |
| "direct_command": ( | |
| "CODEX_HOME=<absolute-target> " | |
| "<absolute-target>/bin/codex [args...]" | |
| ), | |
| "environment_variable": "CODEX_HOME", | |
| "executable_source": "validated-target-owned-standalone-install", | |
| "forwards_arguments": True, | |
| "forwards_child_exit_code": True, | |
| "json_manager_errors_supported": True, | |
| "manager_command": ( | |
| "python3 cli-tools/nddev_codex.py launch " | |
| "--target <absolute-target> -- [args...]" | |
| ), | |
| "requires_clean_managed_target": True, | |
| "required_version": tested, | |
| "streams_child_stdio": True, | |
| "successful_child_output_wrapped": False, | |
| "target_environment_scope": "child-process-only", | |
| "target_must_equal_runtime_home": True, | |
| } | |
| if manifest["runtime_launch"] != expected_launch: | |
| raise SystemExit("manifest launch contract mismatch") | |
| if contract["runtime_launch"] != expected_launch: | |
| raise SystemExit("public launch contract mismatch") | |
| if manifest["software_install"] != contract["software_install"]: | |
| raise SystemExit("software-install contract mismatch") | |
| software = contract["software_install"] | |
| if software["tested_version"] != tested: | |
| raise SystemExit("software-install version mismatch") | |
| if software["installer"]["release_tag"] != f"rust-v{tested}": | |
| raise SystemExit("software-install release tag mismatch") | |
| if software["installer"]["abnormal_exit_cleanup"] != ( | |
| "terminate-process-group-before-unlock" | |
| ): | |
| raise SystemExit("software-install cleanup contract mismatch") | |
| if software["desktop"] != { | |
| "command": ( | |
| "python3 cli-tools/nddev_codex.py desktop --target " | |
| "<absolute-target> [--workspace <absolute-dir>]" | |
| ), | |
| "configuration_binding": "not-guaranteed-by-codex-app", | |
| "delegate": "codex app", | |
| "documented_product": "ChatGPT", | |
| "direct_app_management": False, | |
| "pinned_cli_packaging": "Codex.app/Codex.dmg", | |
| "requires_managed_setup": False, | |
| "supported_platforms": ["macos"], | |
| }: | |
| raise SystemExit("desktop bridge contract mismatch") | |
| expected = { | |
| "safe": (":read-only", "on-request"), | |
| "full-auto": (":danger-full-access", "never"), | |
| } | |
| parse_permission_config = runpy.run_path( | |
| "cli-tools/nddev_codex.py" | |
| )["parse_permission_config"] | |
| for setup_id, values in expected.items(): | |
| path = pathlib.Path(f"setups/{setup_id}/config.toml") | |
| config = parse_permission_config( | |
| path.read_text(encoding="utf-8"), str(path) | |
| ) | |
| actual = (config["default_permissions"], config["approval_policy"]) | |
| if actual != values: | |
| raise SystemExit(f"invalid permission contract for {setup_id}") | |
| py_compile.compile("cli-tools/nddev_codex.py", doraise=True) | |
| PY | |
| git fetch --no-tags --force origin \ | |
| '+refs/heads/main:refs/remotes/origin/main' | |
| tag_commit="$(git rev-parse --verify "${RELEASE_SHA}^{commit}")" | |
| git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main | |
| - name: Verify release-evidence bundle | |
| run: | | |
| set -euo pipefail | |
| python3 -I <<'BUNDLE' | |
| import datetime as dt | |
| import hashlib | |
| import json | |
| import pathlib | |
| import re | |
| import subprocess | |
| import sys | |
| EVIDENCE_PATH = "build/release-evidence.json" | |
| REQUIRED_LANES = ( | |
| "fast", | |
| "release", | |
| "platform:macos", | |
| "platform:ubuntu", | |
| "benchmark:macos", | |
| "benchmark:ubuntu", | |
| ) | |
| SHA256 = re.compile(r"sha256:[0-9a-f]{64}") | |
| def fail(message): | |
| print(f"::error::{message}", file=sys.stderr) | |
| raise SystemExit(1) | |
| def parse_utc(value): | |
| if not isinstance(value, str) or not value.endswith("Z"): | |
| return None | |
| try: | |
| parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) | |
| except ValueError: | |
| return None | |
| return parsed if parsed.utcoffset() == dt.timedelta(0) else None | |
| try: | |
| evidence = json.loads(pathlib.Path(EVIDENCE_PATH).read_text(encoding="utf-8")) | |
| except (OSError, json.JSONDecodeError) as exc: | |
| fail(f"cannot read {EVIDENCE_PATH}: {exc}") | |
| if not isinstance(evidence, dict): | |
| fail("release-evidence must be a JSON object") | |
| if evidence.get("schema_version") != 2: | |
| fail("release-evidence schema_version must be 2 (execution-bound)") | |
| module = evidence.get("module") | |
| if not isinstance(module, dict) or module.get("repository") != "NDDev-it-com/nddev-codex-app": | |
| fail("release-evidence.module.repository must be this repository") | |
| digest = module.get("setup_digest") | |
| if not isinstance(digest, str) or SHA256.fullmatch(digest) is None: | |
| fail("release-evidence.module.setup_digest must be a sha256 content digest") | |
| # Bind the recorded digest to the exact tagged content: hash every | |
| # tracked blob by path and id, excluding the evidence file itself | |
| # (identical to scripts/evidence.compute_setup_digest in the harness). | |
| listing = subprocess.run( | |
| ["git", "ls-files", "-s"], capture_output=True, text=True, check=True | |
| ).stdout | |
| entries = [] | |
| for line in listing.splitlines(): | |
| metadata, _, path = line.partition("\t") | |
| fields = metadata.split() | |
| if not path or len(fields) != 3: | |
| fail(f"unexpected git ls-files entry: {line!r}") | |
| if path == EVIDENCE_PATH: | |
| continue | |
| entries.append(f"{path} {fields[1]}") | |
| recomputed = "sha256:" + hashlib.sha256("\n".join(sorted(entries)).encode("utf-8")).hexdigest() | |
| if digest != recomputed: | |
| fail( | |
| "release-evidence.module.setup_digest does not match the tagged content " | |
| f"(recorded {digest}, recomputed {recomputed})" | |
| ) | |
| harness = evidence.get("harness") | |
| if not isinstance(harness, dict) or re.fullmatch(r"[0-9a-f]{40}", str(harness.get("commit"))) is None: | |
| fail("release-evidence.harness.commit must be a full harness commit SHA") | |
| adapter = evidence.get("adapter") | |
| version_file = pathlib.Path("VERSION").read_text(encoding="utf-8").strip() | |
| if not isinstance(adapter, dict) or adapter.get("version") != version_file: | |
| fail("release-evidence.adapter.version must equal VERSION") | |
| if not isinstance(evidence.get("vendor"), dict) or not evidence["vendor"]: | |
| fail("release-evidence.vendor must be a non-empty object") | |
| platforms = evidence.get("platforms") | |
| if not isinstance(platforms, list) or not platforms: | |
| fail("release-evidence.platforms must be a non-empty array") | |
| promotion = evidence.get("promotion") | |
| if not isinstance(promotion, dict) or promotion.get("decision") != "approved": | |
| fail("release-evidence.promotion.decision must be approved; pending bundles cannot release") | |
| approved_by = promotion.get("approved_by") | |
| if not isinstance(approved_by, dict) or not str(approved_by.get("mechanism", "")).strip() or not str(approved_by.get("identity", "")).strip(): | |
| fail("release-evidence.promotion.approved_by must record mechanism and identity") | |
| lanes = evidence.get("lanes") | |
| if not isinstance(lanes, list): | |
| fail("release-evidence.lanes must be an array of execution records") | |
| seen = {} | |
| for index, record in enumerate(lanes): | |
| context = f"release-evidence.lanes[{index}]" | |
| if not isinstance(record, dict): | |
| fail(f"{context} must be an object") | |
| if record.get("status") != "passed": | |
| fail(f"{context}: status must be passed") | |
| lane = str(record.get("lane", "")) | |
| seen[lane] = seen.get(lane, 0) + 1 | |
| for key in ("workflow", "run_id", "runner", "toolchain", "platform"): | |
| if not str(record.get(key, "")).strip(): | |
| fail(f"{context}: {key} must be a non-empty string") | |
| if record.get("setup_digest") != digest: | |
| fail(f"{context}: setup_digest does not match the bundle module digest") | |
| if not isinstance(record.get("log_digest"), str) or SHA256.fullmatch(record["log_digest"]) is None: | |
| fail(f"{context}: log_digest must be a sha256 digest") | |
| started = parse_utc(record.get("started_at_utc")) | |
| completed = parse_utc(record.get("completed_at_utc")) | |
| if started is None or completed is None or started > completed: | |
| fail(f"{context}: execution timestamps are missing or inverted") | |
| for lane in REQUIRED_LANES: | |
| if seen.get(lane, 0) != 1: | |
| fail(f"release-evidence: lane {lane} must have exactly one passed execution record") | |
| if set(seen) - set(REQUIRED_LANES): | |
| fail("release-evidence: unknown lane records present") | |
| observation = evidence.get("vendor_observation") | |
| if not isinstance(observation, dict): | |
| fail("release-evidence.vendor_observation must record the currentness check") | |
| for key in ("source", "observed_latest_version", "event_date"): | |
| if not str(observation.get(key, "")).strip(): | |
| fail(f"release-evidence.vendor_observation.{key} must be a non-empty string") | |
| if parse_utc(observation.get("observed_at_utc")) is None: | |
| fail("release-evidence.vendor_observation.observed_at_utc must be ISO-8601 UTC") | |
| if not isinstance(observation.get("artifact_evidence_digest"), str) or SHA256.fullmatch(observation["artifact_evidence_digest"]) is None: | |
| fail("release-evidence.vendor_observation.artifact_evidence_digest must be a sha256 digest") | |
| generated_at = parse_utc(evidence.get("generated_at_utc")) | |
| expires_at = parse_utc(evidence.get("expires_at_utc")) | |
| if generated_at is None or expires_at is None: | |
| fail("release-evidence timestamps must be ISO-8601 UTC ending in Z") | |
| now = dt.datetime.now(dt.timezone.utc) | |
| if generated_at >= expires_at: | |
| fail("release-evidence.generated_at_utc must precede expires_at_utc") | |
| if now >= expires_at: | |
| fail("release-evidence has expired; regenerate after re-validation") | |
| if now < generated_at: | |
| fail("release-evidence.generated_at_utc is in the future") | |
| print(f"ok: execution-bound release-evidence verified for {version_file}") | |
| BUNDLE | |
| publish: | |
| name: Publish attested immutable release | |
| needs: verify | |
| permissions: | |
| contents: write # Create the release and upload its immutable assets. | |
| id-token: write # Authenticate keyless signing through GitHub OIDC. | |
| attestations: write # Publish build provenance for the release assets. | |
| artifact-metadata: write # Record artifact metadata for the published release. | |
| uses: NDDev-it-com/ci-workflows/.github/workflows/release-supply-chain.yml@2ccb80e96f5771b6a6b4eae63a4f47e232906dc7 # 0.12.0 | |
| with: | |
| version: ${{ github.ref_name }} | |
| package_name: nddev-codex-app | |
| archive_paths: >- | |
| README.md LICENSE VERSION CHANGELOG.md SECURITY.md .gitignore .agents | |
| .github build cli-tools config plugins references setups | |
| runtime_paths: >- | |
| README.md LICENSE VERSION .agents build cli-tools config plugins | |
| references setups |