Skip to content

fix(team): make worker startup acknowledgement durable #1582

fix(team): make worker startup acknowledgement durable

fix(team): make worker startup acknowledgement durable #1582

Workflow file for this run

name: Upgrade Test
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
workflow_dispatch:
concurrency:
group: upgrade-test-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
upgrade-test:
name: omc update + session-start hook
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── 1. Environment ──────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install npm dependencies
run: npm ci
# ── 2. Install Claude Code ────────────────────────────────────────────────
# Keep Claude Code installed in CI so the upgrade path still runs in a
# realistic environment, but avoid relying on Claude runtime behavior for
# hook verification. Step 6 validates the installed SessionStart hook
# directly from repo-owned artifacts and therefore runs on push + PR.
# ENOTEMPTY hardening: the toolcache global lib/node_modules dir is reused
# across runs on GitHub-hosted runners (and inside the GitHub-hosted tool
# cache). A previously interrupted `npm install -g` can leave the resolved
# scoped package dir and/or npm's staging temp dirs behind, so the next
# install fails with
# npm error ENOTEMPTY: directory not empty, rename
# '.../@anthropic-ai/claude-code' -> '.../@anthropic-ai/.claude-code-XXXX'
# (observed in run 27492794574). Install Claude Code into an isolated npm
# global prefix under RUNNER_TEMP so the install never depends on or
# mutates the shared toolcache global dir, and retry with a fresh prefix to
# absorb any residual rename race. A persistent failure still exits
# non-zero (we never swallow the final failure).
- name: Install Claude Code
run: |
set -euo pipefail
CLAUDE_CODE_PREFIX="$RUNNER_TEMP/claude-code-global"
reset_prefix() {
rm -rf "${CLAUDE_CODE_PREFIX:?}" 2>/dev/null || true
mkdir -p "$CLAUDE_CODE_PREFIX"
}
reset_prefix
ATTEMPTS=3
INSTALLED=false
for attempt in $(seq 1 "$ATTEMPTS"); do
echo "Installing @anthropic-ai/claude-code (attempt $attempt/$ATTEMPTS)"
if npm install -g --prefix "$CLAUDE_CODE_PREFIX" \
@anthropic-ai/claude-code --no-audit --no-fund; then
INSTALLED=true
break
fi
echo "Install attempt $attempt failed; recreating isolated prefix and retrying."
reset_prefix
sleep 2
done
if [ "$INSTALLED" != "true" ]; then
echo "FAIL: npm install -g @anthropic-ai/claude-code failed after $ATTEMPTS attempts"
exit 1
fi
# Expose the isolated prefix bin dir to this and subsequent steps so
# `claude` resolves on PATH (e.g. the "Verify Claude Code is on PATH"
# step's `which claude`).
echo "$CLAUDE_CODE_PREFIX/bin" >> "$GITHUB_PATH"
export PATH="$CLAUDE_CODE_PREFIX/bin:$PATH"
claude --version
- name: Verify Claude Code is on PATH
run: which claude
# ── 3. Install omc@4.9.3 via npm ────────────────────────────────────────
# Establishes the "old version" baseline before `omc update` is run.
#
# ENOTEMPTY hardening: the global node_modules dir is reused across runs on
# GitHub-hosted runners (and within the GitHub-hosted tool cache). A
# previously interrupted `npm install -g` can leave behind the resolved
# package dir and/or npm's staging temp dirs (`.oh-my-claude-sisyphus-*`),
# which makes the next install fail with
# npm ERR! ENOTEMPTY: directory not empty, rename '.../oh-my-claude-sisyphus'
# -> '.../.oh-my-claude-sisyphus-XXXXXXXX'
# and, on rerun, appear to hang. Remove any stale global package + npm
# staging dirs before installing, then retry the install a few times so a
# transient rename race does not fail the run. Persistent install failures
# still surface as a non-zero exit (we never swallow the final failure).
- name: Install omc v4.9.3 via npm
run: |
set -euo pipefail
PKG=oh-my-claude-sisyphus
GLOBAL_ROOT="$(npm root -g)"
clean_stale_global_pkg() {
# Remove the resolved global package dir plus any npm staging temp
# dirs left over from an interrupted global install. `:?` guards
# against an empty GLOBAL_ROOT so we never rm -rf "/".
rm -rf "${GLOBAL_ROOT:?}/$PKG" 2>/dev/null || true
find "$GLOBAL_ROOT" -maxdepth 1 -name ".$PKG-*" -exec rm -rf {} + 2>/dev/null || true
}
clean_stale_global_pkg
ATTEMPTS=3
for attempt in $(seq 1 "$ATTEMPTS"); do
echo "Installing $PKG@4.9.3 (attempt $attempt/$ATTEMPTS)"
if npm install -g "$PKG@4.9.3" --no-audit --no-fund; then
echo "Installed $PKG@4.9.3"
exit 0
fi
echo "Install attempt $attempt failed; cleaning stale global state and retrying."
clean_stale_global_pkg
sleep 2
done
echo "FAIL: npm install -g $PKG@4.9.3 failed after $ATTEMPTS attempts"
exit 1
- name: Verify omc version is 4.9.3
run: |
omc --version > "$RUNNER_TEMP/omc-pre-update.version" 2>&1
VERSION_EXIT=$?
INSTALLED_VERSION=$(cat "$RUNNER_TEMP/omc-pre-update.version")
if [ "$VERSION_EXIT" -ne 0 ]; then
echo "FAIL: omc --version exited with code $VERSION_EXIT"
cat "$RUNNER_TEMP/omc-pre-update.version"
exit 1
fi
echo "Installed omc version: $INSTALLED_VERSION"
if [ "$INSTALLED_VERSION" != "4.9.3" ]; then
echo "Expected version 4.9.3, got: $INSTALLED_VERSION"
exit 1
fi
# ── 4. Run omc update and verify clean stdout only ───────────────────────
# omc update must:
# - Exit 0
# - Produce zero stderr
# Any npm warnings, git warnings, or sync messages to stderr are failures.
# Note: syncMarketplaceClone() is best-effort and skips gracefully when
# the marketplace clone (~/.claude/plugins/marketplaces/omc/) doesn't exist,
# so CI runners with a fresh home directory are handled correctly.
#
# dev can legitimately carry a package/release-notes version bump before
# that version is published to npm. In that window the old 4.9.3 updater
# still tries `npm install -g ...@latest`, which fails with ETARGET before
# any current update/reconcile code can run. Keep the real `omc update`
# path when the current package version exists on npm; otherwise install
# the just-built local package tarball and run the same post-update
# reconciliation command directly. This preserves deterministic coverage
# for OMC-owned install, reconcile, and SessionStart artifacts without
# requiring release publishing from CI.
- name: Build current package for deterministic fallback
run: npm run build
- name: Prepare local package tarball
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
CURRENT_PACKAGE_VERSION="$(node -p "require('./package.json').version")"
TARBALL_NAME="$(npm pack --pack-destination "$RUNNER_TEMP" --silent)"
{
echo "OMC_UPGRADE_TEST_EXPECTED_VERSION=$CURRENT_PACKAGE_VERSION"
echo "OMC_UPGRADE_TEST_TARBALL=$RUNNER_TEMP/$TARBALL_NAME"
} >> "$GITHUB_ENV"
if npm view "oh-my-claude-sisyphus@$CURRENT_PACKAGE_VERSION" version >/dev/null 2>&1; then
set +e
GITHUB_LATEST_VERSION="$(node -e '
const headers = { "User-Agent": "omc-upgrade-test" };
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
fetch("https://api.github.com/repos/Yeachan-Heo/oh-my-claudecode/releases/latest", { headers })
.then(async response => {
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
const release = await response.json();
process.stdout.write(String(release.tag_name || "").replace(/^v/, ""));
})
.catch(error => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
' 2>"$RUNNER_TEMP/github-latest-release.stderr")"
GITHUB_LATEST_EXIT=$?
set -e
if [ "$GITHUB_LATEST_EXIT" -ne 0 ] || [ -z "$GITHUB_LATEST_VERSION" ]; then
echo "OMC_UPGRADE_TEST_USE_LOCAL_TARBALL=true" >> "$GITHUB_ENV"
echo "GitHub latest release lookup failed or returned empty; testing local tarball + update-reconcile."
if [ -s "$RUNNER_TEMP/github-latest-release.stderr" ]; then
echo "GitHub latest release lookup stderr:"
cat "$RUNNER_TEMP/github-latest-release.stderr"
fi
elif [ "$GITHUB_LATEST_VERSION" = "$CURRENT_PACKAGE_VERSION" ]; then
echo "OMC_UPGRADE_TEST_USE_LOCAL_TARBALL=false" >> "$GITHUB_ENV"
echo "Package $CURRENT_PACKAGE_VERSION is published and GitHub latest matches; testing the live omc update path."
else
echo "OMC_UPGRADE_TEST_USE_LOCAL_TARBALL=true" >> "$GITHUB_ENV"
echo "Package $CURRENT_PACKAGE_VERSION is published, but GitHub latest is $GITHUB_LATEST_VERSION; testing local tarball + update-reconcile."
fi
else
echo "OMC_UPGRADE_TEST_USE_LOCAL_TARBALL=true" >> "$GITHUB_ENV"
echo "Package $CURRENT_PACKAGE_VERSION is not published; testing local tarball + update-reconcile."
fi
- name: Run omc update
id: omc-update
env:
# Suppress npm update progress bar to keep stdout clean
npm_config_progress: 'false'
run: |
set -o pipefail
# Capture exit code of omc update itself (or the deterministic
# unpublished-version fallback). npm install may emit deprecation
# warnings to stderr — those are not omc errors. Filter those out and
# fail on everything else.
set +e
if [ "$OMC_UPGRADE_TEST_USE_LOCAL_TARBALL" = "true" ]; then
{
echo "Using local package tarball because deterministic fallback is required for $OMC_UPGRADE_TEST_EXPECTED_VERSION."
npm install -g "$OMC_UPGRADE_TEST_TARBALL"
omc update-reconcile
} 1>"$RUNNER_TEMP/omc-update.stdout.log" 2>"$RUNNER_TEMP/omc-update.stderr.log"
UPDATE_EXIT=$?
else
omc update 1>"$RUNNER_TEMP/omc-update.stdout.log" 2>"$RUNNER_TEMP/omc-update.stderr.log"
UPDATE_EXIT=$?
LIVE_UPDATED_VERSION="$(omc --version 2>/dev/null || true)"
if [ "$LIVE_UPDATED_VERSION" = "4.9.3" ]; then
{
echo "Live omc update left version at 4.9.3; falling back to local tarball + update-reconcile."
npm install -g "$OMC_UPGRADE_TEST_TARBALL"
omc update-reconcile
} >>"$RUNNER_TEMP/omc-update.stdout.log" 2>>"$RUNNER_TEMP/omc-update.stderr.log"
UPDATE_EXIT=$?
fi
fi
set -e
# Check for non-npm stderr (omc warnings/errors/exceptions)
# Exclude npm deprecation warnings: "npm deprecated ...".
# All other stderr lines (omc console.warn, error, exception, fail patterns) are failures.
if grep -vE '^npm deprecated ' "$RUNNER_TEMP/omc-update.stderr.log" 2>/dev/null | grep -qiE '^(error|exception|fail|critical|warn|omc)'; then
echo "FAIL: omc update produced stderr:"
cat "$RUNNER_TEMP/omc-update.stderr.log"
echo "Full stdout:"
cat "$RUNNER_TEMP/omc-update.stdout.log"
exit 1
fi
if [ "$UPDATE_EXIT" -ne 0 ]; then
echo "FAIL: omc update exited with code $UPDATE_EXIT"
echo "Full stdout:"
cat "$RUNNER_TEMP/omc-update.stdout.log"
echo "Full stderr:"
cat "$RUNNER_TEMP/omc-update.stderr.log"
exit 1
fi
if [ "$OMC_UPGRADE_TEST_USE_LOCAL_TARBALL" = "true" ]; then
echo "PASS: local tarball install + update-reconcile succeeded"
else
echo "PASS: omc update succeeded"
fi
# ── 5. Verify new omc version installed ──────────────────────────────────
- name: Verify omc version after update
run: |
omc --version > "$RUNNER_TEMP/omc-post-update.version" 2>&1
VERSION_EXIT=$?
UPDATED_VERSION=$(cat "$RUNNER_TEMP/omc-post-update.version")
if [ "$VERSION_EXIT" -ne 0 ]; then
echo "FAIL: omc --version exited with code $VERSION_EXIT"
cat "$RUNNER_TEMP/omc-post-update.version"
exit 1
fi
echo "Updated omc version: $UPDATED_VERSION"
if [ -z "$UPDATED_VERSION" ]; then
echo "FAIL: omc --version returned empty"
exit 1
fi
if [ "$UPDATED_VERSION" = "4.9.3" ]; then
echo "FAIL: omc version still 4.9.3 after update"
exit 1
fi
if [ "$OMC_UPGRADE_TEST_USE_LOCAL_TARBALL" = "true" ] && [ "$UPDATED_VERSION" != "$OMC_UPGRADE_TEST_EXPECTED_VERSION" ]; then
echo "FAIL: local tarball fallback installed $UPDATED_VERSION, expected $OMC_UPGRADE_TEST_EXPECTED_VERSION"
exit 1
fi
echo "PASS: omc updated from 4.9.3 to $UPDATED_VERSION"
# ── 6. Verify SessionStart hook wiring + execution ───────────────────────
# Validate the update-owned coverage directly:
# - settings.json contains a SessionStart command for session-start.mjs
# - the installed standalone hook script exists
# - the hook script runs successfully on representative SessionStart JSON
#
# This keeps upgrade coverage inside OMC-owned artifacts and avoids relying
# on external Claude CLI print-mode behavior in headless CI.
- name: Verify SessionStart hook wiring and script
run: |
set -o pipefail
CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
SESSION_HOOK="$CLAUDE_CONFIG_DIR/hooks/session-start.mjs"
SETTINGS_PATH="$CLAUDE_CONFIG_DIR/settings.json"
if [ ! -f "$SESSION_HOOK" ]; then
echo "FAIL: missing SessionStart hook script at $SESSION_HOOK"
exit 1
fi
if [ ! -f "$SETTINGS_PATH" ]; then
echo "FAIL: missing Claude settings at $SETTINGS_PATH"
exit 1
fi
SESSION_COMMAND="$(node -e '
const fs = require("fs");
const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
const groups = settings.hooks?.SessionStart ?? [];
const command = groups
.flatMap(group => group?.hooks ?? [])
.find(hook => hook?.type === "command" && typeof hook.command === "string" && hook.command.includes("session-start.mjs"))
?.command ?? "";
process.stdout.write(command);
' "$SETTINGS_PATH")"
if [ -z "$SESSION_COMMAND" ]; then
echo "FAIL: settings.json is missing a SessionStart command for session-start.mjs"
cat "$SETTINGS_PATH"
exit 1
fi
set +e
printf '%s' \
"{\"hook_event_name\":\"SessionStart\",\"session_id\":\"ci-upgrade-test\",\"cwd\":\"$GITHUB_WORKSPACE\"}" \
| node "$SESSION_HOOK" 1>"$RUNNER_TEMP/session.stdout.log" 2>"$RUNNER_TEMP/session.stderr.log"
SESSION_EXIT=$?
set -e
if [ "$SESSION_EXIT" -ne 0 ]; then
echo "FAIL: SessionStart hook exited with code $SESSION_EXIT"
echo "stderr:"
cat "$RUNNER_TEMP/session.stderr.log"
echo "stdout:"
cat "$RUNNER_TEMP/session.stdout.log"
exit 1
fi
if [ -s "$RUNNER_TEMP/session.stderr.log" ]; then
echo "FAIL: SessionStart hook produced stderr:"
cat "$RUNNER_TEMP/session.stderr.log"
echo "stdout:"
cat "$RUNNER_TEMP/session.stdout.log"
exit 1
fi
node -e '
const fs = require("fs");
const output = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
if (output.continue !== true) {
throw new Error("SessionStart hook did not return continue=true");
}
const hookName = output.hookSpecificOutput?.hookEventName;
if (hookName !== undefined && hookName !== "SessionStart") {
throw new Error(`Unexpected hookSpecificOutput.hookEventName: ${hookName}`);
}
' "$RUNNER_TEMP/session.stdout.log"
echo "PASS: SessionStart hook is configured and executes cleanly"