From 7659727f5fac63815be3ca3bfd90c644b2726299 Mon Sep 17 00:00:00 2001 From: iret77 <63622643+iret77@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:02:34 +0200 Subject: [PATCH 1/3] chore(release): replace local release.sh with a refusing stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aiui releases run exclusively in GitHub Actions (release-macos.yml, workflow_dispatch) on a macOS runner — Developer-ID signing + notarization via App Store Connect API key, exactly like every other devhost project (zaplex, nexgenvideo). There is no local release path. The old script's "documented emergency fallback" framing + local-keychain prerequisites repeatedly derailed session agents into "build it on your MacBook" — signing material lives in Actions secrets, not a local keychain, and the maintainer's Mac is not a build host. Replace the body with a stub that refuses and points at the workflow so the fiction can't be read as a real path again. --- scripts/release.sh | 292 ++++----------------------------------------- 1 file changed, 26 insertions(+), 266 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index 81bd6e8..40fced7 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,274 +1,34 @@ #!/usr/bin/env bash -# ⚠️ LEGACY — releases run in CI now. Use the `Release (macOS)` workflow -# (.github/workflows/release-macos.yml, workflow_dispatch) instead of this -# script. It is the CI port of this file; signing + notarization happen on -# GitHub-hosted macOS runners via Developer ID + App Store Connect API key. -# This script stays as documented emergency fallback ONLY (e.g. GitHub -# Actions outage) and requires the local prerequisites below. +# aiui does NOT release locally — not on CI-less machines, not on anyone's Mac. # -# Build, sign, notarize, and publish an aiui release, including the updater -# feed (latest.json) used by tauri-plugin-updater on running clients. +# Releases run exclusively in GitHub Actions, on a GitHub-hosted macOS +# runner, exactly like every other devhost project (zaplex, nexgenvideo): +# Developer-ID signing + notarization (App Store Connect API key), DMG + +# signed updater bundle + latest.json, GitHub release, PyPI publish. The +# signing material lives in GitHub Actions secrets — never in a local +# keychain, never on the maintainer's Mac (which is not a build host). # -# Prerequisites (once per machine): -# - Apple Developer ID Application certificate in the build keychain -# - `xcrun notarytool store-credentials` registered in the build keychain -# - Updater signing keypair generated via `tauri signer generate` -# - `gh auth login` (GitHub CLI) +# Trigger the release: +# gh workflow run release-macos.yml -f version= --repo byte5ai/aiui +# # or: GitHub UI -> Actions -> "Release (macOS)" -> Run workflow +# Validate-first (no PyPI, GitHub pre-release): +# gh workflow run release-macos.yml -f version= \ +# -f prerelease=true -f publish-pypi=false --repo byte5ai/aiui # -# Required environment (loaded from .env.release): -# APPLE_SIGNING_IDENTITY -# NOTARY_PROFILE -# BUILD_KEYCHAIN absolute path to keychain-db -# BUILD_KEYCHAIN_PASS_FILE path to file holding keychain password -# TAURI_SIGNING_PRIVATE_KEY_PATH minisign private key for updater feed -# TAURI_SIGNING_PRIVATE_KEY_PASSWORD (optional; empty if no password) -# UV_PUBLISH_TOKEN PyPI API token for publishing aiui-mcp. -# Generate at https://pypi.org/manage/account/token/ -# (scope: project = aiui-mcp). Without this, the -# Tauri side ships but the Python side stays stale -# on PyPI — exactly how the v0.4.2/v0.4.21 split -# happened on 2026-04-28. -# -# Usage: -# scripts/release.sh 0.1.2 -# scripts/release.sh 0.1.2 --dry +# This script exists only to stop agents and humans from reinventing a +# local build path. If GitHub Actions is down, wait for it — do not sign +# aiui locally. See AGENTS.md -> Releases. set -euo pipefail -# Pipefail surfaces silent failures in any stage of a `|`-chain -# (e.g. `grep | head` where grep failed). `nounset` catches typos in -# variable names. `errexit` aborts on first command failure. All three -# together = no half-run releases. Issue #L-1 in v0.4.10 review. - -VERSION="${1:-}" -DRY="${2:-}" -if [[ -z "$VERSION" ]]; then - echo "usage: $0 [--dry]" >&2 - exit 1 -fi -TAG="v${VERSION}" - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$REPO_ROOT" - -if [[ -f .env.release ]]; then - set -a; source .env.release; set +a -fi -: "${APPLE_SIGNING_IDENTITY:?not set}" -: "${NOTARY_PROFILE:?not set}" -: "${BUILD_KEYCHAIN:?not set}" -: "${BUILD_KEYCHAIN_PASS_FILE:?not set}" -: "${TAURI_SIGNING_PRIVATE_KEY_PATH:?not set}" -# AIUI_SKIP_PYPI=1 → validate-first / pre-release flow: build + sign + notarize -# + GitHub release WITHOUT publishing aiui-mcp to PyPI (PyPI versions are -# permanent; defer until the build is validated). The token is then not needed. -if [[ "${AIUI_SKIP_PYPI:-}" != "1" ]]; then - : "${UV_PUBLISH_TOKEN:?not set — needed for publishing aiui-mcp to PyPI. Put it in .env.release or export before running. See script header for details. (Or set AIUI_SKIP_PYPI=1 to skip PyPI for a pre-release.)}" - export UV_PUBLISH_TOKEN -fi -# Tauri bundler reads TAURI_SIGNING_PRIVATE_KEY (literal key content) during -# `tauri build`, not the _PATH variant. Load the file content here. -export TAURI_SIGNING_PRIVATE_KEY="$(cat "${TAURI_SIGNING_PRIVATE_KEY_PATH}")" -export TAURI_SIGNING_PRIVATE_KEY_PATH -export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" - -KC_PASS="$(cat "$BUILD_KEYCHAIN_PASS_FILE")" -echo "→ Unlocking $BUILD_KEYCHAIN" -security unlock-keychain -p "$KC_PASS" "$BUILD_KEYCHAIN" -security list-keychains -d user -s "$BUILD_KEYCHAIN" $(security list-keychains -d user | tr -d '"' | grep -v "$BUILD_KEYCHAIN") - -APP_DIR="companion/src-tauri/target/aarch64-apple-darwin/release/bundle/macos" -APP_SRC="${APP_DIR}/aiui.app" -DIRECT_ZIP="${REPO_ROOT}/aiui-${VERSION}-arm64.zip" -DIRECT_DMG="${REPO_ROOT}/aiui-${VERSION}-arm64.dmg" -UPDATER_BUNDLE="${APP_DIR}/aiui.app.tar.gz" -UPDATER_SIG="${UPDATER_BUNDLE}.sig" - -echo "→ Checking version sync (Cargo.toml ↔ tauri.conf.json ↔ python/pyproject.toml)" -# Four places need to agree on the version: -# - Cargo.toml `version` (drives the build) -# - tauri.conf.json `version` (drives the bundled Info.plist) -# - python/pyproject.toml `version` (drives the PyPI artifact and -# therefore what `uvx aiui-mcp` resolves to on remote hosts) -# - the `${VERSION}` argument to this script (drives the tag/release) -# Any drift produces a bundle whose CFBundleShortVersionString doesn't -# match what the in-app updater reports — that's how #82 happened. -# Drift between Tauri and PyPI is how the v0.4.2/v0.4.21 widgets-vs-teach -# split happened on 2026-04-28: Tauri shipped, PyPI didn't, remotes kept -# resolving the old prompt names. Issue C-3 in v0.4.10 review. -if ! grep -q "^version = \"${VERSION}\"" companion/src-tauri/Cargo.toml; then - echo " Cargo.toml version does not match ${VERSION} — bump it first." >&2 - exit 1 -fi -TAURI_CONF_VERSION="$(python3 -c 'import json,sys;print(json.load(open("companion/src-tauri/tauri.conf.json"))["version"])')" -if [[ "${TAURI_CONF_VERSION}" != "${VERSION}" ]]; then - echo " tauri.conf.json version is ${TAURI_CONF_VERSION}, expected ${VERSION} — bump it." >&2 - exit 1 -fi -PYPI_VERSION="$(grep -E '^version = ' python/pyproject.toml | awk -F'"' '{print $2}')" -if [[ "${PYPI_VERSION}" != "${VERSION}" ]]; then - echo " python/pyproject.toml version is ${PYPI_VERSION}, expected ${VERSION} — bump it." >&2 - exit 1 -fi - -echo "→ Building frontend" -(cd companion && npm ci && npm run build) - -echo "→ Building Tauri release (signs .app + emits .app.tar.gz + .sig for updater)" -(cd companion && npx tauri build --target aarch64-apple-darwin) - -# Verify the bundled Info.plist actually carries the expected version. The -# in-app updater reads CFBundleShortVersionString to decide what's -# "current"; a mismatch reproduces #82 as soon as the next update lands. -PLIST_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${APP_SRC}/Contents/Info.plist")" -if [[ "${PLIST_VERSION}" != "${VERSION}" ]]; then - echo " Bundled Info.plist version is ${PLIST_VERSION}, expected ${VERSION} — Tauri produced a drifted bundle. Aborting." >&2 - exit 1 -fi -echo " ✓ Info.plist version = ${PLIST_VERSION}" - -echo "→ Codesigning ${APP_SRC}" -codesign --force --deep --options runtime \ - --sign "${APPLE_SIGNING_IDENTITY}" \ - --entitlements companion/src-tauri/entitlements.plist \ - "${APP_SRC}" -codesign --verify --deep --strict --verbose=2 "${APP_SRC}" - -rm -f "${DIRECT_ZIP}" -ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "${DIRECT_ZIP}" - -echo "→ Submitting ${DIRECT_ZIP} to Apple notary service" -xcrun notarytool submit "${DIRECT_ZIP}" \ - --keychain-profile "${NOTARY_PROFILE}" \ - --keychain "${BUILD_KEYCHAIN}" \ - --wait - -echo "→ Stapling notarization ticket" -xcrun stapler staple "${APP_SRC}" -xcrun stapler validate "${APP_SRC}" - -# Re-create the distributable zip AFTER stapling so the ticket is included. -rm -f "${DIRECT_ZIP}" -ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "${DIRECT_ZIP}" - -echo "→ Building DMG (appdmg, branded background + drag-to-Applications)" -rm -f "${DIRECT_DMG}" -(cd companion && npx appdmg src-tauri/dmg/config.json "${DIRECT_DMG}") -codesign --force --sign "${APPLE_SIGNING_IDENTITY}" "${DIRECT_DMG}" -xcrun notarytool submit "${DIRECT_DMG}" \ - --keychain-profile "${NOTARY_PROFILE}" \ - --keychain "${BUILD_KEYCHAIN}" \ - --wait -xcrun stapler staple "${DIRECT_DMG}" - -# Re-create the updater bundle after stapling too, then re-sign it. The -# tauri signer picks the key path up from TAURI_SIGNING_PRIVATE_KEY_PATH, -# no CLI flags needed (and mixing them with env vars errors out). -rm -f "${UPDATER_BUNDLE}" "${UPDATER_SIG}" -tar -C "${APP_DIR}" -czf "${UPDATER_BUNDLE}" aiui.app -UPDATER_BUNDLE_ABS="${REPO_ROOT}/${UPDATER_BUNDLE}" -# Tauri signer CLI errors out if both TAURI_SIGNING_PRIVATE_KEY and -# TAURI_SIGNING_PRIVATE_KEY_PATH are set — unset PATH for just this call. -(cd companion && env -u TAURI_SIGNING_PRIVATE_KEY_PATH \ - npx tauri signer sign "${UPDATER_BUNDLE_ABS}") >/dev/null -SIG_FILE_CONTENT="$(cat "${UPDATER_SIG}")" -# tauri-updater expects the literal sig-file content as the signature field. -SIG_JSON=$(printf '%s' "${SIG_FILE_CONTENT}" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))') -echo "✓ updater signature written to ${UPDATER_SIG}" - -# Build latest.json describing this release. -PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" -LATEST_JSON="${REPO_ROOT}/latest.json" -cat > "${LATEST_JSON}" </dev/null | head -1)" -PY_SDIST="${PY_DIST_DIR}/aiui_mcp-${VERSION}.tar.gz" -if [[ -z "${PY_WHEEL}" || ! -f "${PY_SDIST}" ]]; then - echo " uv build did not produce expected artifacts in ${PY_DIST_DIR}" >&2 - ls -l "${PY_DIST_DIR}" >&2 || true - exit 1 -fi -echo " ✓ Python artifacts: $(basename "${PY_WHEEL}"), $(basename "${PY_SDIST}")" - -if [[ "$DRY" == "--dry" ]]; then - echo "Dry run — artifacts:" - echo " ${DIRECT_DMG}" - echo " ${DIRECT_ZIP}" - echo " ${UPDATER_NAMED}" - echo " ${LATEST_JSON}" - echo " ${PY_WHEEL}" - echo " ${PY_SDIST}" - exit 0 -fi - -if ! git rev-parse "${TAG}" >/dev/null 2>&1; then - git tag -a "${TAG}" -m "Release ${TAG}" -fi -git push origin "${TAG}" - -NOTES_FILE="$(mktemp)" -trap "rm -f ${NOTES_FILE}" EXIT -cat > "${NOTES_FILE}" <&2 <<'MSG' +✋ aiui does not release locally. -# AIUI_RELEASE_PRERELEASE=1 → mark as a GitHub pre-release. GitHub's -# `/releases/latest/` (what the in-app updater reads) skips pre-releases, so -# this does NOT auto-update any client — for validate-first delivery. Promote -# later with `gh release edit ${TAG} --prerelease=false` (then PyPI-publish). -GH_PRERELEASE_FLAG="" -[[ "${AIUI_RELEASE_PRERELEASE:-}" == "1" ]] && GH_PRERELEASE_FLAG="--prerelease" -gh release create "${TAG}" \ - "${DIRECT_DMG}" \ - "${DIRECT_ZIP}" \ - "${UPDATER_NAMED}" \ - "${LATEST_JSON}" \ - --repo byte5ai/aiui \ - --title "aiui ${TAG}" \ - --notes-file "${NOTES_FILE}" \ - ${GH_PRERELEASE_FLAG} +Releases run in GitHub Actions on a macOS runner. Trigger: + gh workflow run release-macos.yml -f version= --repo byte5ai/aiui -echo "✓ Released ${TAG} on GitHub${GH_PRERELEASE_FLAG:+ (pre-release)}" +Validate-first (no PyPI publish, marked as GitHub pre-release): + gh workflow run release-macos.yml -f version= \ + -f prerelease=true -f publish-pypi=false --repo byte5ai/aiui -# PyPI publish AFTER the GitHub release succeeds. If this step fails the -# Tauri side is already shipped and the manual recovery is `cd python && -# uv publish dist/*` once the credential issue is fixed. The pre-flight -# token check at the top of this script is what stops us from getting -# here without a token. -if [[ "${AIUI_SKIP_PYPI:-}" == "1" ]]; then - echo "→ Skipping PyPI publish (AIUI_SKIP_PYPI=1) — run \`cd python && uv publish\` (or promote) when validated" -else - echo "→ Publishing aiui-mcp ${VERSION} to PyPI" - (cd python && uv publish) - echo "✓ Published aiui-mcp ${VERSION} to PyPI" -fi +Details: AGENTS.md -> Releases +MSG +exit 1 From 3c5ce60c94cd16396ac36a83e1441aceca85ed6b Mon Sep 17 00:00:00 2001 From: iret77 <63622643+iret77@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:02:48 +0200 Subject: [PATCH 2/3] docs(release): document the CI-only release path in AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Releases section so a context-empty session hits the truth at the point of action: macOS releases run in GitHub Actions via release-macos.yml (workflow_dispatch), never locally, never on the maintainer's Mac — identical to zaplex/nexgenvideo. Signing material is in Actions secrets; scripts/release.sh is a refusing stub, not a path. --- AGENTS.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f362883..d022a79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ These rules apply to all AI agents working on this repository (Claude, Codex, Co ## Git Workflow - **Never push directly to `main`.** All changes go through feature branches and pull requests. - **Branch naming:** `feat/`, `fix/`, `refactor/`, `docs/`, `chore/`, `test/`, `release/`, `dev/` prefixes. -- **Conventional commits:** `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`, `release:`, `dev:`. +- **Conventional commits:** `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`, `dev:`. - **No `Co-Authored-By:` trailers for Claude or other AI agents.** Commits are made under the configured git identity, with no model-attribution footer. - **Never force-push** to any shared branch. - **Never commit secrets** (`.env`, API keys, tokens, credentials). @@ -22,6 +22,22 @@ A `.hooks/pre-push` hook blocks direct pushes to `main`/`master`. Override only ALLOW_PUSH_TO_MAIN=1 git push origin main ~~~ +## Releases +aiui releases run **exclusively in GitHub Actions**, never locally and never on the maintainer's Mac — identical to every other devhost project (zaplex, nexgenvideo). The maintainer's Mac is **not** a build host; devhost sessions trigger the workflow, they do not build. + +- **macOS release:** the `Release (macOS)` workflow — `.github/workflows/release-macos.yml`, `workflow_dispatch`. It builds on a `macos-14` runner, Developer-ID-signs + notarizes via the App Store Connect API key, builds the DMG + signed updater bundle + `latest.json`, cuts the GitHub release, and publishes `aiui-mcp` to PyPI. Trigger it: + ~~~bash + gh workflow run release-macos.yml -f version= --repo byte5ai/aiui + # validate-first (no PyPI, GitHub pre-release): + gh workflow run release-macos.yml -f version= \ + -f prerelease=true -f publish-pypi=false --repo byte5ai/aiui + ~~~ +- **Windows installer:** `release-windows.yml` (`workflow_dispatch`), attached to the release afterwards. +- **Signing material** lives in GitHub Actions secrets (`MACOS_*`, `TAURI_SIGNING_PRIVATE_KEY*`, `UV_PUBLISH_TOKEN`) — never in a local keychain, never in the repo, never in chat. +- **`scripts/release.sh` is not a release path.** It is a stub that refuses to run. Do not build or sign aiui on a local machine, and do not tell anyone to. If GitHub Actions is down, wait for it. + +Before dispatching, bump the version in `companion/src-tauri/Cargo.toml`, `companion/src-tauri/tauri.conf.json`, and `python/pyproject.toml` so all three agree with the dispatched version — the workflow's first step hard-fails on drift. + ## Engineering Standards This repo's engineering-standards status is tracked in `.github/engineering-standards.yml`. Source of truth: the account's `engineering-standards` repo. From 82a839b0306c04a52121ac71d9fe5ba9d67564a3 Mon Sep 17 00:00:00 2001 From: iret77 <63622643+iret77@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:59:34 +0200 Subject: [PATCH 3/3] fix(release): sign aiui as byte5 GmbH, not high5 ventures GmbH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aiui is a byte5ai repo, so it signs with the byte5 Apple Developer account (org rule: byte5ai -> byte5, iret77/high5-ventures -> high5). The signing identity itself comes from the byte5ai MACOS_CERTIFICATE secret; this only corrects the release-notes provenance string, which still claimed "high5 ventures GmbH" — carried over from the pre-standard era when aiui was signed with the high5 cert (v0.4.x–v0.8.2). --- .github/workflows/release-macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index a8f74aa..28fb3c1 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -262,7 +262,7 @@ jobs: cat > notes.md <