Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 175 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,103 @@ on:
push:
branches: [main]
workflow_dispatch:
schedule:
- cron: "17 3 * * *"

# Hold the production lane through publication and verification. Older queued
# runs must also pass the current-main guard before they can publish or deploy.
concurrency:
group: telemetry-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'production' }}
cancel-in-progress: false

permissions:
contents: read

jobs:
discover:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
statuses: read
outputs:
refresh: ${{ steps.plan.outputs.refresh }}
deploy: ${{ steps.plan.outputs.deploy }}
plan: ${{ steps.plan.outputs.plan }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- id: plan
env:
GH_TOKEN: ${{ github.token }}
run: node scripts/vocabulary-maintenance.mjs plan --base "$GITHUB_SHA" --event "$GITHUB_EVENT_NAME"

generate:
needs: discover
if: needs.discover.outputs.refresh == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
path: telemetry
persist-credentials: false
- uses: actions/checkout@v7
with:
repository: openclaw/openclaw
ref: ${{ fromJSON(needs.discover.outputs.plan).releases[0].revision }}
path: upstream-source
fetch-depth: 0
sparse-checkout: package.json
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
working-directory: telemetry
# This imports released upstream packaging code. No write token or
# Cloudflare secret is available; the next job checks only its data output.
- env:
VOCABULARY_PLAN: ${{ needs.discover.outputs.plan }}
run: node scripts/vocabulary-maintenance.mjs generate --source ../upstream-source
working-directory: telemetry
- uses: actions/upload-artifact@v7
with:
name: generated-vocabulary
if-no-files-found: error
retention-days: 1
path: |
telemetry/data/public-vocabulary.json
telemetry/src/public-vocabulary.ts

check:
needs: [discover, generate]
if: >-
always() && !cancelled() &&
(github.event_name == 'pull_request' ||
(needs.discover.result == 'success' && needs.discover.outputs.deploy == 'true' &&
(needs.generate.result == 'success' || needs.generate.result == 'skipped')))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: actions/download-artifact@v8
if: needs.discover.outputs.refresh == 'true'
with:
name: generated-vocabulary
path: ${{ runner.temp }}/vocabulary
- if: needs.discover.outputs.refresh == 'true'
env:
VOCABULARY_PLAN: ${{ needs.discover.outputs.plan }}
run: node scripts/vocabulary-maintenance.mjs apply --artifact "$RUNNER_TEMP/vocabulary"
- run: npm ci
- run: npm run check
- name: Check vocabulary covers the latest OpenClaw release
Expand All @@ -28,17 +113,104 @@ jobs:
release_sha="$(gh api "repos/openclaw/openclaw/commits/$release_tag" --jq .sha)"
npm run vocabulary:check -- --release-revision "$release_sha"
- run: npx wrangler deploy --dry-run
- uses: actions/upload-artifact@v7
if: needs.discover.outputs.refresh == 'true'
with:
name: checked-vocabulary
if-no-files-found: error
retention-days: 1
path: |
data/public-vocabulary.json
src/public-vocabulary.ts

publish:
needs: [discover, check]
if: needs.discover.outputs.refresh == 'true' && needs.check.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
sha: ${{ steps.commit.outputs.sha }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- uses: actions/download-artifact@v8
with:
name: checked-vocabulary
path: ${{ runner.temp }}/vocabulary
# No dependency installation or producer-supplied executable runs here.
- env:
VOCABULARY_PLAN: ${{ needs.discover.outputs.plan }}
run: node scripts/vocabulary-maintenance.mjs apply --artifact "$RUNNER_TEMP/vocabulary"
- id: commit
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
node scripts/vocabulary-maintenance.mjs guard-main --target "$GITHUB_SHA"
changed="$(git diff --name-only)"
test -n "$changed"
test -z "$(printf '%s\n' "$changed" | grep -Ev '^(data/public-vocabulary.json|src/public-vocabulary.ts)$')"
git diff --check
git add -- data/public-vocabulary.json src/public-vocabulary.ts
git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
-c core.hooksPath=/dev/null commit -m 'chore(telemetry): refresh released public vocabulary'
gh auth setup-git
git -c gc.auto=0 -c maintenance.auto=false push origin HEAD:refs/heads/main
sha="$(git rev-parse HEAD)"
node scripts/vocabulary-maintenance.mjs guard-main --target "$sha"
printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT"

deploy:
needs: check
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: [discover, check, publish]
if: >-
always() && !cancelled() && github.event_name != 'pull_request' &&
github.ref == 'refs/heads/main' && needs.discover.outputs.deploy == 'true' &&
needs.check.result == 'success' &&
(needs.publish.result == 'success' ||
(needs.publish.result == 'skipped' && needs.discover.outputs.refresh == 'false'))
runs-on: ubuntu-latest
permissions:
contents: read
statuses: write
env:
TARGET_SHA: ${{ needs.publish.outputs.sha || github.sha }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ env.TARGET_SHA }}
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npx wrangler deploy
- id: admitted
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$TARGET_SHA"
node scripts/vocabulary-maintenance.mjs guard-main --target "$TARGET_SHA"
gh api "repos/openclaw/telemetry/statuses/$TARGET_SHA" --method POST \
-f state=pending -f context=telemetry/deploy \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --silent
- id: deployed
run: |
npx wrangler deploy --tag "$TARGET_SHA"
node scripts/vocabulary-maintenance.mjs verify-deployment --target "$TARGET_SHA"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# GITHUB_TOKEN's data commit does not trigger another push run. This same
# run deploys it, and a failed/missing receipt makes the next poll retry.
- if: always() && steps.admitted.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
DEPLOY_RESULT: ${{ steps.deployed.outcome == 'success' && 'success' || 'failure' }}
run: |
gh api "repos/openclaw/telemetry/statuses/$TARGET_SHA" --method POST \
-f state="$DEPLOY_RESULT" -f context=telemetry/deploy \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --silent
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Maintain public vocabulary with daily checks of immutable stable releases, retained backfill, isolated generation, constrained publication, serialized deployment, and retryable exact-commit rollout verification.
- Preserve all reported public channel, provider, and plugin names instead of truncating each list to 32 before validation; keep upload and Analytics Engine byte limits covered by regression tests.
- Refresh retained public names from OpenClaw 2026.9.7, including GitHub, QuickJS Code Mode, and Session Share; require a reviewed snapshot for the latest published release in the existing CI check.
- Accept strictly validated, identifier-free update outcomes in a separate, explicitly configured dataset; add a side-effect-free capability check while keeping production collection unbound. Thanks @roboclaw-bot, @fuller-stack-dev, and @vincentkoc.
Expand Down
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ npm run deploy # requires Cloudflare credentials for the OpenClaw account

Pull requests run the typecheck, tests, a public-vocabulary release check, and a Wrangler dry-run build
using the committed lockfile. The release check resolves the latest published OpenClaw release tag
to its commit and fails if that commit has no reviewed vocabulary snapshot. GitHub lookup failures
to its commit and fails if that commit has no retained vocabulary snapshot. GitHub lookup failures
fail the check; they do not report the vocabulary as fresh.
Deploys run from GitHub Actions on pushes to `main` (see
Deploys run from GitHub Actions on pushes to `main`, manual runs, and vocabulary maintenance (see
[`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)), using the `CLOUDFLARE_API_TOKEN`
repository secret.

Expand All @@ -250,8 +250,50 @@ retained snapshots, and the public source of legacy aliases (`cli`, `claude`, `g
[`src/public-vocabulary.ts`](src/public-vocabulary.ts) exports the complete retained `PUBLIC_NAMES`
for ingestion and offline analysis. Neither file contains names learned from telemetry requests.

Before supporting a new OpenClaw release or catalog revision, use Node.js 24 and a trusted local
OpenClaw Git repository containing the candidate commit and its history:
GitHub Actions polls published stable OpenClaw releases once daily at 03:17 UTC. It fully paginates
release metadata and resolves annotated tags to immutable commits. `releases` records the release
identity, tag, commit, and publication time. Automatic bundled-release backfill starts with
`v2026.9.7`, published on September 30, 2026; the older retained snapshots and catalog history remain
intact. This does not establish complete coverage of transient bundled names from earlier releases.
Drafts and prereleases are excluded. Every missed stable release since the inclusive anchor is
processed, including releases with the same publication timestamp.

The existing Deploy workflow owns the whole update:

1. Discovery makes metadata requests only. An unchanged scheduled run stops before installing
dependencies or fetching source when current main already has a successful `telemetry/deploy`
status. Missing, pending, and failed statuses retry validation and deployment. Manual runs also
provide recovery without a source change.
2. Generation runs with read-only repository permission, no persisted checkout credentials, and no
Cloudflare secret. It invokes the existing generator against immutable public Git objects.
3. A fresh read-only job validates only the two generated files against trusted repository code,
then runs the normal checks, complete-vocabulary byte-budget tests, and Wrangler dry-run.
4. A separate job can publish only `data/public-vocabulary.json` and `src/public-vocabulary.ts`.
It checks the artifact again without installing dependencies or executing producer-supplied code.
Existing snapshots and aliases cannot change. Publication is a fast-forward from the exact
discovered main commit; concurrent changes stop it instead of overwriting source.
5. The existing deployment job checks out the exact checked or published commit. All production
runs share one concurrency group, and stale runs fail the current-main guard. The job tags the
Worker with its commit and verifies that the newest deployment sends 100% of traffic to that
exact tagged version before recording success. It retains the existing trusted npm/Wrangler
toolchain; the artifact cannot replace package files, scripts, or workflow code.

Generation never learns names from client requests. The publisher alone receives `contents: write`;
the deployment job alone receives `statuses: write` and the existing `CLOUDFLARE_API_TOKEN`.
No new credential or cross-repository write is required. A commit made with `GITHUB_TOKEN` does not
start the normal push workflow, so its checked deployment happens in the same run.

Merging this workflow into main enables daily generation, data-only publication, and deployment.
Review that operational authority before activation. Coverage can lag until the next daily run plus
GitHub queue delays. GitHub can disable schedules in inactive public repositories. Changed upstream
metadata contracts, moved or deleted tags, incomplete pagination, byte-budget growth, and blocked
fast-forward publication fail visibly and still need maintainer review; automation does not remove
those maintenance boundaries. A failed rollout leaves a retryable status even if its source commit
was already published. Inspect the failed Deploy run, repair the named contract or permission, then
run Deploy manually from current main. Do not weaken the allowlist or force-push past a guard.

For a reviewed manual repair, use Node.js 24 and a trusted local OpenClaw Git repository containing
the candidate commit and its history:

```bash
npm run vocabulary:check -- --source <openclaw-repository> --revision <full-public-commit-sha>
Expand Down Expand Up @@ -280,7 +322,7 @@ The initial snapshot includes all catalog revisions on the public main history s
`844e781ca40952c98ee997b016e3cc5d2f12f9f3`, before name allowlisting began in August 2026.
Refreshes append snapshots; never remove older ones during routine updates. This retains removed or
renamed public entries, including names admitted by the older
moving-catalog implementation. New public names remain rejected until reviewed metadata is deployed.
moving-catalog implementation. New public names remain rejected until validated metadata is deployed.
The vocabulary is compiled into the Worker. Loading it requires neither upstream requests nor
Cache API access, so old allowlist cache entries cannot be reused and cache outages cannot interrupt
name validation. Ingestion checks the compiled vocabulary without exposing its mutable set.
Expand Down
8 changes: 8 additions & 0 deletions data/public-vocabulary.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,5 +600,13 @@
"zoom-meetings"
]
}
],
"releases": [
{
"id": "RE_kwDOQb6kR84X0tXI",
"tag": "v2026.9.7",
"revision": "c074824a27c96d3983043f9eeb33823cd1772d8c",
"publishedAt": "2026-09-30T04:44:14Z"
}
]
}
6 changes: 4 additions & 2 deletions scripts/public-vocabulary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { readProviderOverlays } from "./lib/public-provider-overlays.mjs";

const ROOT = fileURLToPath(new URL("../", import.meta.url));
const METADATA = join(ROOT, "data/public-vocabulary.json");
Expand Down Expand Up @@ -46,6 +45,9 @@ function collectCatalog(body, names) {

/** Read immutable Git objects, never the source checkout's working files. */
export async function buildSnapshot(source, revision, catalogHistoryStart) {
// Publication uses the deterministic renderer without loading parser dependencies
// or executing the upstream packaging helper. Only generation needs this import.
const { readProviderOverlays } = await import("./lib/public-provider-overlays.mjs");
if (!SHA.test(revision) || !SHA.test(catalogHistoryStart)) {
throw new Error("Use full immutable commit SHAs for revision and history start");
}
Expand Down Expand Up @@ -144,7 +146,7 @@ export function renderVocabulary(metadata) {
export function assertReleaseCoverage(metadata, revision) {
if (!SHA.test(revision)) throw new Error("Use a full immutable released commit SHA");
if (!metadata.snapshots.some((snapshot) => snapshot.revision === revision)) {
throw new Error(`Released OpenClaw revision ${revision} has no reviewed vocabulary snapshot; refresh public metadata`);
throw new Error(`Released OpenClaw revision ${revision} has no retained vocabulary snapshot; refresh public metadata`);
}
}

Expand Down
Loading
Loading