Skip to content
Closed
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
23 changes: 23 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "raysense-marketplace",
"version": "0.1.0",
"description": "Raysense — architectural X-ray for your codebase, exposed to AI coding agents through MCP",
"owner": {
"name": "Anton Kundenko",
"email": "anton.kundenko@gmail.com"
},
"plugins": [
{
"name": "raysense",
"description": "Phase-scoped skills (bootstrap / impact / verify / audit) over the raysense MCP server. Surfaces structural health, blast radius, dependency cycles, and persisted memory to coding agents.",
"version": "0.1.0",
"author": {
"name": "Anton Kundenko",
"email": "anton.kundenko@gmail.com"
},
"source": "./claude-plugin",
"category": "development"
}
]
}
251 changes: 192 additions & 59 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,28 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Tag-driven release pipeline. Push a `vX.Y.Z` tag and this workflow:
# 1. Verifies the tag matches Cargo.toml's version field.
# 2. Vendors rayforce at the SHA pinned in `.rayforce-version`.
# 3. Runs cargo test.
# 4. Publishes the crate to crates.io (idempotent: skips if already there).
# 5. Generates release notes from conventional commits since the previous tag.
# 6. Creates the GitHub Release with those notes.
# 7. Smoke-tests the published crate (cargo install + cargo check as dep).
#
# Manual dry-run available via the workflow_dispatch trigger; that path
# does `cargo publish --dry-run` and skips release-creation.

name: publish

on:
release:
types:
- published
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
dry_run:
description: "Run publish commands with --dry-run where possible"
description: "Run publish + smoke commands with --dry-run"
required: true
type: boolean
default: true
Expand All @@ -40,90 +52,211 @@ concurrency:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # needed for `gh release create`

steps:
- name: Checkout raysense
uses: actions/checkout@v6
with:
fetch-depth: 0 # full history needed for changelog generation

- name: Resolve target version
id: version
shell: bash
run: |
set -euo pipefail
cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
if [[ -z "$cargo_version" ]]; then
echo "::error::Could not parse version from Cargo.toml" >&2
exit 1
fi
echo "value=$cargo_version" >> "$GITHUB_OUTPUT"

if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
tag_version="${GITHUB_REF_NAME#v}"
if [[ "$tag_version" != "$cargo_version" ]]; then
echo "::error::Tag $GITHUB_REF_NAME implies version $tag_version but Cargo.toml has $cargo_version. Bump Cargo.toml or retag." >&2
exit 1
fi
echo "is_tag=true" >> "$GITHUB_OUTPUT"
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
else
echo "is_tag=false" >> "$GITHUB_OUTPUT"
fi
echo "Resolved version: $cargo_version"

- name: Vendor rayforce at pinned SHA
# The published .crate must contain the rayforce source so end users
# can `cargo install raysense` without network access. Clone it
# here, strip .git/, and let `cargo package` pick it up via the
# Cargo.toml `include` whitelist (vendor/ is gitignored otherwise).
shell: bash
run: |
set -euo pipefail
sha="$(cat .rayforce-version | tr -d '[:space:]')"
if [[ -z "$sha" ]]; then
echo "::error::.rayforce-version is empty" >&2
exit 1
fi
mkdir -p vendor
rm -rf vendor/rayforce
git -c advice.detachedHead=false clone --quiet \
https://github.com/RayforceDB/rayforce.git vendor/rayforce
git -C vendor/rayforce checkout --quiet "$sha"
rm -rf vendor/rayforce/.git
test -f vendor/rayforce/Makefile || { echo "::error::Makefile missing" >&2; exit 1; }

- name: Test
run: cargo test

- name: Package and publish crates
- name: Publish to crates.io
shell: bash
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
DRY_RUN: ${{ inputs.dry_run || 'false' }}
VERSION: ${{ steps.version.outputs.value }}
run: |
set -euo pipefail

dry_run=false
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.dry_run }}" == "true" ]]; then
dry_run=true
fi

version_of() {
sed -n 's/^version = "\(.*\)"/\1/p' "crates/$1/Cargo.toml" | head -n 1
already_published() {
cargo search raysense --limit 1 | grep -q "^raysense = \"$VERSION\""
}

wait_for_crate() {
local package="$1"
local version="$2"
for delay in 10 20 30 60 90; do
if crate_exists "$package" "$version"; then
return 0
fi
sleep "$delay"
done
echo "$package $version did not appear in the registry index in time" >&2
return 1
}
# `--allow-dirty` is required because the CI vendor step writes
# vendor/rayforce/ at clone time, but vendor/ is in .gitignore
# (the source of truth is upstream + .rayforce-version, not the
# committed tree). cargo package would otherwise refuse to ship
# what it sees as "uncommitted changes".
if [[ "$DRY_RUN" == "true" ]]; then
echo "::notice::Dry run: cargo package + cargo publish --dry-run"
cargo package --allow-dirty
cargo publish --dry-run --allow-dirty
exit 0
fi

crate_exists() {
local package="$1"
local version="$2"
cargo search "$package" --limit 1 | grep -q "^$package = \"$version\""
}
if already_published; then
echo "raysense $VERSION is already on crates.io, skipping publish"
exit 0
fi

publish_crate() {
local package="$1"
local version
version="$(version_of "$package")"

if [[ "$dry_run" == "true" ]]; then
if ! cargo package -p "$package"; then
echo "::notice::$package package check requires already-published dependency versions"
return 0
fi
if ! cargo publish -p "$package" --dry-run; then
echo "::notice::$package publish dry run requires already-published dependency versions"
return 0
fi
return 0
fi
cargo package --allow-dirty
cargo publish --allow-dirty

if crate_exists "$package" "$version"; then
echo "$package $version is already published"
return 0
# Wait for the registry index to catch up before declaring success.
for delay in 10 20 30 60 90; do
if already_published; then
echo "raysense $VERSION is now visible on crates.io"
exit 0
fi
sleep "$delay"
done
echo "::error::raysense $VERSION did not appear in the registry index in time" >&2
exit 1

- name: Generate release notes
if: steps.version.outputs.is_tag == 'true'
id: notes
shell: bash
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail

cargo package -p "$package"
cargo publish -p "$package"
wait_for_crate "$package" "$version"
# Pick the closest tag that strictly precedes HEAD (i.e. the
# previous release). On the first ever release there is none,
# so fall back to the full history.
prev_tag="$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)"
if [[ -n "$prev_tag" ]]; then
range="${prev_tag}..HEAD"
compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/${prev_tag}...${TAG}"
else
range="HEAD"
compare_url="https://github.com/${GITHUB_REPOSITORY}/commits/${TAG}"
fi

# Group commits by conventional-commit type. Each group emits
# only if it had any matching commits.
notes=release-notes.md
: > "$notes"

emit_section() {
local heading="$1" pattern="$2" entries
entries="$(git log --pretty='format:- %s (%h)' "$range" \
| grep -E "^- ${pattern}(\([^)]*\))?(!)?: " \
| sed -E "s/^- ${pattern}(\([^)]*\))?(!)?: /- /" || true)"
if [[ -n "$entries" ]]; then
{
echo "### $heading"
echo
echo "$entries"
echo
} >> "$notes"
fi
}

publish_crate raysense
{
echo "## What's changed"
echo
} >> "$notes"

emit_section "Features" "feat"
emit_section "Bug fixes" "fix"
emit_section "Performance" "perf"
emit_section "Refactoring" "refactor"
emit_section "Documentation" "docs"
emit_section "Build & CI" "build|ci"
emit_section "Chores" "chore|style|test"

# Catch-all for anything that did not match the patterns above.
other="$(git log --pretty='format:- %s (%h)' "$range" \
| grep -vE "^- (feat|fix|perf|refactor|docs|build|ci|chore|style|test)(\([^)]*\))?(!)?: " || true)"
if [[ -n "$other" ]]; then
{
echo "### Other"
echo
echo "$other"
echo
} >> "$notes"
fi

{
echo "**Full changelog:** $compare_url"
} >> "$notes"

echo "Generated notes:"
cat "$notes"
echo "notes_file=$notes" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
if: steps.version.outputs.is_tag == 'true' && (inputs.dry_run != true)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
NOTES_FILE: ${{ steps.notes.outputs.notes_file }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists, updating notes"
gh release edit "$TAG" --notes-file "$NOTES_FILE"
else
gh release create "$TAG" \
--title "raysense $TAG" \
--notes-file "$NOTES_FILE"
fi

- name: Post-release smoke
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
- name: Smoke test the published crate
if: inputs.dry_run != true
shell: bash
env:
VERSION: ${{ steps.version.outputs.value }}
run: |
set -euo pipefail
version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
smoke_dir="$(mktemp -d)"

cargo install raysense --version "$version" --root "$smoke_dir/install"
cargo install raysense --version "$VERSION" --root "$smoke_dir/install"
"$smoke_dir/install/bin/raysense" --version

cargo new "$smoke_dir/library-smoke"
cd "$smoke_dir/library-smoke"
cargo add "raysense@$version"
cargo add "raysense@$VERSION"
cargo check
23 changes: 2 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
# Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

CLAUDE.md
/target/
Cargo.lock
/local/
.codex
/deps/rayforce/
/vendor/
1 change: 1 addition & 0 deletions .rayforce-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6614c1baebf25f0fd0db02cb0de214a92e22f550
22 changes: 20 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ repository = "https://github.com/RayforceDB/raysense"
description = "Architectural X-ray for your codebase. Live, local, agent-ready."
readme = "README.md"
links = "rayforce"
# Whitelist files shipped in the published .crate. `vendor/rayforce/**` is
# included even though it's gitignored — CI populates it from upstream
# rayforce at the pinned SHA before `cargo package` runs.
include = [
"src/**/*.rs",
"build.rs",
"Cargo.toml",
"README.md",
"docs/*.svg",
".rayforce-version",
# Bundled rayforce source — narrow to just what `make lib` needs.
# Excludes website/, docs/, test/, examples/, bench/ to keep the
# published .crate well under the crates.io 10 MiB ceiling.
"vendor/rayforce/Makefile",
"vendor/rayforce/LICENSE",
"vendor/rayforce/include/**/*.h",
"vendor/rayforce/src/**/*.c",
"vendor/rayforce/src/**/*.h",
]

[[bin]]
name = "raysense"
Expand Down Expand Up @@ -56,5 +75,4 @@ tree-sitter-python = "0.25.0"
tree-sitter-rust = "0.24.2"
tree-sitter-typescript = "0.23.2"

[build-dependencies]
cc = "1"
# build.rs uses only std + git/make subprocesses; no build-deps.
Loading