diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..671dfd6 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,49 @@ +# Native CI — Hanzo Git act_runners (label hanzo-build-linux-amd64). +# amd64 test + build gate; mirrors the amd64 lanes of .github/workflows/ci.yml. +# The macOS (Safari) and cross-platform Playwright matrix stay on GitHub-hosted +# runners until native macOS/Windows act_runners are green +# (runners-act-migration.md Phase 2). act_runner reads this unmodified; +# GitHub.com ignores .gitea/workflows, so the GitHub release lanes are untouched. +name: ci + +on: + push: + branches: [main, dev] + tags: ['v*'] + pull_request: + branches: [main] + +jobs: + test: + runs-on: hanzo-build-linux-amd64 + container: node:22-bookworm + steps: + - uses: actions/checkout@v4 + - run: corepack enable + - run: pnpm install --frozen-lockfile + + # Shared workspace lib first — consumers import its built dist/. + - run: pnpm --filter @hanzo/cards build + - run: pnpm --filter @hanzo/browser-extension test + - run: pnpm --filter @hanzo/aci test + - run: pnpm --filter @hanzo/auth test + + build: + runs-on: hanzo-build-linux-amd64 + container: node:22-bookworm + needs: [test] + steps: + - uses: actions/checkout@v4 + - run: corepack enable + - run: pnpm install --frozen-lockfile + + - name: Build browser extension + working-directory: packages/browser + run: node src/build.js + + - name: Check bundle budgets + working-directory: packages/browser + run: | + [ -f dist/browser-extension/background.js ] || node src/build.js + pnpm run check:bundle-budget + continue-on-error: true diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..9782fe4 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,102 @@ +# Native store-publish — Hanzo Git act_runner (label hanzo-build-linux-amd64). +# This is the SYSTEM OF RECORD for publishing the browser extension to the +# Chrome Web Store + Firefox AMO, and for the npm package. GitHub only mirrors. +# +# Secrets model — KMS ONLY. The single bootstrap credential is the KMS machine +# identity (KMS_CLIENT_ID / KMS_CLIENT_SECRET, a git.hanzo.ai Actions secret). +# Every store credential is pulled from KMS at publish time +# (org `hanzo`, env `prod`, path `/extension-publish` — see +# deploy/kms/extension-publish-kms-sync.yaml). No CWS/AMO/npm secret is ever +# stored in GitHub or Gitea Actions secrets. +# +# amd64/Linux only. Safari (macOS) + Windows desktop stay on GitHub-hosted +# runners until native macOS/Windows act_runners are green +# (runners-act-migration.md Phase 2). +name: publish + +on: + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + publish: + runs-on: hanzo-build-linux-amd64 + container: node:22-bookworm + steps: + - uses: actions/checkout@v4 + + - name: Tools (jq, pnpm) + run: | + apt-get update && apt-get install -y --no-install-recommends jq curl + corepack enable + - run: pnpm install --frozen-lockfile + + # ── The ONE secret hop: KMS machine identity → store creds ── + # Login once, read each key from hanzo:/extension-publish, mask + export. + # A missing key exports empty; each publish step below gates on its creds, + # so partial provisioning publishes exactly what KMS has. + - name: Load publish secrets from KMS + env: + KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }} + KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }} + run: | + if [ -z "$KMS_CLIENT_ID" ] || [ -z "$KMS_CLIENT_SECRET" ]; then + echo "::warning::KMS machine identity absent — cannot pull publish creds. Seed KMS_CLIENT_ID/SECRET as git.hanzo.ai Actions secrets." + exit 0 + fi + KMS=https://kms.hanzo.ai; ORG=hanzo; ENVN=prod; P=extension-publish + TOKEN=$(curl -sf "$KMS/v1/kms/auth/login" -H 'Content-Type: application/json' \ + -d "{\"clientId\":\"$KMS_CLIENT_ID\",\"clientSecret\":\"$KMS_CLIENT_SECRET\"}" | jq -r .accessToken) + [ -n "$TOKEN" ] && [ "$TOKEN" != null ] || { echo "::error::KMS login failed"; exit 1; } + for KEY in CHROME_EXTENSION_ID CHROME_CLIENT_ID CHROME_CLIENT_SECRET CHROME_REFRESH_TOKEN AMO_API_KEY AMO_API_SECRET NPM_TOKEN; do + VAL=$(curl -sf "$KMS/v1/kms/orgs/$ORG/secrets/$P/$KEY?env=$ENVN" \ + -H "Authorization: Bearer $TOKEN" | jq -r '.secret.value // empty') + if [ -n "$VAL" ]; then echo "::add-mask::$VAL"; fi + echo "$KEY=$VAL" >> "$GITHUB_ENV" + done + + - name: Build browser extension + working-directory: packages/browser + run: node src/build.js + + - name: Package (web-ext — no zip binary on the runner) + working-directory: packages/browser + run: | + VERSION=$(node -p "require('./package.json').version") + npx web-ext build --source-dir dist/browser-extension/chrome --artifacts-dir dist --filename hanzo-ai-chrome-v${VERSION}.zip --overwrite-dest + npx web-ext build --source-dir dist/browser-extension/firefox --artifacts-dir dist --filename hanzo-ai-firefox-v${VERSION}.zip --overwrite-dest + + - name: Publish to Chrome Web Store + if: env.CHROME_EXTENSION_ID != '' + working-directory: packages/browser + run: | + ACCESS_TOKEN=$(curl -s -X POST "https://oauth2.googleapis.com/token" \ + -d "client_id=$CHROME_CLIENT_ID" -d "client_secret=$CHROME_CLIENT_SECRET" \ + -d "refresh_token=$CHROME_REFRESH_TOKEN" -d "grant_type=refresh_token" | jq -r '.access_token') + VERSION=$(node -p "require('./package.json').version") + curl -sf -X PUT "https://www.googleapis.com/upload/chromewebstore/v1.1/items/$CHROME_EXTENSION_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" -H "x-goog-api-version: 2" \ + -T dist/hanzo-ai-chrome-v${VERSION}.zip + curl -sf -X POST "https://www.googleapis.com/chromewebstore/v1.1/items/$CHROME_EXTENSION_ID/publish" \ + -H "Authorization: Bearer $ACCESS_TOKEN" -H "x-goog-api-version: 2" -H "Content-Length: 0" + + - name: Publish to Firefox Add-ons + if: env.AMO_API_KEY != '' + working-directory: packages/browser + run: | + npx web-ext sign --source-dir dist/browser-extension/firefox \ + --api-key "$AMO_API_KEY" --api-secret "$AMO_API_SECRET" \ + --channel listed --id "hanzo-ai@hanzo.ai" + + # npm — canonical home is here. The .github npm job stays live until this + # runs green (KMS NPM_TOKEN seeded), then it is deleted. pnpm publish skips + # versions already on npm, so any transition overlap is idempotent. + - name: Publish @hanzo/* to npm + if: env.NPM_TOKEN != '' + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc + echo "@hanzo:registry=https://registry.npmjs.org/" >> .npmrc + pnpm --filter @hanzo/cards build + pnpm -r --if-present run build || true + pnpm -r publish --access public --no-git-checks --ignore-scripts --report-summary || true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c551a0c..9f3c763 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,12 @@ name: Publish +# Store-publish (Chrome Web Store, Firefox AMO, VS Code, Open VSX, JetBrains) and +# every store credential have moved to the native pipeline on Hanzo Git: +# .gitea/workflows/publish.yml — reads creds from KMS (deploy/kms/…), NOT GitHub. +# GitHub retains only: build + the downloadable GitHub Release (mirror +# distribution), and the live npm publish (interim — migrates to native once the +# KMS NPM_TOKEN is seeded and the native run is verified green). + on: push: tags: @@ -17,25 +24,16 @@ permissions: contents: write jobs: - # ─── Check available secrets ─── + # ─── npm publish gate (the one store credential still on GitHub) ─── secrets: - name: Load KMS Secrets + name: Check npm token runs-on: hanzo-build-linux-amd64 outputs: - has-chrome: ${{ steps.check.outputs.has-chrome }} - has-firefox: ${{ steps.check.outputs.has-firefox }} - has-vsce: ${{ steps.check.outputs.has-vsce }} has-npm: ${{ steps.check.outputs.has-npm }} - has-jetbrains: ${{ steps.check.outputs.has-jetbrains }} steps: - - name: Check available secrets + - name: Check npm token id: check - run: | - echo "has-chrome=${{ secrets.CHROME_EXTENSION_ID != '' }}" >> $GITHUB_OUTPUT - echo "has-firefox=${{ secrets.AMO_API_KEY != '' }}" >> $GITHUB_OUTPUT - echo "has-vsce=${{ secrets.VSCE_PAT != '' }}" >> $GITHUB_OUTPUT - echo "has-npm=${{ secrets.NPM_TOKEN != '' }}" >> $GITHUB_OUTPUT - echo "has-jetbrains=${{ secrets.JETBRAINS_TOKEN != '' }}" >> $GITHUB_OUTPUT + run: echo "has-npm=${{ secrets.NPM_TOKEN != '' }}" >> $GITHUB_OUTPUT # ─── Chrome + Firefox ─── browser: @@ -84,40 +82,6 @@ jobs: run: npx web-ext lint --source-dir dist/browser-extension/firefox continue-on-error: true - - name: Publish to Chrome Web Store - if: needs.secrets.outputs.has-chrome == 'true' - working-directory: packages/browser - run: | - ACCESS_TOKEN=$(curl -s -X POST "https://oauth2.googleapis.com/token" \ - -d "client_id=${{ secrets.CHROME_CLIENT_ID }}" \ - -d "client_secret=${{ secrets.CHROME_CLIENT_SECRET }}" \ - -d "refresh_token=${{ secrets.CHROME_REFRESH_TOKEN }}" \ - -d "grant_type=refresh_token" | jq -r '.access_token') - VERSION=$(node -p "require('./package.json').version") - curl -sf -X PUT \ - "https://www.googleapis.com/upload/chromewebstore/v1.1/items/${{ secrets.CHROME_EXTENSION_ID }}" \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -H "x-goog-api-version: 2" \ - -T dist/hanzo-ai-chrome-v${VERSION}.zip - curl -sf -X POST \ - "https://www.googleapis.com/chromewebstore/v1.1/items/${{ secrets.CHROME_EXTENSION_ID }}/publish" \ - -H "Authorization: Bearer $ACCESS_TOKEN" \ - -H "x-goog-api-version: 2" \ - -H "Content-Length: 0" - continue-on-error: true - - - name: Publish to Firefox Add-ons - if: needs.secrets.outputs.has-firefox == 'true' - working-directory: packages/browser - run: | - npx web-ext sign \ - --source-dir dist/browser-extension/firefox \ - --api-key "${{ secrets.AMO_API_KEY }}" \ - --api-secret "${{ secrets.AMO_API_SECRET }}" \ - --channel listed \ - --id "hanzo-ai@hanzo.ai" - continue-on-error: true - # ─── Safari (macOS + iOS) ─── safari: name: Safari (macOS + iOS) @@ -228,18 +192,6 @@ jobs: name: hanzo-ai-ide path: packages/vscode/release-assets/* - - name: Publish to VS Code Marketplace - if: needs.secrets.outputs.has-vsce == 'true' - working-directory: packages/vscode - run: vsce publish -p ${{ secrets.VSCE_PAT }} - continue-on-error: true - - - name: Publish to Open VSX - if: needs.secrets.outputs.has-vsce == 'true' - working-directory: packages/vscode - run: ovsx publish *.vsix -p ${{ secrets.OVSX_PAT }} - continue-on-error: true - # ─── npm (all @hanzo/* workspace packages) ─── npm: name: npm @@ -318,14 +270,6 @@ jobs: name: hanzo-ai-jetbrains path: /tmp/hanzo-ai-jetbrains-v*.zip - - name: Publish to JetBrains Marketplace - if: needs.secrets.outputs.has-jetbrains == 'true' - working-directory: packages/jetbrains - run: ./gradlew publishPlugin - env: - PUBLISH_TOKEN: ${{ secrets.JETBRAINS_TOKEN }} - continue-on-error: true - # ─── Microsoft Office add-in (Word / Excel / PowerPoint) ─── office: name: Office Add-in diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..567f66d --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,35 @@ +# Mirror GitHub → Hanzo Git (git.hanzo.ai) — the ONLY thing GitHub does as a +# system of record. Hanzo Git is the source forge; its act_runners fire the +# native pipeline (.gitea/workflows/*). GitHub is a public mirror. +# +# Credential: HANZO_GIT_TOKEN — a low-privilege Hanzo-Git push token, the ONE +# secret GitHub still holds. It is NOT a store/publish credential (those live in +# KMS only). Absent the token this no-ops with a warning, so it never blocks a +# push before the operator seeds it. Alternatively the operator enables Gitea's +# native pull-mirror server-side and this workflow becomes unnecessary. +name: sync + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +jobs: + mirror: + runs-on: hanzo-build-linux-amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Push to git.hanzo.ai + env: + HANZO_GIT_TOKEN: ${{ secrets.HANZO_GIT_TOKEN }} + run: | + if [ -z "$HANZO_GIT_TOKEN" ]; then + echo "::warning::HANZO_GIT_TOKEN unset — mirror skipped. Seed it (or enable Gitea pull-mirror) to activate the native forge." + exit 0 + fi + git push --prune "https://x-access-token:${HANZO_GIT_TOKEN}@git.hanzo.ai/hanzoai/extension.git" \ + "+refs/remotes/origin/main:refs/heads/main" "+refs/tags/*:refs/tags/*" diff --git a/LLM.md b/LLM.md index e5c2902..ff8b0b3 100644 --- a/LLM.md +++ b/LLM.md @@ -30,6 +30,19 @@ per-browser manifests come from `zapd install-host` (`zap-proto/zapd`). Patch only (X.Y.Z+1), never major. `package.json` is the source of truth; `build.js` stamps it into every manifest (chrome/firefox/safari). +## CI/CD (native — Hanzo Git, KMS, act_runner) +Git of record is `git.hanzo.ai/hanzoai/extension`; GitHub is a mirror. +- `.gitea/workflows/*` — native pipeline on `hanzo-build-linux-amd64` act_runners: + `ci.yml` (amd64 test + build), `publish.yml` (Chrome Web Store + Firefox AMO + npm). +- Secrets are **KMS only**. The pipeline logs in with the KMS machine identity + (`KMS_CLIENT_ID`/`KMS_CLIENT_SECRET`) and pulls store creds from `hanzo:/extension-publish` + (env `prod`). CR: `deploy/kms/extension-publish-kms-sync.yaml`. No store cred in + any Actions secret store. +- `.github/workflows/` mirrors + degrades gracefully: `sync.yml` pushes main+tags to + Hanzo Git; `ci.yml`/`cross-platform-e2e.yml` keep the macOS(Safari)/Windows/matrix + lanes that still need GitHub-hosted runners; `publish.yml` keeps only the GitHub + Release + the interim npm publish (store-publish removed → native). See PUBLISHING.md. + ## Cross-platform — WXT migration (canonical plan, not yet executed) One WebExtension codebase, every engine. **WXT** = build/SDK layer (build matrix, diff --git a/PUBLISHING.md b/PUBLISHING.md index 8dbb98c..8c8c858 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,33 +1,48 @@ # Publishing Hanzo Extensions -This document describes how to publish Hanzo extensions to various marketplaces. +Publishing is NATIVE: it runs on Hanzo Git (`git.hanzo.ai`) act_runners, and +every store credential lives in Hanzo KMS. GitHub only mirrors the repo. -## Required Secrets +## Topology (one way) -Add these secrets to your GitHub repository settings: +- **Git of record:** `git.hanzo.ai/hanzoai/extension`. GitHub is a public + mirror; `.github/workflows/sync.yml` pushes `main` + tags to Hanzo Git. +- **CI/CD:** `.gitea/workflows/*` on `hanzo-build-linux-amd64` act_runners — + `ci.yml` (test + build) and `publish.yml` (Chrome Web Store + Firefox AMO + npm). +- **Secrets — KMS ONLY.** The native pipeline logs in with the KMS machine + identity (`KMS_CLIENT_ID` / `KMS_CLIENT_SECRET`) and pulls store creds from + org `hanzo`, env `prod`, path `/extension-publish` + (see `deploy/kms/extension-publish-kms-sync.yaml`). No store credential is + ever a GitHub or Gitea Actions secret. +- **amd64/Linux publishes natively.** Safari (macOS) + desktop (Windows) stay on + GitHub-hosted runners until native macOS/Windows act_runners are green + (`universe/docs/architecture/runners-act-migration.md`). -| Secret | Description | How to Get | -|--------|-------------|------------| -| `VSCE_PAT` | VS Code Marketplace token | [Create PAT](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token) | -| `OVSX_PAT` | Open VSX Registry token | [Create token](https://open-vsx.org/user-settings/tokens) | -| `NPM_TOKEN` | npm publish token | `npm token create` or [npm.js tokens](https://www.npmjs.com/settings/~/tokens) | -| `JETBRAINS_TOKEN` | JetBrains Marketplace token | [Generate token](https://plugins.jetbrains.com/author/me/tokens) | -| `CERTIFICATE_CHAIN` | JetBrains plugin signing cert | [Plugin signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.html) | -| `PRIVATE_KEY` | JetBrains plugin signing key | See above | -| `PRIVATE_KEY_PASSWORD` | JetBrains key password | See above | +## KMS keys the operator seeds (path `hanzo:/extension-publish`, env `prod`) + +| Key(s) | Store | +|--------|-------| +| `CHROME_EXTENSION_ID`, `CHROME_CLIENT_ID`, `CHROME_CLIENT_SECRET`, `CHROME_REFRESH_TOKEN` | Chrome Web Store | +| `AMO_API_KEY`, `AMO_API_SECRET` | Firefox Add-ons | +| `NPM_TOKEN` | npm (`@hanzo/*`) | ## Publishing Methods -### Automatic (GitHub Actions) +### Automatic (native) -1. Create a GitHub release with a version tag (e.g., `v1.6.0`) -2. The `publish.yml` workflow will automatically publish to all marketplaces +Push a `vX.Y.Z` tag. GitHub mirrors it to Hanzo Git, which fires +`.gitea/workflows/publish.yml`. Or run `workflow_dispatch` on Hanzo Git. -Or manually trigger: ```bash -gh workflow run publish.yml +# version lives in packages/browser/package.json (patch only, never major) +git tag v1.9.35 && git push origin v1.9.35 ``` +> Interim: the live npm publish still also runs from `.github/workflows/publish.yml` +> until the KMS `NPM_TOKEN` is seeded and one native `publish.yml` run is verified +> green; then the GitHub npm job is deleted. `pnpm publish` skips versions already +> on npm, so the overlap is idempotent. + ### Manual Publishing #### VS Code Marketplace diff --git a/deploy/kms/extension-publish-kms-sync.yaml b/deploy/kms/extension-publish-kms-sync.yaml new file mode 100644 index 0000000..9a28c59 --- /dev/null +++ b/deploy/kms/extension-publish-kms-sync.yaml @@ -0,0 +1,68 @@ +# Store-publish credentials for the Hanzo browser extension — KMS is the ONLY home. +# +# The native publish pipeline (.gitea/workflows/publish.yml, act_runner on +# git.hanzo.ai) reads these at publish time. No store credential lives in a +# GitHub or Gitea Actions secret — the ONLY bootstrap credential is the KMS +# machine identity (KMS_CLIENT_ID / KMS_CLIENT_SECRET), already provisioned. +# +# ── Operator steps (the VALUES are the store-account owner's; not in git) ── +# 1. KMS (kms.hanzo.ai) → org `hanzo`, env `prod`, project path `/extension-publish`. +# Add the keys below. These are the Chrome Web Store + Firefox AMO +# credentials — the store-account owner drops the real values here: +# CHROME_EXTENSION_ID Chrome Web Store item id +# CHROME_CLIENT_ID Google OAuth client id (CWS API) +# CHROME_CLIENT_SECRET Google OAuth client secret +# CHROME_REFRESH_TOKEN Google OAuth refresh token +# AMO_API_KEY addons.mozilla.org JWT issuer +# AMO_API_SECRET addons.mozilla.org JWT secret +# Optional (same path, if these stores are moved off GitHub too): +# VSCE_PAT OVSX_PAT JETBRAINS_TOKEN NPM_TOKEN +# kms secret set --project hanzo --env prod --path /extension-publish \ +# CHROME_EXTENSION_ID "" # …repeat per key +# 2. Grant the extension repo's KMS machine identity (the KMS_CLIENT_ID it +# already uses) READ on project `hanzo`, path `/extension-publish`. +# 3. Re-provision KMS_CLIENT_ID / KMS_CLIENT_SECRET as git.hanzo.ai Actions +# secrets so the native runner can log in (they exist on GitHub today — +# runners-act-migration.md Phase 1.2). +# +# Canonical GitOps home is universe/infra/k8s/kms-canonical-crs/hanzo/ (generated +# by scripts/kms-canonical-v1alpha1-gen.py). This in-repo copy is the extension's +# declared secret contract — reflect it into universe, do not diverge. +--- +apiVersion: secrets.lux.network/v1alpha1 +kind: KMSSecret +metadata: + name: extension-publish-kms-sync + namespace: hanzo + labels: + app.kubernetes.io/name: extension + app.kubernetes.io/component: kms-secret + app.kubernetes.io/part-of: hanzo-universe + kms.hanzo.ai/org: hanzo + kms.hanzo.ai/env: prod +spec: + hostAPI: http://kms.hanzo.svc + resyncInterval: 60 + authentication: + universalAuth: + # Shared platform machine identity (same as the other hanzo CRs). The + # operator scopes it to read /extension-publish. + credentialsRef: + secretName: hanzo-platform-iam-creds + secretNamespace: hanzo + secretsScope: + projectSlug: hanzo + envSlug: prod + secretsPath: /extension-publish + keys: + - CHROME_EXTENSION_ID + - CHROME_CLIENT_ID + - CHROME_CLIENT_SECRET + - CHROME_REFRESH_TOKEN + - AMO_API_KEY + - AMO_API_SECRET + managedSecretReference: + secretName: extension-publish-secrets + secretNamespace: hanzo + secretType: Opaque + creationPolicy: Orphan