{"kind":"Skill","metadata":{"namespace":"community","name":"openclaw-release-ci","version":"0.1.0"},"spec":{"description":"Run, watch, debug, and summarize OpenClaw full release CI, release checks, live provider gates, install/update proofs, and release-secret preflights.","files":{"SKILL.md":"---\nname: openclaw-release-ci\ndescription: \"Run, watch, debug, and summarize OpenClaw full release CI, release checks, live provider gates, install/update proofs, and release-secret preflights.\"\n---\n\n# OpenClaw Release CI\n\nUse this with `$openclaw-release-maintainer` and `$openclaw-testing` when a release candidate needs full validation, install/update proof, live provider checks, or CI recovery.\n\n## Guardrails\n\n- No version bump, tag, npm publish, GitHub release, or release promotion without explicit operator approval.\n- Validate provider secrets before dispatching expensive full release matrices.\n- Do not set GitHub secrets from unvalidated 1Password candidates. If a candidate returns 401/403, leave the existing secret alone and report the exact missing provider.\n- Use `$one-password` for secret reads/writes: one persistent tmux session, targeted items only, no secret output.\n- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.\n- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.\n- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.\n\n## Preflight\n\nBefore full release validation:\n\n```bash\nnode .agents/skills/openclaw-release-ci/scripts/verify-provider-secrets.mjs --required openai,anthropic,fireworks\ngh api rate_limit --jq '.resources.core'\ngit status --short --branch\ngit rev-parse HEAD\n```\n\nIf env lacks keys, use `$one-password` to inject or set them, then rerun the script. The script prints only provider status and HTTP class, never tokens.\n\n## Dispatch\n\nPrefer the trusted workflow on `main`, target the exact release SHA:\n\n```bash\ngh workflow run full-release-validation.yml \\\n  --repo openclaw/openclaw \\\n  --ref main \\\n  -f ref=\u003crelease-sha\u003e \\\n  -f provider=openai \\\n  -f mode=both \\\n  -f release_profile=full \\\n  -f rerun_group=all\n```\n\nUse `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.\n\n## Watch\n\nUse the summary helper instead of repeated raw polling:\n\n```bash\nnode .agents/skills/openclaw-release-ci/scripts/release-ci-summary.mjs \u003cfull-release-run-id\u003e\n```\n\nThen watch only when useful:\n\n```bash\ngh run watch \u003cfull-release-run-id\u003e --repo openclaw/openclaw --exit-status\n```\n\nStop watchers before ending the turn or switching strategy.\n\n## Failure Triage\n\n1. Confirm parent SHA and child run IDs.\n2. List failed jobs only:\n   ```bash\n   gh run view \u003cchild-run-id\u003e --repo openclaw/openclaw --json jobs \\\n     --jq '.jobs[] | select(.conclusion==\"failure\" or .conclusion==\"timed_out\" or .conclusion==\"cancelled\") | [.databaseId,.name,.conclusion,.url] | @tsv'\n   ```\n3. Fetch one failed job log. If rate-limited, note reset time and avoid more REST calls.\n4. For secret-looking failures, validate the provider endpoint from the same secret source before editing code.\n5. For live-cache failures, inspect whether it is missing/invalid key, empty text, provider refusal, timeout, or baseline miss. Do not weaken release gates without clear provider evidence.\n6. Fix narrowly, run local/changed proof, commit, push, rerun the smallest matching group.\n\n## Evidence\n\nRecord:\n\n- release SHA\n- full parent run URL\n- child run IDs and conclusions: CI, Release Checks, Plugin Prerelease, NPM Telegram\n- targeted local proof commands\n- provider-secret preflight result\n- known gaps or unrelated failures\n\nFor lessons and recovery patterns, read `references/release-ci-notes.md`.\n","agents/openai.yaml":"interface:\n  display_name: \"OpenClaw Release CI\"\n  short_description: \"Verify and debug OpenClaw release validation runs\"\n  default_prompt: \"Use $openclaw-release-ci to preflight provider secrets, watch full release validation, summarize child runs, and triage only failing release lanes.\"\n","references/release-ci-notes.md":"# Release CI Notes\n\n## What Went Wrong\n\n- Full validation was started before all provider keys were proven valid.\n- GitHub secret presence was confused with key validity.\n- Repeated `gh run view` and log fetches exhausted REST quota.\n- Parent run state was less useful than child run evidence.\n- Live-cache failures needed structured classification: invalid key, empty provider output, timeout, or real cache regression.\n- Background watchers accumulated and made interruption recovery harder.\n\n## Better Defaults\n\n- Run provider-secret preflight first. Require real `/models` or equivalent endpoint checks for release-blocking providers.\n- Keep one watcher open. Use child summaries every few minutes, not every few seconds.\n- Fetch failed-job logs only after a job reaches a terminal failing state.\n- Prefer narrow `rerun_group` recovery after a focused fix.\n- Leave bad secrets unset. A 401 candidate from 1Password should not overwrite GitHub.\n- Make the final release evidence note durable: parent URL, child run URLs, SHA, command proof, and gaps.\n\n## Secret Handling Pattern\n\n- Use `$one-password`; never run broad env dumps.\n- Search exact item titles or known ids.\n- Validate candidates without printing values.\n- Set GitHub secrets only after endpoint validation succeeds.\n- After setting, verify metadata with `gh secret list`, not value output.\n\n## Live Cache Pattern\n\n- Empty text with token usage is a provider/output issue until proven otherwise.\n- Retry lane-level mismatches once with a fresh session id.\n- Keep cache baselines strict, but log enough structured usage to distinguish cache miss from response mismatch.\n- If a provider key validates locally but fails in Actions, inspect whether the workflow reads the expected secret name.\n\n## Quota-Safe GitHub Pattern\n\n- Check `gh api rate_limit --jq '.resources.core'` before log-heavy work.\n- Use one child-run listing call, then inspect failed jobs only.\n- If remaining quota is low, pause until reset; do not keep polling.\n- Prefer GraphQL only for metadata when REST is exhausted; logs still need REST.\n","scripts/release-ci-summary.mjs":"#!/usr/bin/env node\nimport { execFileSync } from \"node:child_process\";\nimport process from \"node:process\";\n\nconst runId = process.argv[2];\nconst repo = process.env.OPENCLAW_RELEASE_REPO || \"openclaw/openclaw\";\n\nif (!runId) {\n  console.error(\"usage: release-ci-summary.mjs \u003cfull-release-run-id\u003e\");\n  process.exit(2);\n}\n\nfunction gh(args) {\n  return execFileSync(\"gh\", args, {\n    encoding: \"utf8\",\n    stdio: [\"ignore\", \"pipe\", \"pipe\"],\n  });\n}\n\nfunction jsonGh(args) {\n  return JSON.parse(gh(args));\n}\n\nfunction rate() {\n  try {\n    return jsonGh([\"api\", \"rate_limit\"]).resources.core;\n  } catch {\n    return undefined;\n  }\n}\n\nconst core = rate();\nif (core) {\n  const reset = new Date(core.reset * 1000).toISOString();\n  console.log(`rate: remaining=${core.remaining}/${core.limit} reset=${reset}`);\n  if (core.remaining \u003c 20) {\n    console.error(\"rate too low for CI summary; wait for reset before polling\");\n    process.exit(3);\n  }\n}\n\nconst parent = jsonGh([\n  \"run\",\n  \"view\",\n  runId,\n  \"--repo\",\n  repo,\n  \"--json\",\n  \"status,conclusion,createdAt,headSha,url,jobs\",\n]);\n\nconsole.log(`parent: ${runId} ${parent.status}/${parent.conclusion || \"none\"}`);\nconsole.log(`sha: ${parent.headSha}`);\nconsole.log(`url: ${parent.url}`);\n\nfor (const job of parent.jobs ?? []) {\n  const marker = job.conclusion || job.status;\n  console.log(`parent-job: ${marker} ${job.name}`);\n}\n\nconst since = parent.createdAt;\nconst runList = gh([\n  \"api\",\n  `repos/${repo}/actions/runs?per_page=100`,\n  \"--jq\",\n  `.workflow_runs[] | select(.created_at \u003e= \"${since}\") | select(.name==\"CI\" or .name==\"OpenClaw Release Checks\" or .name==\"Plugin Prerelease\" or .name==\"NPM Telegram Beta E2E\" or .name==\"Full Release Validation\") | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv`,\n]).trim();\n\nif (!runList) {\n  console.log(\"children: none found yet\");\n  process.exit(0);\n}\n\nconsole.log(\"children:\");\nfor (const line of runList.split(\"\\n\")) {\n  const [id, name, status, conclusion, sha, url] = line.split(\"\\t\");\n  console.log(`child: ${id} ${name} ${status}/${conclusion || \"none\"} sha=${sha}`);\n  console.log(`child-url: ${url}`);\n}\n","scripts/verify-provider-secrets.mjs":"#!/usr/bin/env node\nimport process from \"node:process\";\n\nconst args = new Map();\nfor (let index = 2; index \u003c process.argv.length; index += 1) {\n  const arg = process.argv[index];\n  if (!arg.startsWith(\"--\")) continue;\n  const [key, inlineValue] = arg.slice(2).split(\"=\", 2);\n  const value = inlineValue ?? process.argv[index + 1];\n  if (inlineValue === undefined) index += 1;\n  args.set(key, value);\n}\n\nconst requiredInput = String(args.get(\"required\") ?? \"openai,anthropic\").trim();\nconst required = new Set(\n  (requiredInput.toLowerCase() === \"none\" ? \"\" : requiredInput)\n    .split(\",\")\n    .map((entry) =\u003e entry.trim().toLowerCase())\n    .filter(Boolean),\n);\n\nconst timeoutMs = Number(args.get(\"timeout-ms\") ?? 10_000);\n\nfunction envFirst(names) {\n  for (const name of names) {\n    const value = process.env[name]?.trim();\n    if (value) return { name, value };\n  }\n  return undefined;\n}\n\nasync function checkProvider(id, config) {\n  const secret = envFirst(config.env);\n  if (!secret) {\n    return { id, ok: false, status: \"missing\", env: config.env.join(\"|\") };\n  }\n\n  const controller = new AbortController();\n  const timer = setTimeout(() =\u003e controller.abort(), timeoutMs);\n  try {\n    const headers = config.headers(secret.value);\n    const response = await fetch(config.url, {\n      headers,\n      signal: controller.signal,\n    });\n    return {\n      id,\n      ok: response.ok,\n      status: response.ok ? \"ok\" : `http_${response.status}`,\n      env: secret.name,\n    };\n  } catch (error) {\n    return {\n      id,\n      ok: false,\n      status: error?.name === \"AbortError\" ? \"timeout\" : \"error\",\n      env: secret.name,\n    };\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\nconst providers = {\n  openai: {\n    env: [\"OPENAI_API_KEY\"],\n    url: \"https://api.openai.com/v1/models\",\n    headers: (token) =\u003e ({ authorization: `Bearer ${token}` }),\n  },\n  anthropic: {\n    env: [\"ANTHROPIC_API_KEY\", \"ANTHROPIC_API_TOKEN\"],\n    url: \"https://api.anthropic.com/v1/models\",\n    headers: (token) =\u003e ({\n      \"anthropic-version\": \"2023-06-01\",\n      \"x-api-key\": token,\n    }),\n  },\n  fireworks: {\n    env: [\"FIREWORKS_API_KEY\"],\n    url: \"https://api.fireworks.ai/inference/v1/models\",\n    headers: (token) =\u003e ({ authorization: `Bearer ${token}` }),\n  },\n  openrouter: {\n    env: [\"OPENROUTER_API_KEY\"],\n    url: \"https://openrouter.ai/api/v1/models\",\n    headers: (token) =\u003e ({ authorization: `Bearer ${token}` }),\n  },\n};\n\nconst unknown = [...required].filter((id) =\u003e !providers[id]);\nif (unknown.length \u003e 0) {\n  console.error(`unknown providers: ${unknown.join(\",\")}`);\n  process.exit(2);\n}\n\nconst results = [];\nfor (const id of Object.keys(providers)) {\n  if (required.has(id) || envFirst(providers[id].env)) {\n    results.push(await checkProvider(id, providers[id]));\n  }\n}\n\nlet failed = false;\nfor (const result of results) {\n  const requiredLabel = required.has(result.id) ? \"required\" : \"optional\";\n  console.log(`${result.id}: ${result.status} env=${result.env} ${requiredLabel}`);\n  if (required.has(result.id) \u0026\u0026 !result.ok) failed = true;\n}\n\nif (failed) {\n  console.error(\"release provider secret preflight failed\");\n  process.exit(1);\n}\n"},"import":{"commit_sha":"424c6d0a5f4665b803ad6768d08b0be7659deaf4","imported_at":"2026-05-18T20:13:36Z","license_text":"MIT License\n\nCopyright (c) 2025 Peter Steinberger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","owner":"openclaw","repo":"openclaw/openclaw","source_url":"https://github.com/openclaw/openclaw/tree/424c6d0a5f4665b803ad6768d08b0be7659deaf4/.agents/skills/openclaw-release-ci"}},"content_hash":[240,118,132,50,176,111,164,91,211,196,97,189,228,249,58,105,83,101,77,110,33,49,201,250,9,22,31,54,52,78,20,118],"trust_level":"unsigned","yanked":false}
