|
| 1 | +name: channel-health |
| 2 | + |
| 3 | +# End-to-end distribution-channel verification, from the USER's side. |
| 4 | +# |
| 5 | +# June 2026 postmortem: every internal pipeline was green while three |
| 6 | +# channels were silently degraded for weeks — the community-marketplace |
| 7 | +# pin served April's v0.2.9 to new plugin installs, auto-update was dead |
| 8 | +# for existing binary users, and nobody noticed because nothing checked |
| 9 | +# what a fresh user actually receives. This workflow is that check. |
| 10 | +# |
| 11 | +# Channels verified against the newest git release tags: |
| 12 | +# 1. npm latest == newest v* release |
| 13 | +# 2. Open VSX latest == newest extension-v* release |
| 14 | +# 3. plugin-repo plugin.json == newest v* release |
| 15 | +# 4. community-marketplace SHA pin == plugin-repo HEAD |
| 16 | +# (the pin is bumped by ANTHROPIC's pipeline on our plugin-repo |
| 17 | +# pushes and the public catalog syncs nightly — so a 48h grace |
| 18 | +# period applies before we alert; direct PRs to that repo are |
| 19 | +# auto-closed, it is a read-only mirror) |
| 20 | +# |
| 21 | +# On any drift: opens (or comments on) a tracking issue and fails the run. |
| 22 | + |
| 23 | +on: |
| 24 | + schedule: |
| 25 | + - cron: "0 9 * * 1,4" # Mon + Thu 09:00 UTC |
| 26 | + workflow_dispatch: |
| 27 | + |
| 28 | +jobs: |
| 29 | + check: |
| 30 | + runs-on: ubuntu-latest |
| 31 | + permissions: |
| 32 | + contents: read |
| 33 | + issues: write |
| 34 | + steps: |
| 35 | + - name: Verify every channel against the newest release tags |
| 36 | + env: |
| 37 | + GH_TOKEN: ${{ github.token }} |
| 38 | + run: | |
| 39 | + set -uo pipefail |
| 40 | + problems=() |
| 41 | +
|
| 42 | + tags="$(gh api 'repos/AxmeAI/axme-code/releases?per_page=30' --jq '.[].tag_name')" |
| 43 | + cli_tag="$(echo "$tags" | grep -E '^v[0-9]' | head -1 || true)" |
| 44 | + ext_tag="$(echo "$tags" | grep -E '^extension-v[0-9]' | head -1 || true)" |
| 45 | + if [ -z "$cli_tag" ]; then |
| 46 | + problems+=("no \`v*\` tag in the releases list — install.sh and auto-update cannot resolve a version") |
| 47 | + fi |
| 48 | + cli_ver="${cli_tag#v}" |
| 49 | + ext_ver="${ext_tag#extension-v}" |
| 50 | + echo "Newest releases: CLI ${cli_tag:-NONE}, extension ${ext_tag:-NONE}" |
| 51 | +
|
| 52 | + # 1. npm |
| 53 | + npm_ver="$(npm view @axme/code version 2>/dev/null || echo FETCH_FAILED)" |
| 54 | + if [ -n "$cli_tag" ] && [ "$npm_ver" != "$cli_ver" ]; then |
| 55 | + problems+=("npm latest is \`$npm_ver\`, newest CLI release is \`$cli_ver\`") |
| 56 | + fi |
| 57 | +
|
| 58 | + # 2. Open VSX |
| 59 | + ovsx_ver="$(curl -fsSL https://open-vsx.org/api/AxmeAI/axme-code 2>/dev/null | jq -r '.version // "FETCH_FAILED"' || echo FETCH_FAILED)" |
| 60 | + if [ -n "$ext_tag" ] && [ "$ovsx_ver" != "$ext_ver" ]; then |
| 61 | + problems+=("Open VSX latest is \`$ovsx_ver\`, newest extension release is \`$ext_ver\`") |
| 62 | + fi |
| 63 | +
|
| 64 | + # 3. Plugin repo sync |
| 65 | + plugin_ver="$(gh api repos/AxmeAI/axme-code-plugin/contents/.claude-plugin/plugin.json --jq '.content' 2>/dev/null | base64 -d | jq -r '.version' || echo FETCH_FAILED)" |
| 66 | + if [ -n "$cli_tag" ] && [ "$plugin_ver" != "$cli_ver" ]; then |
| 67 | + problems+=("plugin-repo plugin.json is \`$plugin_ver\`, newest CLI release is \`$cli_ver\`") |
| 68 | + fi |
| 69 | +
|
| 70 | + # 4. Community marketplace pin (48h grace for Anthropic's pipeline) |
| 71 | + pin="$(curl -fsSL https://raw.githubusercontent.com/anthropics/claude-plugins-community/main/.claude-plugin/marketplace.json 2>/dev/null \ |
| 72 | + | jq -r '.plugins[] | select(.name == "axme-code") | .source.sha' || echo FETCH_FAILED)" |
| 73 | + head_json="$(gh api repos/AxmeAI/axme-code-plugin/commits/main 2>/dev/null || echo '{}')" |
| 74 | + head_sha="$(echo "$head_json" | jq -r '.sha // "FETCH_FAILED"')" |
| 75 | + if [ "$pin" != "$head_sha" ]; then |
| 76 | + head_date="$(echo "$head_json" | jq -r '.commit.committer.date // empty')" |
| 77 | + head_age_h=999 |
| 78 | + if [ -n "$head_date" ]; then |
| 79 | + head_age_h=$(( ( $(date +%s) - $(date -d "$head_date" +%s) ) / 3600 )) |
| 80 | + fi |
| 81 | + if [ "$head_age_h" -ge 48 ]; then |
| 82 | + problems+=("community-marketplace pin \`${pin:0:7}\` ≠ plugin-repo HEAD \`${head_sha:0:7}\` and HEAD is ${head_age_h}h old — Anthropic's auto-bump / nightly sync has not picked it up. Direct PRs are auto-closed (read-only mirror); escalate via https://clau.de/plugin-directory-submission") |
| 83 | + else |
| 84 | + echo "marketplace pin behind HEAD, but HEAD is only ${head_age_h}h old — inside the nightly-sync grace period" |
| 85 | + fi |
| 86 | + else |
| 87 | + echo "marketplace pin current: ${pin:0:7}" |
| 88 | + fi |
| 89 | +
|
| 90 | + # --- Report --- |
| 91 | + if [ "${#problems[@]}" -gt 0 ]; then |
| 92 | + body="$(printf -- '- %s\n' "${problems[@]}")" |
| 93 | + echo "::error::Channel drift detected" |
| 94 | + echo "$body" |
| 95 | + title="Channel health: distribution drift detected" |
| 96 | + existing="$(gh issue list --repo AxmeAI/axme-code --state open --search "in:title \"$title\"" --json number --jq '.[0].number // empty')" |
| 97 | + if [ -n "$existing" ]; then |
| 98 | + gh issue comment "$existing" --repo AxmeAI/axme-code --body "$(printf 'Still failing as of %s:\n\n%s' "$(date -u +%F)" "$body")" |
| 99 | + else |
| 100 | + gh issue create --repo AxmeAI/axme-code \ |
| 101 | + --title "$title" \ |
| 102 | + --body "$(printf 'Automated channel-health check (%s) found drift between what CI released and what users actually receive:\n\n%s\n\nRunbook: see the header comments of scripts/release.sh and .github/workflows/channel-health.yml.' "$(date -u +%F)" "$body")" |
| 103 | + fi |
| 104 | + exit 1 |
| 105 | + fi |
| 106 | + echo "All channels healthy: CLI ${cli_tag:-n/a} (npm ✓, plugin repo ✓), extension ${ext_tag:-n/a} (Open VSX ✓), marketplace pin current or in grace." |
0 commit comments