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
12 changes: 12 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
- [ ] All commits signed off (`git commit -s`, DCO)
- [ ] `CHANGELOG.md` updated under `[Unreleased]` if user-visible

## Registry submission (only if adding a row to `docs/registry.md`)

- [ ] Not applicable — this PR does not add/change a conformance registry entry
- [ ] The exact, reproducible `contextgraph-inspect ... --json` invocation used
to produce the listed report is included below (no self-attested
listings — a maintainer must be able to re-run it and get the same
result)
- [ ] Every check in the linked report is `pass` or `skip`, none `fail`

<!-- If applicable, paste the invocation and, if convenient, the JSON report
itself (or attach it) here. -->

## Protocol-stability impact (if a spec/wire change)

- [ ] Not applicable — no wire or spec change
Expand Down
121 changes: 121 additions & 0 deletions .github/scripts/downstream-canary-stella.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Downstream canary (issue #29): build stella against THIS repo's HEAD.
#
# stella consumes contextgraph-types::ContextFrame (and, transitively,
# contextgraph-host / contextgraph-trace / contextgraph-conformance) as a
# pinned git dependency. That pin only moves when a human bumps it, so a
# breaking change here can sit unnoticed until someone does. This script
# closes that gap: it patches a stella checkout to build against a *local*
# CGP checkout (this repo, at whatever ref is checked out — HEAD in CI) via
# Cargo's `[patch]` table, then builds and tests every stella crate that
# actually depends on a contextgraph-* crate.
#
# This is the code-side half of the #27 boundary enforcement (see
# docs/adaptive-context-reconciliation.md and docs/adr/0007-protocol-product-
# boundary.md); the docs-side half is stella's own `normative-home` workflow
# (stella PR #500), which checks the *pointer* rather than the *build*.
#
# Usage (matches the .github/scripts/conformance-*.sh convention — env vars,
# no flags, safe to run twice):
# CGP_DIR=/path/to/context-graph-protocol \
# STELLA_DIR=/path/to/stella \
# .github/scripts/downstream-canary-stella.sh
#
# Deliberately advisory (see the calling workflow's continue-on-error): a
# real break here is exactly the kind of pre-freeze signal issue #29 wants,
# but a canary that could fail *this* repo's own required checks would just
# get muted, which defeats the point.
#
# Grep, not rg; find, not fd — this script has to run unmodified on GitHub's
# stock ubuntu-latest runner and on a contributor's machine with no extra
# tools installed, so it only uses what a bare POSIX + coreutils + cargo
# environment already guarantees.
set -euo pipefail

CGP_DIR="${CGP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
STELLA_DIR="${STELLA_DIR:-}"

if [[ -z "$STELLA_DIR" ]]; then
echo "::error::STELLA_DIR is not set — point it at a checkout of macanderson/stella"
exit 1
fi

CGP_DIR="$(cd "$CGP_DIR" && pwd)"
STELLA_DIR="$(cd "$STELLA_DIR" && pwd)"
STELLA_MANIFEST="$STELLA_DIR/Cargo.toml"
CGP_GIT_SOURCE="https://github.com/macanderson/context-graph-protocol"
SENTINEL="# --- downstream-canary-stella.sh: local CGP patch (do not commit) ---"

if [[ ! -f "$STELLA_MANIFEST" ]]; then
echo "::error::$STELLA_MANIFEST not found — is STELLA_DIR a stella checkout?"
exit 1
fi

# Discover the contextgraph-* crates this checkout actually ships, from their
# own `[package] name`, rather than hardcoding the list — so a rename or a
# split crate is picked up automatically instead of silently going unpatched.
crates=()
for manifest in "$CGP_DIR"/contextgraph-*/Cargo.toml; do
[[ -f "$manifest" ]] || continue
name=$(grep -m1 '^name = ' "$manifest" | cut -d'"' -f2)
[[ -n "$name" ]] && crates+=("$name")
done

if [[ "${#crates[@]}" -eq 0 ]]; then
echo "::error::no contextgraph-*/Cargo.toml found under $CGP_DIR"
exit 1
fi

echo "CGP crates available to patch in: ${crates[*]}"

if grep -qF "$SENTINEL" "$STELLA_MANIFEST"; then
echo "stella's Cargo.toml already carries the local-CGP patch — leaving it as-is."
else
echo "Patching $STELLA_MANIFEST to pin contextgraph-* at $CGP_DIR (local checkout)"
{
echo ""
echo "$SENTINEL"
echo "[patch.\"$CGP_GIT_SOURCE\"]"
for crate in "${crates[@]}"; do
printf '%s = { path = "%s/%s" }\n' "$crate" "$CGP_DIR" "$crate"
done
} >>"$STELLA_MANIFEST"
fi

echo "--- patched Cargo.toml tail ---"
tail -n "$(( ${#crates[@]} + 3 ))" "$STELLA_MANIFEST"
echo "-------------------------------"

# Discover which stella crates depend on a contextgraph-* crate at all, from
# their manifests, rather than hardcoding stella-graph/stella-context/
# stella-cli — so the canary keeps tracking the real dependency edge as
# stella's own crate graph changes.
dependents=()
while IFS= read -r manifest; do
dependents+=("$(basename "$(dirname "$manifest")")")
done < <(cd "$STELLA_DIR" && find . -mindepth 2 -maxdepth 2 -name Cargo.toml \
-exec grep -lE '^contextgraph-[a-z-]+ = ' {} \; | sort -u)

if [[ "${#dependents[@]}" -eq 0 ]]; then
echo "::error::no stella crate depends on contextgraph-* — is STELLA_DIR stale, or did the dependency move?"
exit 1
fi

echo "stella crates depending on contextgraph-*: ${dependents[*]}"

package_args=()
for pkg in "${dependents[@]}"; do
package_args+=(-p "$pkg")
done

cd "$STELLA_DIR"
echo "--- cargo build (${dependents[*]}) against local CGP checkout ---"
cargo build "${package_args[@]}"

if [[ "${DOWNSTREAM_CANARY_BUILD_ONLY:-0}" == "1" ]]; then
echo "DOWNSTREAM_CANARY_BUILD_ONLY=1 — skipping cargo test."
exit 0
fi

echo "--- cargo test (${dependents[*]}) against local CGP checkout ---"
cargo test "${package_args[@]}"
57 changes: 57 additions & 0 deletions .github/scripts/wait-for-crate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Poll the crates.io sparse index until a just-published crate version is
# visible, so the next `cargo publish` in the dependency chain (which
# resolves its path dependency's version requirement against the registry,
# not the local path — see PUBLISHING.md) doesn't race the CDN. Usually
# resolves in seconds; PUBLISHING.md notes it can occasionally take a minute
# or two.
set -euo pipefail

if [[ $# -lt 2 ]]; then
echo "usage: $0 <crate-name> <version> [max-attempts] [sleep-seconds]" >&2
exit 2
fi

crate="$1"
version="$2"
max_attempts="${3:-30}"
sleep_seconds="${4:-10}"

# Sparse index path convention: https://doc.rust-lang.org/cargo/reference/registry-index.html#index-files
lower=$(printf '%s' "$crate" | tr '[:upper:]' '[:lower:]')
len=${#lower}
if [[ $len -eq 1 ]]; then
path="1/$lower"
elif [[ $len -eq 2 ]]; then
path="2/$lower"
elif [[ $len -eq 3 ]]; then
path="3/${lower:0:1}/$lower"
else
path="${lower:0:2}/${lower:2:2}/$lower"
fi

url="https://index.crates.io/$path"

for attempt in $(seq 1 "$max_attempts"); do
if curl -fsSL "$url" 2>/dev/null | python3 -c "
import json, sys

target = '$version'
for line in sys.stdin:
line = line.strip()
if not line:
continue
entry = json.loads(line)
if entry.get('vers') == target:
sys.exit(0)
sys.exit(1)
"; then
echo "$crate $version is live on the sparse index."
exit 0
fi
echo "Attempt $attempt/$max_attempts: $crate $version not yet visible on the sparse index, waiting ${sleep_seconds}s..."
sleep "$sleep_seconds"
done

echo "::error::$crate $version did not appear on the sparse index after $((max_attempts * sleep_seconds))s"
exit 1
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@ jobs:
- run: pip install jsonschema
- run: python3 schema/validate-examples.py

publish-dry-run:
name: contextgraph-types packages cleanly (crates.io dry run)
runs-on: ubuntu-latest
# Cheap, credential-free proof that the first crate in the publish chain
# (see PUBLISHING.md) still packages, resolves, and compiles in isolation.
# `--dry-run` never authenticates and never uploads — it aborts right
# before that step. Verified: `cargo publish --dry-run -p contextgraph-types`
# needs no `cargo login` and no CARGO_REGISTRY_TOKEN. Scoped to
# contextgraph-types only because it's the one crate in the chain with no
# unpublished workspace-internal dependency to resolve — contextgraph-host
# and contextgraph-conformance can't dry-run until contextgraph-types is
# actually live on crates.io (see PUBLISHING.md's note on why local
# pre-publish verification is asymmetric).
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo publish --dry-run -p contextgraph-types

site:
name: docs site builds
runs-on: ubuntu-latest
Expand Down
126 changes: 126 additions & 0 deletions .github/workflows/downstream-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Downstream Canary

# The code-side half of the #27 boundary enforcement (see
# docs/adaptive-context-reconciliation.md's "Enforcement" section and ADR
# 0007's Consequences). Downstream docs now hold only a pinned pointer to
# this repo for frame/wire semantics — the risk that remains is a code/type
# break in contextgraph-* that the pinned `rev` in a downstream Cargo.toml
# doesn't surface until a human bumps it. This workflow builds the known
# downstream consumer (stella) against THIS repo's HEAD so that break is
# visible before the freeze, not after.
#
# Deliberately advisory, not a required check: this repo's own gate must stay
# green on this repo's own guarantees, not on a downstream project's
# unrelated churn. `continue-on-error` + an explicit ::warning:: keeps the
# signal visible without letting a foreign repo block a merge here.

on:
schedule:
# Daily, off the hour, so it doesn't line up with everyone else's cron.
- cron: "17 6 * * *"
workflow_dispatch: {}
pull_request:
# Only when a PR could plausibly move the thing this canary watches —
# the wire-level crates themselves, or the canary's own definition.
paths:
- "contextgraph-types/**"
- "contextgraph-host/**"
- "contextgraph-trace/**"
- "contextgraph-conformance/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/downstream-canary.yml"
- ".github/scripts/downstream-canary-stella.sh"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always

jobs:
stella-canary:
name: stella builds against CGP HEAD (advisory)
runs-on: ubuntu-latest
steps:
- name: Checkout context-graph-protocol (this repo, HEAD)
uses: actions/checkout@v5
with:
path: cgp

- name: Checkout stella (public)
uses: actions/checkout@v5
with:
repository: macanderson/stella
path: stella

- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
cgp
stella

- name: Build + test stella's contextgraph-* consumers against local HEAD
id: build
continue-on-error: true
env:
CGP_DIR: ${{ github.workspace }}/cgp
STELLA_DIR: ${{ github.workspace }}/stella
run: ./cgp/.github/scripts/downstream-canary-stella.sh

- name: Flag the break (advisory — does not fail the job)
if: steps.build.outcome == 'failure'
run: |
echo "::warning title=downstream canary::stella no longer builds against context-graph-protocol HEAD (${{ github.sha }}) — a breaking change to contextgraph-types::ContextFrame or another wire type likely needs a coordinated stella update before the next freeze/tag."
{
echo "### :warning: Downstream canary: \`stella\` failed"
echo
echo "stella (macanderson/stella) no longer builds/tests against this repo's HEAD (\`${{ github.sha }}\`). See the \`build\` step log above for the compiler error."
} >> "$GITHUB_STEP_SUMMARY"

oxagen-canary:
name: oxagen conformance fixtures pinned to CGP HEAD (advisory, deferred)
runs-on: ubuntu-latest
steps:
# oxagen-platform is private, so reading it at all needs a token with
# cross-org repo access — that token does not exist yet
# (OXAGEN_PLATFORM_TOKEN). Wiring it is the deferred human step this
# job is waiting on; until then it degrades to a no-op notice instead
# of a red (or silently absent) job.
- name: Check for cross-org access
id: gate
env:
HAS_TOKEN: ${{ secrets.OXAGEN_PLATFORM_TOKEN != '' }}
run: |
echo "has_token=$HAS_TOKEN" >> "$GITHUB_OUTPUT"
if [[ "$HAS_TOKEN" != "true" ]]; then
echo "::notice title=downstream canary::oxagen-canary is a no-op — OXAGEN_PLATFORM_TOKEN is not set, so this repo cannot check out the private macanderson/oxagen-platform to validate its pinned CGP conformance fixtures. Wiring that token (a fine-grained PAT with read access to that repo) is the deferred human step; see docs/adaptive-context-reconciliation.md."
fi

- name: Checkout oxagen-platform (private, cross-org)
if: steps.gate.outputs.has_token == 'true'
uses: actions/checkout@v5
with:
repository: macanderson/oxagen-platform
token: ${{ secrets.OXAGEN_PLATFORM_TOKEN }}
path: oxagen
sparse-checkout: |
docs/specs/adaptive-context

- name: oxagen's CGP fixtures, pinned against this HEAD (deferred to #28)
if: steps.gate.outputs.has_token == 'true'
continue-on-error: true
run: |
# oxagen-platform's own spec (docs/specs/adaptive-context/spec.md
# §3, "Out (deferred, with owners)") explicitly defers running the
# Rust contextgraph-conformance suite against its HTTP endpoint
# until this repo ships the lifecycle capability (issue #28). Until
# #28 lands there is no wire surface on the oxagen side for this
# job to build or test against — so, with access wired, this step
# only asserts the pinned fixtures directory that #28 will exercise
# is still where the spec says it is, as a placeholder that turns
# into a real conformance run once #28 ships.
test -d oxagen/docs/specs/adaptive-context
echo "::notice title=downstream canary::oxagen cross-org access is wired, but the real fixture-vs-HEAD conformance run stays deferred until #28 (lifecycle capability) ships — see docs/specs/adaptive-context/spec.md §3 in oxagen-platform."
Loading
Loading