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
106 changes: 106 additions & 0 deletions .github/workflows/channel-health.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: channel-health

# End-to-end distribution-channel verification, from the USER's side.
#
# June 2026 postmortem: every internal pipeline was green while three
# channels were silently degraded for weeks — the community-marketplace
# pin served April's v0.2.9 to new plugin installs, auto-update was dead
# for existing binary users, and nobody noticed because nothing checked
# what a fresh user actually receives. This workflow is that check.
#
# Channels verified against the newest git release tags:
# 1. npm latest == newest v* release
# 2. Open VSX latest == newest extension-v* release
# 3. plugin-repo plugin.json == newest v* release
# 4. community-marketplace SHA pin == plugin-repo HEAD
# (the pin is bumped by ANTHROPIC's pipeline on our plugin-repo
# pushes and the public catalog syncs nightly — so a 48h grace
# period applies before we alert; direct PRs to that repo are
# auto-closed, it is a read-only mirror)
#
# On any drift: opens (or comments on) a tracking issue and fails the run.

on:
schedule:
- cron: "0 9 * * 1,4" # Mon + Thu 09:00 UTC
workflow_dispatch:

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Verify every channel against the newest release tags
env:
GH_TOKEN: ${{ github.token }}
run: |
set -uo pipefail
problems=()

tags="$(gh api 'repos/AxmeAI/axme-code/releases?per_page=30' --jq '.[].tag_name')"
cli_tag="$(echo "$tags" | grep -E '^v[0-9]' | head -1 || true)"
ext_tag="$(echo "$tags" | grep -E '^extension-v[0-9]' | head -1 || true)"
if [ -z "$cli_tag" ]; then
problems+=("no \`v*\` tag in the releases list — install.sh and auto-update cannot resolve a version")
fi
cli_ver="${cli_tag#v}"
ext_ver="${ext_tag#extension-v}"
echo "Newest releases: CLI ${cli_tag:-NONE}, extension ${ext_tag:-NONE}"

# 1. npm
npm_ver="$(npm view @axme/code version 2>/dev/null || echo FETCH_FAILED)"
if [ -n "$cli_tag" ] && [ "$npm_ver" != "$cli_ver" ]; then
problems+=("npm latest is \`$npm_ver\`, newest CLI release is \`$cli_ver\`")
fi

# 2. Open VSX
ovsx_ver="$(curl -fsSL https://open-vsx.org/api/AxmeAI/axme-code 2>/dev/null | jq -r '.version // "FETCH_FAILED"' || echo FETCH_FAILED)"
if [ -n "$ext_tag" ] && [ "$ovsx_ver" != "$ext_ver" ]; then
problems+=("Open VSX latest is \`$ovsx_ver\`, newest extension release is \`$ext_ver\`")
fi

# 3. Plugin repo sync
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)"
if [ -n "$cli_tag" ] && [ "$plugin_ver" != "$cli_ver" ]; then
problems+=("plugin-repo plugin.json is \`$plugin_ver\`, newest CLI release is \`$cli_ver\`")
fi

# 4. Community marketplace pin (48h grace for Anthropic's pipeline)
pin="$(curl -fsSL https://raw.githubusercontent.com/anthropics/claude-plugins-community/main/.claude-plugin/marketplace.json 2>/dev/null \
| jq -r '.plugins[] | select(.name == "axme-code") | .source.sha' || echo FETCH_FAILED)"
head_json="$(gh api repos/AxmeAI/axme-code-plugin/commits/main 2>/dev/null || echo '{}')"
head_sha="$(echo "$head_json" | jq -r '.sha // "FETCH_FAILED"')"
if [ "$pin" != "$head_sha" ]; then
head_date="$(echo "$head_json" | jq -r '.commit.committer.date // empty')"
head_age_h=999
if [ -n "$head_date" ]; then
head_age_h=$(( ( $(date +%s) - $(date -d "$head_date" +%s) ) / 3600 ))
fi
if [ "$head_age_h" -ge 48 ]; then
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")
else
echo "marketplace pin behind HEAD, but HEAD is only ${head_age_h}h old — inside the nightly-sync grace period"
fi
else
echo "marketplace pin current: ${pin:0:7}"
fi

# --- Report ---
if [ "${#problems[@]}" -gt 0 ]; then
body="$(printf -- '- %s\n' "${problems[@]}")"
echo "::error::Channel drift detected"
echo "$body"
title="Channel health: distribution drift detected"
existing="$(gh issue list --repo AxmeAI/axme-code --state open --search "in:title \"$title\"" --json number --jq '.[0].number // empty')"
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo AxmeAI/axme-code --body "$(printf 'Still failing as of %s:\n\n%s' "$(date -u +%F)" "$body")"
else
gh issue create --repo AxmeAI/axme-code \
--title "$title" \
--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")"
fi
exit 1
fi
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."
28 changes: 18 additions & 10 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
# IMPORTANT — the Claude Code marketplace pins us by SHA:
# `claude plugin install axme-code@claude-community` reads
# anthropics/claude-plugins-community/.claude-plugin/marketplace.json,
# which pins our plugin to a COMMIT SHA of AxmeAI/axme-code-plugin. Our
# sync job updates the plugin repo, but new users keep getting the pinned
# SHA until someone opens a PR to claude-plugins-community bumping it.
# which pins our plugin to a COMMIT SHA of AxmeAI/axme-code-plugin.
# That repo is a READ-ONLY MIRROR — direct PRs are closed by a bot
# (verified 2026-06-11, PR #63). Per the official docs
# (code.claude.com/docs/en/plugins), ANTHROPIC's pipeline bumps the pin
# automatically when we push new commits to the plugin repo, and the
# public catalog syncs nightly — so allow 24-48h after a release.
# (Discovered 2026-06-11: the pin still pointed at v0.2.9 from April —
# every release since had been invisible to plugin installers.) The
# postflight check below fails loudly until the marketplace PR lands.
# either their auto-bump postdates April or it is silently rejecting
# our updates.) If the pin is still stale 48h after a release, escalate
# via https://clau.de/plugin-directory-submission. The scheduled
# channel-health workflow checks this twice a week and opens an issue.
#
# Why this exists:
# The v0.2.7 release took ~5 retries because of drift between manual steps:
Expand Down Expand Up @@ -444,11 +449,14 @@ pinned_sha="$(curl -fsSL "https://raw.githubusercontent.com/anthropics/claude-pl
if [ "$pinned_sha" = "$plugin_head" ] && [ "$pinned_sha" != "FETCH_FAILED" ]; then
ok "marketplace pin is current ($pinned_sha)"
else
err "marketplace pins $pinned_sha but $PLUGIN_REPO main is $plugin_head"
err " New plugin installs will keep getting the OLD version until this lands:"
err " 1. Fork anthropics/claude-plugins-community"
err " 2. In .claude-plugin/marketplace.json set axme-code .source.sha = $plugin_head"
err " 3. Open a PR (their CI + maintainers review it)"
warn "marketplace pins $pinned_sha but $PLUGIN_REPO main is $plugin_head"
warn " New plugin installs keep getting the OLD version until the pin updates."
warn " The pin is bumped by ANTHROPIC's pipeline (triggered by our plugin-repo"
warn " pushes); the public catalog then syncs nightly — allow 24-48h."
warn " Do NOT open a PR to anthropics/claude-plugins-community: it is a"
warn " read-only mirror and a bot auto-closes PRs (verified 2026-06-11, #63)."
warn " Still stale after 48h? Escalate: https://clau.de/plugin-directory-submission"
warn " (channel-health.yml re-checks twice a week and opens an issue on drift)"
fi

# --- Done ---
Expand Down
Loading