mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
feat(standalone): complete Terminal exact lifecycle (#7319)
* feat(standalone): bootstrap terminal exact lifecycle * feat(standalone): complete terminal exact lifecycle * fix(ci): build standalone during postinstall * fix(standalone): repair exact lifecycle recovery Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): make exact updates replay-safe Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(terminal): enforce replay-safe exact carrier Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): align active update replay state Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): preserve exact channel lifecycle Generated-By: looper 0.11.2 (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com>
This commit is contained in:
@@ -35,6 +35,12 @@
|
||||
"packages/sidecar/",
|
||||
"packages/sidecar-proto/"
|
||||
],
|
||||
"terminal-scene": [
|
||||
"suite://workspace-install",
|
||||
"apps/closure/",
|
||||
"packages/standalone/",
|
||||
"shells/terminal/"
|
||||
],
|
||||
"daemon": [
|
||||
"suite://workspace-install",
|
||||
"apps/daemon/",
|
||||
@@ -168,6 +174,12 @@
|
||||
"runnerClass": "visual_hot",
|
||||
"products": "manifest",
|
||||
"reusable": false
|
||||
},
|
||||
"terminal_scene": {
|
||||
"inputs": ["suite://terminal-scene"],
|
||||
"runnerClass": "workspace_unit",
|
||||
"products": "manifest",
|
||||
"reusable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"web_tests_required",
|
||||
"tools_dev_tests_required",
|
||||
"tools_pack_tests_required",
|
||||
"terminal_scene_required",
|
||||
"windows_tools_pack_payload_tests_required",
|
||||
"ui_critical_validation_required",
|
||||
"ui_p0_validation_required",
|
||||
@@ -156,6 +157,14 @@
|
||||
},
|
||||
"effects": ["tools_pack_tests_required"], "confidence": "medium"
|
||||
},
|
||||
{
|
||||
"id": "terminal-scene-sources",
|
||||
"match": {
|
||||
"prefixes": ["apps/closure/", "packages/standalone/", "shells/terminal/"],
|
||||
"exact": [".github/scripts/pack.py", ".github/scripts/release.py", ".github/workflows/release-exact.yml"]
|
||||
},
|
||||
"effects": ["terminal_scene_required", "workspace_validation_required"], "confidence": "certain"
|
||||
},
|
||||
{
|
||||
"id": "workspace-manifests-and-ci",
|
||||
"match": {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Orchestrate Terminal scene build/reuse and owner-controlled promotion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
CHANNELS = {"betahyx", "previewhyx"}
|
||||
TARGETS = {"darwin-arm64", "win32-x64"}
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_json(path: Path, value: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def run_terminal(request: Path, receipt: Path) -> None:
|
||||
run(["pnpm", "--filter", "@open-design/terminal", "exact:pack", "--", "--request", str(request), "--receipt", str(receipt)])
|
||||
|
||||
|
||||
def signing_keys() -> list[tuple[str, str]]:
|
||||
keys = []
|
||||
for suffix in ("", "_NEXT"):
|
||||
key_id = os.environ.get(f"OD_EXACT_SIGNING_KEY_ID{suffix}")
|
||||
key = os.environ.get(f"OD_EXACT_ED25519_PRIVATE_KEY{suffix}")
|
||||
key_file = os.environ.get(f"OD_EXACT_ED25519_PRIVATE_KEY_FILE{suffix}")
|
||||
if not key and key_file:
|
||||
key = Path(key_file).read_text(encoding="utf-8")
|
||||
if key_id or key:
|
||||
if not key_id or not key:
|
||||
raise SystemExit(f"incomplete signing key pair: {suffix or 'primary'}")
|
||||
keys.append((key_id, key))
|
||||
if not keys:
|
||||
raise SystemExit("at least one exact signing key is required")
|
||||
return keys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--request", required=True, type=Path)
|
||||
parser.add_argument("--receipt", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
request = read_json(args.request.resolve())
|
||||
if request.get("schemaVersion") != 1 or request.get("operation") != "exact.pack":
|
||||
raise SystemExit("unsupported exact pack request")
|
||||
channel = request.get("channel")
|
||||
if channel not in CHANNELS:
|
||||
raise SystemExit("channel must be betahyx or previewhyx")
|
||||
release_version = request.get("releaseVersion", "")
|
||||
if re.fullmatch(rf"\d+\.\d+\.\d+-{channel}\.\d+", release_version) is None:
|
||||
raise SystemExit("releaseVersion does not belong to the requested channel")
|
||||
source_commit = request.get("sourceCommit", "")
|
||||
if re.fullmatch(r"[a-f0-9]{40}", source_commit) is None:
|
||||
raise SystemExit("sourceCommit must be a full 40-character SHA")
|
||||
targets = request.get("targets")
|
||||
if not isinstance(targets, list) or {item.get("target") for item in targets} != TARGETS:
|
||||
raise SystemExit("targets must contain exactly darwin-arm64 and win32-x64")
|
||||
|
||||
output = Path(request["outputDirectory"]).resolve()
|
||||
scene = Path(request["sceneDirectory"]).resolve()
|
||||
contracts = output / ".contracts"
|
||||
contracts.mkdir(parents=True, exist_ok=True)
|
||||
scene_reused = (scene / "scene.json").is_file()
|
||||
|
||||
if not scene_reused:
|
||||
run(["pnpm", "--filter", "@open-design/closure", "build"])
|
||||
closure_package = read_json(Path("apps/closure/package.json"))
|
||||
scene_request = contracts / "terminal-scene-build-request.json"
|
||||
scene_receipt = contracts / "terminal-scene-build-receipt.json"
|
||||
write_json(scene_request, {
|
||||
"schemaVersion": 1,
|
||||
"operation": "terminal.scene.build",
|
||||
"standaloneVersion": request["standaloneVersion"],
|
||||
"closureVersion": closure_package["version"],
|
||||
"closureArtifactFile": str(Path("apps/closure/dist/fixture.mjs").resolve()),
|
||||
"targets": targets,
|
||||
"sceneDirectory": str(scene),
|
||||
})
|
||||
run_terminal(scene_request, scene_receipt)
|
||||
|
||||
scene_manifest = read_json(scene / "scene.json")
|
||||
if scene_manifest.get("standaloneVersion") != request.get("standaloneVersion"):
|
||||
raise SystemExit("requested standaloneVersion differs from Terminal scene")
|
||||
|
||||
temporary_files: list[Path] = []
|
||||
try:
|
||||
signers = []
|
||||
for key_id, key in signing_keys():
|
||||
handle = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False)
|
||||
with handle:
|
||||
handle.write(key)
|
||||
path = Path(handle.name)
|
||||
temporary_files.append(path)
|
||||
signers.append({"keyId": key_id, "privateKeyFile": str(path)})
|
||||
promote_request = contracts / "terminal-scene-promote-request.json"
|
||||
promote_receipt = contracts / "terminal-scene-promote-receipt.json"
|
||||
write_json(promote_request, {
|
||||
"schemaVersion": 1,
|
||||
"operation": "terminal.scene.promote",
|
||||
"sceneDirectory": str(scene),
|
||||
"sourceCommit": source_commit,
|
||||
"channel": channel,
|
||||
"releaseVersion": release_version,
|
||||
"publishedAt": request["publishedAt"],
|
||||
"artifactBaseUrl": request["artifactBaseUrl"].rstrip("/"),
|
||||
"outputDirectory": str(output),
|
||||
"signers": signers,
|
||||
})
|
||||
run_terminal(promote_request, promote_receipt)
|
||||
finally:
|
||||
for path in temporary_files:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
promoted = read_json(promote_receipt)
|
||||
write_json(args.receipt.resolve(), {
|
||||
"schemaVersion": 1,
|
||||
"operation": "exact.pack",
|
||||
"channel": channel,
|
||||
"releaseVersion": release_version,
|
||||
"sourceCommit": source_commit,
|
||||
"sceneDirectory": str(scene),
|
||||
"sceneDigest": promoted["sceneDigest"],
|
||||
"sceneReused": scene_reused,
|
||||
"artifacts": promoted["artifacts"],
|
||||
"documents": promoted["documents"],
|
||||
"closureMetadataFile": promoted["closureMetadataFile"],
|
||||
"terminalMetadataFile": promoted["terminalMetadataFile"],
|
||||
"channelHeadFile": promoted["channelHeadFile"],
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish exact receipts: immutable objects, readback, then monotonic latest CAS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
CHANNELS = {"betahyx", "previewhyx"}
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_json(path: Path, value: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def request(url: str, method: str = "GET", body: bytes | None = None, headers: dict[str, str] | None = None):
|
||||
request_headers = dict(headers or {})
|
||||
token = os.environ.get("OD_EXACT_RELEASE_TOKEN")
|
||||
if token:
|
||||
request_headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, data=body, headers=request_headers, method=method)
|
||||
try:
|
||||
return urllib.request.urlopen(req)
|
||||
except urllib.error.HTTPError as error:
|
||||
return error
|
||||
|
||||
|
||||
def put_immutable(url: str, body: bytes, content_type: str) -> str:
|
||||
response = request(url, "PUT", body, {"If-None-Match": "*", "Content-Type": content_type, "Cache-Control": "public, max-age=31536000, immutable"})
|
||||
if response.status == 412:
|
||||
current = request(url)
|
||||
if current.status != 200 or current.read() != body:
|
||||
raise SystemExit(f"immutable object collision: {url}")
|
||||
return current.headers.get("ETag", "")
|
||||
if response.status != 200:
|
||||
raise SystemExit(f"immutable upload failed ({response.status}): {url}")
|
||||
return response.headers.get("ETag", "")
|
||||
|
||||
|
||||
def release_number(version: str, channel: str) -> tuple[tuple[int, int, int], int]:
|
||||
match = re.fullmatch(rf"(\d+)\.(\d+)\.(\d+)-{channel}\.(\d+)", version)
|
||||
if match is None:
|
||||
raise SystemExit("invalid counted release version")
|
||||
return ((int(match[1]), int(match[2]), int(match[3])), int(match[4]))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--request", required=True, type=Path)
|
||||
parser.add_argument("--receipt", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
publish = read_json(args.request.resolve())
|
||||
if publish.get("schemaVersion") != 1 or publish.get("operation") != "exact.release":
|
||||
raise SystemExit("unsupported exact release request")
|
||||
pack = read_json(Path(publish["packReceipt"]).resolve())
|
||||
channel = pack.get("channel")
|
||||
if channel not in CHANNELS:
|
||||
raise SystemExit("release channel must be betahyx or previewhyx")
|
||||
endpoint = publish["endpointUrl"].rstrip("/")
|
||||
bucket = publish["bucket"].strip("/")
|
||||
prefix = f"{endpoint}/{bucket}/{channel}"
|
||||
version = pack["releaseVersion"]
|
||||
uploaded: list[dict] = []
|
||||
for artifact in pack["artifacts"]:
|
||||
path = Path(artifact["file"]).resolve()
|
||||
body = path.read_bytes()
|
||||
if hashlib.sha256(body).hexdigest() != artifact["sha256"] or len(body) != artifact["size"]:
|
||||
raise SystemExit(f"pack receipt verification failed: {path}")
|
||||
url = f"{prefix}/{version}/{path.name}"
|
||||
etag = put_immutable(url, body, "application/octet-stream")
|
||||
uploaded.append({"url": url, "etag": etag, "sha256": artifact["sha256"]})
|
||||
|
||||
documents = []
|
||||
for document in pack["documents"]:
|
||||
path = Path(document["file"]).resolve()
|
||||
body = path.read_bytes()
|
||||
if hashlib.sha256(body).hexdigest() != document["sha256"] or len(body) != document["size"]:
|
||||
raise SystemExit(f"document receipt verification failed: {path}")
|
||||
url = f"{prefix}/{version}/{path.name}"
|
||||
etag = put_immutable(url, body, "application/json; charset=utf-8")
|
||||
readback = request(url)
|
||||
if readback.status != 200 or readback.read() != body:
|
||||
raise SystemExit(f"exact document readback failed: {url}")
|
||||
documents.append({"url": url, "etag": etag, "sha256": document["sha256"]})
|
||||
|
||||
head_path = Path(pack["channelHeadFile"]).resolve()
|
||||
head_body = head_path.read_bytes()
|
||||
latest_url = f"{prefix}/latest/channel-head.json"
|
||||
current = request(latest_url)
|
||||
headers = {"Content-Type": "application/json; charset=utf-8", "Cache-Control": "public, max-age=60"}
|
||||
if current.status == 404:
|
||||
headers["If-None-Match"] = "*"
|
||||
elif current.status == 200:
|
||||
current_body = current.read()
|
||||
if current_body == head_body:
|
||||
latest_etag = current.headers.get("ETag", "")
|
||||
write_json(args.receipt.resolve(), {"schemaVersion": 1, "operation": "exact.release", "channel": channel, "releaseVersion": version, "latestChannelHeadUrl": latest_url, "latestChannelHeadEtag": latest_etag, "documents": documents, "artifacts": uploaded, "replayed": True})
|
||||
return
|
||||
current_head = json.loads(current_body)["head"]
|
||||
incoming_head = json.loads(head_body)["head"]
|
||||
advanced = False
|
||||
for lane in ("closure", "terminal"):
|
||||
current_version = current_head["lanes"][lane]["releaseVersion"]
|
||||
incoming_version = incoming_head["lanes"][lane]["releaseVersion"]
|
||||
order = release_number(incoming_version, channel) > release_number(current_version, channel)
|
||||
if release_number(incoming_version, channel) < release_number(current_version, channel):
|
||||
raise SystemExit(f"{lane} lane would move backward: {current_version} -> {incoming_version}")
|
||||
advanced = advanced or order
|
||||
if not advanced:
|
||||
raise SystemExit("channel head CAS would not advance either lane")
|
||||
headers["If-Match"] = current.headers.get("ETag", "")
|
||||
else:
|
||||
raise SystemExit(f"latest inspection failed ({current.status})")
|
||||
promoted = request(latest_url, "PUT", head_body, headers)
|
||||
if promoted.status != 200:
|
||||
raise SystemExit(f"latest CAS failed ({promoted.status})")
|
||||
write_json(args.receipt.resolve(), {"schemaVersion": 1, "operation": "exact.release", "channel": channel, "releaseVersion": version, "latestChannelHeadUrl": latest_url, "latestChannelHeadEtag": promoted.headers.get("ETag", ""), "documents": documents, "artifacts": uploaded, "replayed": False})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -25,6 +25,7 @@ WORKLOADS = {
|
||||
"playwright_critical",
|
||||
"ui_p0",
|
||||
"playwright_visual",
|
||||
"terminal_scene",
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +196,7 @@ def enabled_workloads(outputs, ci_mode, full_lanes):
|
||||
"playwright_critical": outputs["ui_critical_validation_required"] and not ui_p0,
|
||||
"ui_p0": ui_p0,
|
||||
"playwright_visual": full_lanes or outputs["visual_validation_required"],
|
||||
"terminal_scene": full_lanes or outputs["terminal_scene_required"],
|
||||
}
|
||||
if set(enabled) != WORKLOADS:
|
||||
raise AssertionError("scope workload map drifted")
|
||||
|
||||
@@ -321,6 +321,11 @@ jobs:
|
||||
pnpm --filter @open-design/platform test
|
||||
pnpm --filter @open-design/sidecar test
|
||||
pnpm --filter @open-design/sidecar-proto test
|
||||
if [ "${{ fromJSON(needs.plan.outputs.scopes).terminal_scene_required }}" = "true" ]; then
|
||||
pnpm --filter @open-design/standalone test
|
||||
pnpm --filter @open-design/closure test
|
||||
pnpm --filter @open-design/terminal test
|
||||
fi
|
||||
if [ "${{ fromJSON(needs.plan.outputs.scopes).tools_dev_tests_required }}" = "true" ]; then
|
||||
pnpm --filter @open-design/tools-dev test
|
||||
fi
|
||||
@@ -657,6 +662,91 @@ jobs:
|
||||
if: ${{ steps.visual.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
terminal_scene:
|
||||
name: Terminal distribution scene
|
||||
needs: [plan, runners]
|
||||
if: ${{ fromJSON(needs.plan.outputs.run).terminal_scene }}
|
||||
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).workspace_unit }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Setup workspace
|
||||
uses: ./.github/actions/setup-workspace
|
||||
with:
|
||||
runner-labels: ${{ toJSON(fromJSON(needs.runners.outputs.runs_on).workspace_unit) }}
|
||||
|
||||
- name: Download convergence identity
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ needs.plan.outputs.convergence_plan_artifact }}
|
||||
path: ${{ runner.temp }}/terminal-convergence
|
||||
|
||||
- name: Fetch locked official Node carriers
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/terminal-node"
|
||||
python3 - <<'PY'
|
||||
import hashlib, json, os, urllib.request
|
||||
from pathlib import Path
|
||||
lock = json.loads(Path('shells/terminal/node-lock.json').read_text())
|
||||
root = Path(os.environ['RUNNER_TEMP']) / 'terminal-node'
|
||||
for value in lock['targets'].values():
|
||||
destination = root / value['archive']
|
||||
urllib.request.urlretrieve(value['url'], destination)
|
||||
if hashlib.sha256(destination.read_bytes()).hexdigest() != value['sha256']:
|
||||
raise SystemExit(f"official Node digest mismatch: {destination.name}")
|
||||
PY
|
||||
|
||||
- name: Build release-neutral Terminal scene
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm --filter @open-design/closure build
|
||||
pnpm --filter @open-design/terminal build
|
||||
python3 - <<'PY'
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
lock = json.loads(Path('shells/terminal/node-lock.json').read_text())
|
||||
closure = json.loads(Path('apps/closure/package.json').read_text())
|
||||
standalone = json.loads(Path('packages/standalone/package.json').read_text())
|
||||
root = Path(os.environ['RUNNER_TEMP'])
|
||||
targets = [{'target': target, 'nodeArchiveFile': str((root / 'terminal-node' / value['archive']).resolve()), 'nodeArchiveSha256': value['sha256']} for target, value in lock['targets'].items()]
|
||||
request = {'schemaVersion': 1, 'operation': 'terminal.scene.build', 'standaloneVersion': standalone['version'], 'closureVersion': closure['version'], 'closureArtifactFile': str(Path('apps/closure/dist/fixture.mjs').resolve()), 'targets': targets, 'sceneDirectory': str((root / 'terminal-scene').resolve())}
|
||||
(root / 'terminal-scene-request.json').write_text(json.dumps(request, sort_keys=True, separators=(',', ':')) + '\n')
|
||||
PY
|
||||
node shells/terminal/dist/exact-pack.mjs --request "$RUNNER_TEMP/terminal-scene-request.json" --receipt "$RUNNER_TEMP/terminal-scene-receipt.json"
|
||||
|
||||
- name: Write generic convergence product manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$RUNNER_TEMP/convergence-manifest/terminal_scene"
|
||||
python3 - <<'PY'
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
root = Path(os.environ['RUNNER_TEMP'])
|
||||
pending = json.loads((root / 'terminal-convergence' / 'pending-convergence.json').read_text())
|
||||
workload = pending['workloads']['terminal_scene']
|
||||
manifest = {'workload': 'terminal_scene', 'digest': workload['digest'], 'executionClass': workload['executionClass'], 'products': {'scene': {'type': 'job', 'source': 'terminal_scene', 'data': {'contract': 'terminal-scene-v1', 'owner': 'shells/terminal'}}}}
|
||||
(root / 'convergence-manifest' / 'terminal_scene' / 'product-manifest.json').write_text(json.dumps(manifest, sort_keys=True, separators=(',', ':')) + '\n')
|
||||
PY
|
||||
|
||||
- name: Upload Terminal scene product
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: terminal_scene
|
||||
path: ${{ runner.temp }}/terminal-scene
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Terminal scene convergence manifest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: terminal-scene-convergence-manifest
|
||||
path: ${{ runner.temp }}/convergence-manifest
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# This policy is a sibling of the validation workloads, not their prerequisite.
|
||||
# Labeled merge groups still receive the same workload coverage before the
|
||||
# required Validate workspace check rejects the converged result.
|
||||
@@ -868,6 +958,7 @@ jobs:
|
||||
- playwright_critical
|
||||
- ui_p0
|
||||
- playwright_visual
|
||||
- terminal_scene
|
||||
if: ${{ always() }}
|
||||
runs-on: ${{ fromJSON(needs.runners.outputs.runs_on).control }}
|
||||
timeout-minutes: 5
|
||||
@@ -910,6 +1001,14 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Download Terminal scene convergence manifest
|
||||
if: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && fromJSON(needs.plan.outputs.run).terminal_scene && needs.terminal_scene.result == 'success' }}
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: terminal-scene-convergence-manifest
|
||||
path: ${{ runner.temp }}/convergence-products
|
||||
|
||||
- name: Download pending convergence plan
|
||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||
continue-on-error: true
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
name: Release exact Terminal reference
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_sha:
|
||||
description: Full 40-character source commit to build
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: Isolated exact channel
|
||||
required: true
|
||||
type: choice
|
||||
options: [betahyx, previewhyx]
|
||||
release_version:
|
||||
description: Counted version, for example 0.1.0-betahyx.1
|
||||
required: true
|
||||
type: string
|
||||
standalone_version:
|
||||
description: Independent standalone protocol/runtime version
|
||||
required: true
|
||||
default: 0.1.0
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-exact-${{ inputs.channel }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
scene:
|
||||
name: Resolve release-neutral Terminal scene
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- name: Validate immutable inputs
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
RELEASE_VERSION: ${{ inputs.release_version }}
|
||||
run: |
|
||||
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||
[[ "$CHANNEL" == "betahyx" || "$CHANNEL" == "previewhyx" ]]
|
||||
[[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-${CHANNEL}\.[0-9]+$ ]]
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.source_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify checkout identity
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
||||
git merge-base --is-ancestor "$SOURCE_SHA" origin/main
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.18.0
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Restore converged Terminal scene when available
|
||||
id: convergence
|
||||
env:
|
||||
OD_WORKLOAD_RESULTS_BASE_URL: ${{ secrets.OD_WORKLOAD_RESULTS_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .github/scripts/runners.py
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
workloads = ['static_gate', 'preflight', 'workspace_unit_tests', 'daemon_unit_tests', 'windows_tools_pack_payload_tests', 'web_workspace_tests', 'e2e_vitest', 'playwright_critical', 'ui_p0', 'playwright_visual', 'terminal_scene']
|
||||
Path('.tmp/exact').mkdir(parents=True, exist_ok=True)
|
||||
Path('.tmp/exact/scope-plan.json').write_text(json.dumps({'enabled': {name: name == 'terminal_scene' for name in workloads}}, sort_keys=True) + '\n')
|
||||
PY
|
||||
RUNNER_PLAN_JSON="$(python3 - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
lines = Path(os.environ['GITHUB_OUTPUT']).read_text().splitlines()
|
||||
print(next(line.split('=', 1)[1] for line in lines if line.startswith('runs_on=')))
|
||||
PY
|
||||
)"
|
||||
python3 .github/scripts/convergence.py github-output --workflow ci --scope-plan .tmp/exact/scope-plan.json --runner-plan-json "$RUNNER_PLAN_JSON" --mode enforce --pending .tmp/exact/pending-convergence.json
|
||||
python3 - <<'PY'
|
||||
import hashlib, json, os, urllib.request, zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
root = Path('.tmp/exact')
|
||||
workload = json.loads((root / 'pending-convergence.json').read_text())['workloads']['terminal_scene']
|
||||
result = workload.get('result')
|
||||
if not workload.get('resultHit') or not result:
|
||||
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
||||
output.write('hit=false\n')
|
||||
raise SystemExit(0)
|
||||
product = result['products']['scene']
|
||||
archive = root / 'terminal-scene.zip'
|
||||
urllib.request.urlretrieve(product['source'], archive)
|
||||
if hashlib.sha256(archive.read_bytes()).hexdigest() != product['data']['sha256']:
|
||||
raise SystemExit('converged Terminal scene digest mismatch')
|
||||
destination = root / 'scene'
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(archive) as source:
|
||||
for entry in source.infolist():
|
||||
path = PurePosixPath(entry.filename)
|
||||
if entry.filename.startswith(('/', '\\')) or '\\' in entry.filename or '..' in path.parts:
|
||||
raise SystemExit(f'unsafe converged scene entry: {entry.filename!r}')
|
||||
source.extractall(destination)
|
||||
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
||||
output.write('hit=true\n')
|
||||
PY
|
||||
|
||||
- name: Fetch locked official Node carriers on convergence miss
|
||||
if: steps.convergence.outputs.hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .tmp/exact/node
|
||||
python3 - <<'PY'
|
||||
import hashlib, json, urllib.request
|
||||
from pathlib import Path
|
||||
lock = json.loads(Path('shells/terminal/node-lock.json').read_text())
|
||||
root = Path('.tmp/exact/node')
|
||||
for value in lock['targets'].values():
|
||||
destination = root / value['archive']
|
||||
urllib.request.urlretrieve(value['url'], destination)
|
||||
if hashlib.sha256(destination.read_bytes()).hexdigest() != value['sha256']:
|
||||
raise SystemExit(f"official Node digest mismatch: {destination.name}")
|
||||
PY
|
||||
|
||||
- name: Build Terminal-owned scene on convergence miss
|
||||
if: steps.convergence.outputs.hit != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm --filter @open-design/closure build
|
||||
pnpm --filter @open-design/terminal build
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
lock = json.loads(Path('shells/terminal/node-lock.json').read_text())
|
||||
closure = json.loads(Path('apps/closure/package.json').read_text())
|
||||
standalone = json.loads(Path('packages/standalone/package.json').read_text())
|
||||
targets = [{'target': target, 'nodeArchiveFile': str((Path('.tmp/exact/node') / value['archive']).resolve()), 'nodeArchiveSha256': value['sha256']} for target, value in lock['targets'].items()]
|
||||
request = {'schemaVersion': 1, 'operation': 'terminal.scene.build', 'standaloneVersion': standalone['version'], 'closureVersion': closure['version'], 'closureArtifactFile': str(Path('apps/closure/dist/fixture.mjs').resolve()), 'targets': targets, 'sceneDirectory': str(Path('.tmp/exact/scene').resolve())}
|
||||
Path('.tmp/exact/scene-request.json').write_text(json.dumps(request, sort_keys=True, separators=(',', ':')) + '\n')
|
||||
PY
|
||||
node shells/terminal/dist/exact-pack.mjs --request .tmp/exact/scene-request.json --receipt .tmp/exact/scene-receipt.json
|
||||
|
||||
- name: Verify scene remains release-neutral
|
||||
run: |
|
||||
test -f .tmp/exact/scene/scene.json
|
||||
! grep -Eq 'betahyx|previewhyx|publishedAt|artifactBaseUrl|signatures' .tmp/exact/scene/scene.json
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: exact-terminal-scene-${{ inputs.source_sha }}
|
||||
path: .tmp/exact/scene
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
publish:
|
||||
name: Promote, sign, and publish exact release
|
||||
needs: [scene]
|
||||
runs-on: ubuntu-latest
|
||||
environment: standalone-exact-release
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.source_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify checkout identity
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
run: |
|
||||
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
||||
git merge-base --is-ancestor "$SOURCE_SHA" origin/main
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.33.2
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24.18.0
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: exact-terminal-scene-${{ inputs.source_sha }}
|
||||
path: .tmp/exact/scene
|
||||
|
||||
- name: Write promotion request
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_sha }}
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
RELEASE_VERSION: ${{ inputs.release_version }}
|
||||
STANDALONE_VERSION: ${{ inputs.standalone_version }}
|
||||
STORAGE_ENDPOINT: ${{ secrets.EXACT_RELEASE_ENDPOINT }}
|
||||
STORAGE_BUCKET: ${{ secrets.EXACT_RELEASE_BUCKET }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, subprocess
|
||||
from pathlib import Path
|
||||
root = Path('.tmp/exact')
|
||||
output = root / 'output'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
channel = os.environ['CHANNEL']
|
||||
version = os.environ['RELEASE_VERSION']
|
||||
published_at = subprocess.check_output(['git', 'show', '--no-patch', '--format=%cI', os.environ['SOURCE_SHA']], text=True).strip()
|
||||
prefix = f"{os.environ['STORAGE_ENDPOINT'].rstrip('/')}/{os.environ['STORAGE_BUCKET'].strip('/')}/{channel}/{version}"
|
||||
lock = json.loads(Path('shells/terminal/node-lock.json').read_text())
|
||||
targets = [{'target': target, 'nodeArchiveFile': f"/convergence-scene/{value['archive']}", 'nodeArchiveSha256': value['sha256']} for target, value in lock['targets'].items()]
|
||||
request = {'schemaVersion': 1, 'operation': 'exact.pack', 'channel': channel, 'releaseVersion': version, 'standaloneVersion': os.environ['STANDALONE_VERSION'], 'sourceCommit': os.environ['SOURCE_SHA'], 'publishedAt': published_at, 'artifactBaseUrl': prefix, 'outputDirectory': str(output.resolve()), 'sceneDirectory': str((root / 'scene').resolve()), 'targets': targets}
|
||||
(root / 'pack-request.json').write_text(json.dumps(request, sort_keys=True, separators=(',', ':')) + '\n')
|
||||
PY
|
||||
|
||||
- name: Promote and sign Terminal scene
|
||||
env:
|
||||
OD_EXACT_SIGNING_KEY_ID: ${{ secrets.EXACT_SIGNING_KEY_ID }}
|
||||
OD_EXACT_ED25519_PRIVATE_KEY: ${{ secrets.EXACT_ED25519_PRIVATE_KEY }}
|
||||
OD_EXACT_SIGNING_KEY_ID_NEXT: ${{ secrets.EXACT_SIGNING_KEY_ID_NEXT }}
|
||||
OD_EXACT_ED25519_PRIVATE_KEY_NEXT: ${{ secrets.EXACT_ED25519_PRIVATE_KEY_NEXT }}
|
||||
run: python3 .github/scripts/pack.py --request .tmp/exact/pack-request.json --receipt .tmp/exact/pack-receipt.json
|
||||
|
||||
- name: Publish immutable release and channel-head CAS
|
||||
env:
|
||||
OD_EXACT_RELEASE_TOKEN: ${{ secrets.EXACT_RELEASE_TOKEN }}
|
||||
STORAGE_ENDPOINT: ${{ secrets.EXACT_RELEASE_ENDPOINT }}
|
||||
STORAGE_BUCKET: ${{ secrets.EXACT_RELEASE_BUCKET }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
value = {'schemaVersion': 1, 'operation': 'exact.release', 'packReceipt': str(Path('.tmp/exact/pack-receipt.json').resolve()), 'endpointUrl': os.environ['STORAGE_ENDPOINT'], 'bucket': os.environ['STORAGE_BUCKET']}
|
||||
Path('.tmp/exact/release-request.json').write_text(json.dumps(value, sort_keys=True, separators=(',', ':')) + '\n')
|
||||
PY
|
||||
python3 .github/scripts/release.py --request .tmp/exact/release-request.json --receipt .tmp/exact/release-receipt.json
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: exact-${{ inputs.channel }}-${{ inputs.release_version }}-receipts
|
||||
path: |
|
||||
.tmp/exact/*-request.json
|
||||
.tmp/exact/*-receipt.json
|
||||
.tmp/exact/output/.contracts/*.json
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
Reference in New Issue
Block a user