graphify-mesh 0.0.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphify_mesh-0.0.22/.github/smoke/end-to-end.sh +338 -0
- graphify_mesh-0.0.22/.github/workflows/ci.yml +50 -0
- graphify_mesh-0.0.22/.github/workflows/pages.yml +99 -0
- graphify_mesh-0.0.22/.github/workflows/release.yml +247 -0
- graphify_mesh-0.0.22/.github/workflows/stage-build.yml +94 -0
- graphify_mesh-0.0.22/.github/workflows/stage-security.yml +141 -0
- graphify_mesh-0.0.22/.github/workflows/stage-smoke.yml +49 -0
- graphify_mesh-0.0.22/.github/workflows/stage-tests.yml +117 -0
- graphify_mesh-0.0.22/.gitignore +35 -0
- graphify_mesh-0.0.22/.gitleaksignore +1 -0
- graphify_mesh-0.0.22/.graphifyignore +34 -0
- graphify_mesh-0.0.22/.trufflehog-exclude.txt +20 -0
- graphify_mesh-0.0.22/CHANGELOG.md +441 -0
- graphify_mesh-0.0.22/Dockerfile.test +57 -0
- graphify_mesh-0.0.22/LICENSE +21 -0
- graphify_mesh-0.0.22/PKG-INFO +219 -0
- graphify_mesh-0.0.22/README.md +150 -0
- graphify_mesh-0.0.22/docs/_config.yml +1 -0
- graphify_mesh-0.0.22/docs/architecture.md +275 -0
- graphify_mesh-0.0.22/docs/assets/css/style.scss +442 -0
- graphify_mesh-0.0.22/docs/configuration.md +434 -0
- graphify_mesh-0.0.22/docs/index.md +55 -0
- graphify_mesh-0.0.22/docs/keeping-sync-up-to-date.md +374 -0
- graphify_mesh-0.0.22/docs/mcp-server.md +302 -0
- graphify_mesh-0.0.22/docs/setup.md +269 -0
- graphify_mesh-0.0.22/examples/hooks/graphify-mesh-cwd.py +75 -0
- graphify_mesh-0.0.22/examples/manual-relations.example.json +34 -0
- graphify_mesh-0.0.22/examples/manual-relations.schema.json +43 -0
- graphify_mesh-0.0.22/examples/registry.example.json +31 -0
- graphify_mesh-0.0.22/examples/systemd/graphify-mesh-sync.env.example +37 -0
- graphify_mesh-0.0.22/examples/systemd/graphify-mesh-sync.service +94 -0
- graphify_mesh-0.0.22/examples/systemd/graphify-mesh-sync.timer +21 -0
- graphify_mesh-0.0.22/examples/systemd/reap-graphify-serve.service +31 -0
- graphify_mesh-0.0.22/examples/systemd/reap-graphify-serve.timer +27 -0
- graphify_mesh-0.0.22/pyproject.toml +170 -0
- graphify_mesh-0.0.22/src/graphify_mesh/__init__.py +20 -0
- graphify_mesh-0.0.22/src/graphify_mesh/_version.py +24 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/__init__.py +61 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/anchors.py +223 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/citation.py +17 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/config.py +195 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/context_pack.py +212 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/embed_query.py +206 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/frames.py +93 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/http_app.py +367 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/lexical_read.py +208 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/project_map.py +92 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/ranking.py +209 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/retrieval.py +343 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/rwlock.py +61 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/scope.py +207 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/sdk_app.py +108 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/server.py +813 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/similar.py +284 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/stdio_guard.py +287 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/store.py +733 -0
- graphify_mesh-0.0.22/src/graphify_mesh/server/traverse.py +305 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/__init__.py +26 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/backend.py +80 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/cli.py +249 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/clustering.py +182 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/config.py +815 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/discovery.py +668 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/embed_similarity.py +516 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/embedding.py +1372 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/graphify_cli.py +941 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/lexical_index.py +293 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/locking.py +30 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/naming.py +563 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/naming_state.py +143 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/overlay.py +146 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/overlay_api.py +312 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/overlay_depends.py +324 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/overlay_refs.py +143 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/overlay_similar.py +357 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/perms.py +116 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/pipeline.py +1891 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/progress.py +41 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/publish.py +374 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/reaper.py +201 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/reaper_cli.py +62 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/registry.py +184 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/repo_tags.py +299 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/rss.py +68 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/source_cache.py +149 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/state.py +268 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/sync_project.py +793 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/tls.py +119 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/validate.py +277 -0
- graphify_mesh-0.0.22/src/graphify_mesh/sync/vectors.py +234 -0
- graphify_mesh-0.0.22/tests/compat/test_graphify_contract.py +227 -0
- graphify_mesh-0.0.22/tests/fixtures/fake_graphify/graphify +358 -0
- graphify_mesh-0.0.22/tests/fixtures/graphs/repo_a.json +14 -0
- graphify_mesh-0.0.22/tests/fixtures/graphs/repo_b.json +12 -0
- graphify_mesh-0.0.22/tests/fixtures/graphs/repo_forbidden_edge.json +12 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/README.md +80 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/build_corpus.py +526 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/corpus.json +975 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/expected.json +991 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r1.php-mixed.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r2.php-service.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r3.tsx-kit.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r4.php-app.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r5.php-mixed.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r6.ts-app.npy +0 -0
- graphify_mesh-0.0.22/tests/fixtures/similarity/r7.tsx-kit.npy +0 -0
- graphify_mesh-0.0.22/tests/hardening/_env_helper.py +30 -0
- graphify_mesh-0.0.22/tests/hardening/test_gates.py +279 -0
- graphify_mesh-0.0.22/tests/hardening/test_reaper.py +269 -0
- graphify_mesh-0.0.22/tests/hardening/test_reaper_cli.py +69 -0
- graphify_mesh-0.0.22/tests/hooks/test_cwd_hook.py +160 -0
- graphify_mesh-0.0.22/tests/server/conftest.py +358 -0
- graphify_mesh-0.0.22/tests/server/test_anchors.py +343 -0
- graphify_mesh-0.0.22/tests/server/test_cache_concurrency.py +57 -0
- graphify_mesh-0.0.22/tests/server/test_cli.py +57 -0
- graphify_mesh-0.0.22/tests/server/test_config_transport.py +150 -0
- graphify_mesh-0.0.22/tests/server/test_context_pack.py +86 -0
- graphify_mesh-0.0.22/tests/server/test_cross_project_exclude.py +90 -0
- graphify_mesh-0.0.22/tests/server/test_disabled_repos.py +299 -0
- graphify_mesh-0.0.22/tests/server/test_embed_query.py +156 -0
- graphify_mesh-0.0.22/tests/server/test_hardening.py +248 -0
- graphify_mesh-0.0.22/tests/server/test_http_app.py +377 -0
- graphify_mesh-0.0.22/tests/server/test_http_concurrency.py +189 -0
- graphify_mesh-0.0.22/tests/server/test_lexical_read.py +269 -0
- graphify_mesh-0.0.22/tests/server/test_list_repos.py +184 -0
- graphify_mesh-0.0.22/tests/server/test_project_map.py +85 -0
- graphify_mesh-0.0.22/tests/server/test_ranking_fusion.py +223 -0
- graphify_mesh-0.0.22/tests/server/test_retrieval_vectors.py +90 -0
- graphify_mesh-0.0.22/tests/server/test_rwlock.py +111 -0
- graphify_mesh-0.0.22/tests/server/test_scope.py +245 -0
- graphify_mesh-0.0.22/tests/server/test_scope_before_ranking.py +63 -0
- graphify_mesh-0.0.22/tests/server/test_sdk_app.py +84 -0
- graphify_mesh-0.0.22/tests/server/test_sdk_surface.py +84 -0
- graphify_mesh-0.0.22/tests/server/test_server.py +300 -0
- graphify_mesh-0.0.22/tests/server/test_similar.py +104 -0
- graphify_mesh-0.0.22/tests/server/test_stdio_e2e.py +455 -0
- graphify_mesh-0.0.22/tests/server/test_stdio_transport.py +428 -0
- graphify_mesh-0.0.22/tests/server/test_store.py +717 -0
- graphify_mesh-0.0.22/tests/server/test_store_concurrency.py +167 -0
- graphify_mesh-0.0.22/tests/server/test_transport_parity.py +93 -0
- graphify_mesh-0.0.22/tests/server/test_traverse.py +376 -0
- graphify_mesh-0.0.22/tests/server/test_traverse_bounds.py +105 -0
- graphify_mesh-0.0.22/tests/sync/conftest.py +192 -0
- graphify_mesh-0.0.22/tests/sync/test_ast_only.py +91 -0
- graphify_mesh-0.0.22/tests/sync/test_atomic_restore_and_caps.py +147 -0
- graphify_mesh-0.0.22/tests/sync/test_backend.py +72 -0
- graphify_mesh-0.0.22/tests/sync/test_bootstrap_and_dirty.py +112 -0
- graphify_mesh-0.0.22/tests/sync/test_child_home_isolation.py +534 -0
- graphify_mesh-0.0.22/tests/sync/test_cli_timeout.py +240 -0
- graphify_mesh-0.0.22/tests/sync/test_clustering.py +169 -0
- graphify_mesh-0.0.22/tests/sync/test_config_urls.py +114 -0
- graphify_mesh-0.0.22/tests/sync/test_discovery.py +140 -0
- graphify_mesh-0.0.22/tests/sync/test_discovery_depth.py +602 -0
- graphify_mesh-0.0.22/tests/sync/test_discovery_scan_integrity.py +285 -0
- graphify_mesh-0.0.22/tests/sync/test_embed_similarity.py +1280 -0
- graphify_mesh-0.0.22/tests/sync/test_embedding.py +1508 -0
- graphify_mesh-0.0.22/tests/sync/test_extract_concurrency.py +116 -0
- graphify_mesh-0.0.22/tests/sync/test_extract_concurrency_settings.py +131 -0
- graphify_mesh-0.0.22/tests/sync/test_extract_loop_fixes.py +191 -0
- graphify_mesh-0.0.22/tests/sync/test_failure_reason.py +36 -0
- graphify_mesh-0.0.22/tests/sync/test_infra_outage.py +1079 -0
- graphify_mesh-0.0.22/tests/sync/test_labeling_settings.py +61 -0
- graphify_mesh-0.0.22/tests/sync/test_lexical_index.py +153 -0
- graphify_mesh-0.0.22/tests/sync/test_locking.py +111 -0
- graphify_mesh-0.0.22/tests/sync/test_naming.py +990 -0
- graphify_mesh-0.0.22/tests/sync/test_naming_state.py +61 -0
- graphify_mesh-0.0.22/tests/sync/test_overlay.py +632 -0
- graphify_mesh-0.0.22/tests/sync/test_perms.py +228 -0
- graphify_mesh-0.0.22/tests/sync/test_php_builder_and_shrink_check.py +1321 -0
- graphify_mesh-0.0.22/tests/sync/test_pipeline_publish_order.py +564 -0
- graphify_mesh-0.0.22/tests/sync/test_publish_prune.py +409 -0
- graphify_mesh-0.0.22/tests/sync/test_registry.py +58 -0
- graphify_mesh-0.0.22/tests/sync/test_repo_tags.py +149 -0
- graphify_mesh-0.0.22/tests/sync/test_repo_tags_version_parity.py +213 -0
- graphify_mesh-0.0.22/tests/sync/test_rss.py +72 -0
- graphify_mesh-0.0.22/tests/sync/test_scan_settings.py +249 -0
- graphify_mesh-0.0.22/tests/sync/test_shrink_grant.py +280 -0
- graphify_mesh-0.0.22/tests/sync/test_shrink_grant_binding.py +192 -0
- graphify_mesh-0.0.22/tests/sync/test_shrink_tolerance.py +114 -0
- graphify_mesh-0.0.22/tests/sync/test_staging_sweep.py +107 -0
- graphify_mesh-0.0.22/tests/sync/test_state_recovery.py +42 -0
- graphify_mesh-0.0.22/tests/sync/test_sync_engine.py +420 -0
- graphify_mesh-0.0.22/tests/sync/test_tls.py +81 -0
- graphify_mesh-0.0.22/tests/sync/test_validate.py +144 -0
- graphify_mesh-0.0.22/tests/sync/test_vectors.py +166 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# End-to-end smoke for graphify-mesh: build a two-repo fixture mesh, run the
|
|
4
|
+
# real sync pipeline against the real `graphify` binary, then boot the HTTP MCP
|
|
5
|
+
# server against what it published and talk MCP to it.
|
|
6
|
+
#
|
|
7
|
+
# Nothing here is faked. The unit suite points GRAPHIFY_BIN at
|
|
8
|
+
# tests/fixtures/fake_graphify/graphify, so until this script existed no CI job
|
|
9
|
+
# had ever executed `graphify merge-graphs` for real, nor opened the HTTP
|
|
10
|
+
# transport's socket. Those are exactly the two seams where a working unit suite
|
|
11
|
+
# still ships a broken release.
|
|
12
|
+
#
|
|
13
|
+
# Usage: bash .github/smoke/end-to-end.sh <workdir>
|
|
14
|
+
# The workdir is an argument rather than a mktemp so a developer can run this by
|
|
15
|
+
# hand exactly the way CI runs it and then poke at the tree it left behind.
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
WORK="${1:?usage: end-to-end.sh <workdir>}"
|
|
20
|
+
mkdir -p "$WORK"
|
|
21
|
+
WORK="$(cd "$WORK" && pwd)"
|
|
22
|
+
|
|
23
|
+
# Mirror the production topology the unit fixtures use (tests/sync/conftest.py):
|
|
24
|
+
# the mesh root lives *inside* the scan root, as a sibling of the scanned
|
|
25
|
+
# projects. Discovery's approved-root containment check is sensitive to this
|
|
26
|
+
# layout, so a smoke that flattened it would exercise a shape nobody deploys.
|
|
27
|
+
SCAN_ROOT="$WORK/www"
|
|
28
|
+
MESH_ROOT="$SCAN_ROOT/graph-mesh"
|
|
29
|
+
REGISTRY="$MESH_ROOT/bin/registry.json"
|
|
30
|
+
|
|
31
|
+
fail() { echo "::error::$*" >&2; exit 1; }
|
|
32
|
+
step() { echo; echo "=== $* ==="; }
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Fixture
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Each fake project needs real parseable source, not just a canned graph.json:
|
|
38
|
+
# on a repo it has no prior state for, the pipeline picks the `update` action
|
|
39
|
+
# and shells out to `graphify update <root>`, which re-derives the graph from
|
|
40
|
+
# the AST. Source that produced no nodes would trip the shrink guard and the
|
|
41
|
+
# run would refuse the repo instead of merging it.
|
|
42
|
+
make_repo() {
|
|
43
|
+
local name="$1" prefix="$2"
|
|
44
|
+
local root="$SCAN_ROOT/$name"
|
|
45
|
+
local collection="$MESH_ROOT/graphify/smoke/$name"
|
|
46
|
+
|
|
47
|
+
mkdir -p "$root/src" "$collection"
|
|
48
|
+
cat >"$root/src/${prefix}_core.py" <<EOF
|
|
49
|
+
"""Fixture module for the ${name} smoke repo."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ${prefix^}Service:
|
|
53
|
+
def handle(self, payload):
|
|
54
|
+
return ${prefix}_normalize(payload)
|
|
55
|
+
|
|
56
|
+
def describe(self):
|
|
57
|
+
return "${prefix}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ${prefix}_normalize(payload):
|
|
61
|
+
return {k: v for k, v in sorted(payload.items())}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ${prefix}_entrypoint():
|
|
65
|
+
return ${prefix^}Service().handle({})
|
|
66
|
+
EOF
|
|
67
|
+
cat >"$root/src/${prefix}_support.py" <<EOF
|
|
68
|
+
"""Second fixture module so the repo has more than one source file."""
|
|
69
|
+
|
|
70
|
+
from ${prefix}_core import ${prefix^}Service
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ${prefix^}Cache:
|
|
74
|
+
def __init__(self):
|
|
75
|
+
self._store = {}
|
|
76
|
+
|
|
77
|
+
def get(self, key):
|
|
78
|
+
return self._store.get(key)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ${prefix}_build_service():
|
|
82
|
+
return ${prefix^}Service()
|
|
83
|
+
EOF
|
|
84
|
+
|
|
85
|
+
# The graphify-out symlink is what discovery walks the scan root looking for,
|
|
86
|
+
# and it points at the registry-declared collection path — the project dir
|
|
87
|
+
# never owns its own output directory.
|
|
88
|
+
ln -sfn "$collection" "$root/graphify-out"
|
|
89
|
+
|
|
90
|
+
# Seed a graph so the repo is an `update` (AST-only) and not a `bootstrap`.
|
|
91
|
+
# Bootstrap runs `graphify extract --backend ollama`, and neither the CI
|
|
92
|
+
# runner nor the test container has an Ollama to reach.
|
|
93
|
+
cp "$SEED_GRAPH" "$collection/graph.json"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
step "Building the fixture mesh under $WORK"
|
|
97
|
+
SEED_GRAPH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/tests/fixtures/graphs/repo_a.json"
|
|
98
|
+
[ -f "$SEED_GRAPH" ] || fail "seed graph fixture not found at $SEED_GRAPH"
|
|
99
|
+
|
|
100
|
+
mkdir -p "$MESH_ROOT/bin"
|
|
101
|
+
make_repo repo-a alpha
|
|
102
|
+
make_repo repo-b beta
|
|
103
|
+
|
|
104
|
+
cat >"$REGISTRY" <<EOF
|
|
105
|
+
{
|
|
106
|
+
"repos": [
|
|
107
|
+
{
|
|
108
|
+
"repo_id": "smoke.repo-a",
|
|
109
|
+
"root": "$SCAN_ROOT/repo-a",
|
|
110
|
+
"collection_path": "$MESH_ROOT/graphify/smoke/repo-a",
|
|
111
|
+
"enabled": true
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"repo_id": "smoke.repo-b",
|
|
115
|
+
"root": "$SCAN_ROOT/repo-b",
|
|
116
|
+
"collection_path": "$MESH_ROOT/graphify/smoke/repo-b",
|
|
117
|
+
"enabled": true
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"disabled": [],
|
|
121
|
+
"external_roots": []
|
|
122
|
+
}
|
|
123
|
+
EOF
|
|
124
|
+
|
|
125
|
+
# The registry is read by a process that also runs child binaries, so the sync
|
|
126
|
+
# entrypoint audits its permissions and complains about a group/world-writable
|
|
127
|
+
# config. A default umask on a CI runner is fine, but be explicit.
|
|
128
|
+
chmod 600 "$REGISTRY"
|
|
129
|
+
|
|
130
|
+
GLOBAL_DIR="$MESH_ROOT/graphify/global"
|
|
131
|
+
CURRENT="$GLOBAL_DIR/current"
|
|
132
|
+
|
|
133
|
+
# --skip-labeling / --skip-embedding: both stages talk to Ollama, which exists
|
|
134
|
+
# on neither the GitHub runner nor the test container. Everything this smoke
|
|
135
|
+
# asserts on — discovery, per-repo sync, merge, repo-tag remap, overlay,
|
|
136
|
+
# lexical index, validate, publish — runs regardless.
|
|
137
|
+
SYNC_ARGS=(
|
|
138
|
+
--once
|
|
139
|
+
--mesh-root "$MESH_ROOT"
|
|
140
|
+
--scan-root "$SCAN_ROOT"
|
|
141
|
+
--scan-depth 2
|
|
142
|
+
--registry "$REGISTRY"
|
|
143
|
+
--skip-labeling
|
|
144
|
+
--skip-embedding
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Phase 1 — a dry run must write nothing into the mesh root
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Dry-run redirects the publish step (sync/pipeline.py) and the naming
|
|
151
|
+
# workspace (sync/config.py) into an ephemeral staging root. A dry run that
|
|
152
|
+
# leaves a generation behind is a real regression: it means somebody scheduled
|
|
153
|
+
# a "safe" preview that mutated the tree every agent reads from.
|
|
154
|
+
step "Phase 1: dry run"
|
|
155
|
+
graphify-mesh-sync "${SYNC_ARGS[@]}" --dry-run
|
|
156
|
+
|
|
157
|
+
if [ -e "$CURRENT" ]; then
|
|
158
|
+
fail "dry run published a generation: $CURRENT exists"
|
|
159
|
+
fi
|
|
160
|
+
if [ -d "$GLOBAL_DIR/generations" ] && [ -n "$(ls -A "$GLOBAL_DIR/generations")" ]; then
|
|
161
|
+
fail "dry run wrote into $GLOBAL_DIR/generations"
|
|
162
|
+
fi
|
|
163
|
+
echo "OK: no generation published by the dry run"
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Phase 2 — a real run publishes a complete, two-repo generation
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
step "Phase 2: real run"
|
|
169
|
+
graphify-mesh-sync "${SYNC_ARGS[@]}"
|
|
170
|
+
|
|
171
|
+
[ -L "$CURRENT" ] || fail "no current symlink at $CURRENT"
|
|
172
|
+
GEN="$(readlink -f "$CURRENT")"
|
|
173
|
+
[ -d "$GEN" ] || fail "current symlink does not resolve to a directory: $CURRENT"
|
|
174
|
+
echo "OK: current -> $GEN"
|
|
175
|
+
|
|
176
|
+
# Assert on the published artifacts rather than on the pipeline's own exit
|
|
177
|
+
# code: publish flips the symlink last, so a half-written generation is
|
|
178
|
+
# precisely the failure mode a green exit code would hide.
|
|
179
|
+
python3 - "$GEN" <<'PY'
|
|
180
|
+
import json, sys
|
|
181
|
+
from pathlib import Path
|
|
182
|
+
|
|
183
|
+
gen = Path(sys.argv[1])
|
|
184
|
+
docs = {}
|
|
185
|
+
for name in ("global-graph.json", "cross-project-overlay.json", "lexical-index.json"):
|
|
186
|
+
path = gen / name
|
|
187
|
+
if not path.is_file():
|
|
188
|
+
sys.exit(f"::error::missing published file: {path}")
|
|
189
|
+
try:
|
|
190
|
+
docs[name] = json.loads(path.read_text(encoding="utf-8"))
|
|
191
|
+
except json.JSONDecodeError as err:
|
|
192
|
+
sys.exit(f"::error::{path} is not valid JSON: {err}")
|
|
193
|
+
print(f"OK: {name} exists and parses")
|
|
194
|
+
|
|
195
|
+
# A merge that silently drops one input still produces a valid graph, so the
|
|
196
|
+
# only assertion worth making is per-repo attribution: every fixture repo must
|
|
197
|
+
# own at least one node in the merged structural graph, tagged with the
|
|
198
|
+
# repo_id the registry declared (not graphify's own auto tag, which the remap
|
|
199
|
+
# stage rewrites).
|
|
200
|
+
nodes = docs["global-graph.json"].get("nodes") or []
|
|
201
|
+
seen = {}
|
|
202
|
+
for node in nodes:
|
|
203
|
+
repo = node.get("repo_id") or node.get("repo")
|
|
204
|
+
if repo:
|
|
205
|
+
seen[repo] = seen.get(repo, 0) + 1
|
|
206
|
+
print(f"node count={len(nodes)} repo attribution={seen}")
|
|
207
|
+
missing = [r for r in ("smoke.repo-a", "smoke.repo-b") if seen.get(r, 0) == 0]
|
|
208
|
+
if missing:
|
|
209
|
+
sys.exit(f"::error::merged graph has no nodes for: {', '.join(missing)}")
|
|
210
|
+
print("OK: both fixture repos are represented in global-graph.json")
|
|
211
|
+
PY
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# Phase 3 — the HTTP transport serves MCP, and refuses without the token
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
step "Phase 3: HTTP MCP server"
|
|
217
|
+
|
|
218
|
+
# Never a literal in the file, never echoed: this token is the only thing
|
|
219
|
+
# between the daemon and every other process on the machine.
|
|
220
|
+
GRAPHIFY_MESH_HTTP_TOKEN="$(openssl rand -hex 32)"
|
|
221
|
+
export GRAPHIFY_MESH_HTTP_TOKEN
|
|
222
|
+
HTTP_HOST=127.0.0.1
|
|
223
|
+
HTTP_PORT=19744
|
|
224
|
+
HTTP_PATH=/mcp
|
|
225
|
+
URL="http://${HTTP_HOST}:${HTTP_PORT}${HTTP_PATH}"
|
|
226
|
+
|
|
227
|
+
SERVER_PID=""
|
|
228
|
+
cleanup() {
|
|
229
|
+
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
230
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
231
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
232
|
+
fi
|
|
233
|
+
}
|
|
234
|
+
# A failing assertion below must not leave a listening daemon behind for the
|
|
235
|
+
# rest of the job (or, locally, for the rest of the day).
|
|
236
|
+
trap cleanup EXIT
|
|
237
|
+
|
|
238
|
+
# The server has no --mesh-root/--registry flags; both come from the
|
|
239
|
+
# environment (src/graphify_mesh/server/config.py), and mesh_root otherwise
|
|
240
|
+
# defaults to the current working directory.
|
|
241
|
+
GRAPHIFY_MESH_ROOT="$MESH_ROOT" GRAPHIFY_MESH_REGISTRY="$REGISTRY" \
|
|
242
|
+
graphify-mesh-server \
|
|
243
|
+
--transport http --host "$HTTP_HOST" --port "$HTTP_PORT" --path "$HTTP_PATH" \
|
|
244
|
+
>"$WORK/server.log" 2>&1 &
|
|
245
|
+
SERVER_PID=$!
|
|
246
|
+
|
|
247
|
+
# Wait for the port instead of sleeping a fixed interval, and surface the
|
|
248
|
+
# server's own log if it died during startup (a config error exits immediately).
|
|
249
|
+
for _ in $(seq 1 60); do
|
|
250
|
+
if python3 -c "
|
|
251
|
+
import socket,sys
|
|
252
|
+
s=socket.socket()
|
|
253
|
+
s.settimeout(0.5)
|
|
254
|
+
sys.exit(0 if s.connect_ex(('$HTTP_HOST', $HTTP_PORT))==0 else 1)
|
|
255
|
+
"; then
|
|
256
|
+
break
|
|
257
|
+
fi
|
|
258
|
+
kill -0 "$SERVER_PID" 2>/dev/null || { cat "$WORK/server.log" >&2; fail "server exited during startup"; }
|
|
259
|
+
sleep 0.5
|
|
260
|
+
done
|
|
261
|
+
|
|
262
|
+
# The MCP conversation and the negative check share one script because both
|
|
263
|
+
# need the same headers and the same streamable-HTTP framing. Python's urllib
|
|
264
|
+
# rather than curl: it is already a hard dependency of everything else here,
|
|
265
|
+
# and it can assert on the parsed body instead of on grep.
|
|
266
|
+
python3 - "$URL" <<'PY'
|
|
267
|
+
import json, os, sys, urllib.error, urllib.request
|
|
268
|
+
|
|
269
|
+
url = sys.argv[1]
|
|
270
|
+
token = os.environ["GRAPHIFY_MESH_HTTP_TOKEN"]
|
|
271
|
+
EXPECTED = {"search", "cross_project", "find_similar", "project_map", "neighbors", "context_pack", "list_repos"}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def post(payload, bearer):
|
|
275
|
+
body = json.dumps(payload).encode()
|
|
276
|
+
headers = {
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
# The streamable-HTTP transport requires the client to declare it can
|
|
279
|
+
# take either framing, even though this server is configured
|
|
280
|
+
# json_response=True and never opens an SSE stream.
|
|
281
|
+
"Accept": "application/json, text/event-stream",
|
|
282
|
+
}
|
|
283
|
+
if bearer is not None:
|
|
284
|
+
headers["Authorization"] = f"Bearer {bearer}"
|
|
285
|
+
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
|
|
286
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
287
|
+
return resp.status, resp.read().decode()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def rpc(payload, bearer=token):
|
|
291
|
+
status, text = post(payload, bearer)
|
|
292
|
+
if status != 200:
|
|
293
|
+
sys.exit(f"::error::{payload.get('method')} returned HTTP {status}: {text[:400]}")
|
|
294
|
+
# json_response=True still permits an SSE-shaped body; unwrap it if present
|
|
295
|
+
# so this assertion does not depend on which framing the SDK picked.
|
|
296
|
+
if text.startswith("event:") or text.startswith("data:"):
|
|
297
|
+
text = "\n".join(
|
|
298
|
+
line[len("data:"):].strip()
|
|
299
|
+
for line in text.splitlines()
|
|
300
|
+
if line.startswith("data:")
|
|
301
|
+
)
|
|
302
|
+
return json.loads(text)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
init = rpc({
|
|
306
|
+
"jsonrpc": "2.0",
|
|
307
|
+
"id": 1,
|
|
308
|
+
"method": "initialize",
|
|
309
|
+
"params": {
|
|
310
|
+
"protocolVersion": "2025-06-18",
|
|
311
|
+
"capabilities": {},
|
|
312
|
+
"clientInfo": {"name": "graphify-mesh-smoke", "version": "0"},
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
if "error" in init:
|
|
316
|
+
sys.exit(f"::error::initialize failed: {init['error']}")
|
|
317
|
+
print("OK: initialize ->", init["result"]["serverInfo"])
|
|
318
|
+
|
|
319
|
+
listed = rpc({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}})
|
|
320
|
+
if "error" in listed:
|
|
321
|
+
sys.exit(f"::error::tools/list failed: {listed['error']}")
|
|
322
|
+
names = {t["name"] for t in listed["result"]["tools"]}
|
|
323
|
+
print("OK: tools/list ->", sorted(names))
|
|
324
|
+
if names != EXPECTED:
|
|
325
|
+
sys.exit(f"::error::tool set mismatch: missing={EXPECTED - names} unexpected={names - EXPECTED}")
|
|
326
|
+
|
|
327
|
+
# The negative half. A smoke that only ever sends the right token proves the
|
|
328
|
+
# happy path and nothing about the thing the token exists for.
|
|
329
|
+
for label, bearer in (("no token", None), ("wrong token", "x" * 64)):
|
|
330
|
+
try:
|
|
331
|
+
status, text = post({"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}, bearer)
|
|
332
|
+
except urllib.error.HTTPError as err:
|
|
333
|
+
print(f"OK: {label} refused with HTTP {err.code}")
|
|
334
|
+
continue
|
|
335
|
+
sys.exit(f"::error::{label} was accepted with HTTP {status}: {text[:200]}")
|
|
336
|
+
PY
|
|
337
|
+
|
|
338
|
+
step "All three phases passed"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
# The nightly run asks a question no push-triggered run can ask: did the
|
|
8
|
+
# world change while this repository stood still? pip-audit answers it for
|
|
9
|
+
# the dependency tree, since a CVE gets published against code that has not
|
|
10
|
+
# been touched, and the floating-latest lane of graphify-compat answers it
|
|
11
|
+
# for upstream graphifyy, whose next release can break the two coupling
|
|
12
|
+
# points this package imports. Both stay green on the last push and go red
|
|
13
|
+
# on their own schedule.
|
|
14
|
+
schedule:
|
|
15
|
+
- cron: '0 3 * * *'
|
|
16
|
+
# Manual re-run for the same two checks, without waiting for 03:00.
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
# A newer push to the same branch or pull request makes the run in flight
|
|
23
|
+
# pointless, so cancel it.
|
|
24
|
+
concurrency:
|
|
25
|
+
group: ci-${{ github.ref }}
|
|
26
|
+
cancel-in-progress: true
|
|
27
|
+
|
|
28
|
+
# One pipeline, four stages, in this order: security, tests, build, smoke. Each
|
|
29
|
+
# stage is a reusable workflow whose jobs run in parallel; the stages themselves
|
|
30
|
+
# are sequential, so a leaked secret stops the run before anything is built and
|
|
31
|
+
# a failing test stops it before anything is published. release.yml calls these
|
|
32
|
+
# same four stage workflows in the same order and adds the release stage on top,
|
|
33
|
+
# so a tag cannot take a shortcut past a check a pull request has to pass.
|
|
34
|
+
jobs:
|
|
35
|
+
security:
|
|
36
|
+
uses: ./.github/workflows/stage-security.yml
|
|
37
|
+
|
|
38
|
+
tests:
|
|
39
|
+
needs: security
|
|
40
|
+
uses: ./.github/workflows/stage-tests.yml
|
|
41
|
+
|
|
42
|
+
# No expected_version on this path: outside a release there is no tag to
|
|
43
|
+
# compare the built wheel against, so the build runs without the version gate.
|
|
44
|
+
build:
|
|
45
|
+
needs: tests
|
|
46
|
+
uses: ./.github/workflows/stage-build.yml
|
|
47
|
+
|
|
48
|
+
smoke:
|
|
49
|
+
needs: build
|
|
50
|
+
uses: ./.github/workflows/stage-smoke.yml
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Pages
|
|
2
|
+
|
|
3
|
+
# The site is two sources joined at deploy time: the Jekyll landing page from
|
|
4
|
+
# docs/ on main, and the PEP 503 index that release.yml stores on the gh-pages
|
|
5
|
+
# branch. A branch-based Pages source can serve only one of them, which is why
|
|
6
|
+
# /simple/ returned 404 while the landing page worked.
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
paths:
|
|
12
|
+
- "docs/**"
|
|
13
|
+
- ".github/workflows/pages.yml"
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
# workflow_run uses the default branch ref, unlike workflow_call, which would
|
|
16
|
+
# inherit the release tag and be rejected by the protected Pages environment.
|
|
17
|
+
workflow_run:
|
|
18
|
+
workflows: [Release]
|
|
19
|
+
types: [completed]
|
|
20
|
+
|
|
21
|
+
# Default deny; each job below requests only what it needs.
|
|
22
|
+
permissions: {}
|
|
23
|
+
|
|
24
|
+
# One deployment at a time, and never cancel one in flight: a cancelled deploy
|
|
25
|
+
# can leave the site on a half-finished release. Queued runs still collapse.
|
|
26
|
+
concurrency:
|
|
27
|
+
group: pages
|
|
28
|
+
cancel-in-progress: false
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
build:
|
|
32
|
+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
steps:
|
|
37
|
+
# Always main: a release run starts from a tag ref, but the site shows the
|
|
38
|
+
# docs as they are on main.
|
|
39
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
40
|
+
with:
|
|
41
|
+
ref: main
|
|
42
|
+
persist-credentials: false
|
|
43
|
+
|
|
44
|
+
- name: Check whether gh-pages exists
|
|
45
|
+
# Same check as release.yml: actions/checkout fails noisily on a ref
|
|
46
|
+
# that does not exist, which is the state before the first release.
|
|
47
|
+
id: gh-pages-check
|
|
48
|
+
run: |
|
|
49
|
+
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
|
|
50
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
51
|
+
else
|
|
52
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
- name: Check out gh-pages
|
|
56
|
+
if: steps.gh-pages-check.outputs.exists == 'true'
|
|
57
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
58
|
+
with:
|
|
59
|
+
ref: gh-pages
|
|
60
|
+
path: index-branch
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
|
|
63
|
+
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
|
64
|
+
|
|
65
|
+
- name: Build the landing page
|
|
66
|
+
uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13
|
|
67
|
+
with:
|
|
68
|
+
source: ./docs
|
|
69
|
+
destination: ./_site
|
|
70
|
+
|
|
71
|
+
- name: Add the simple index
|
|
72
|
+
if: steps.gh-pages-check.outputs.exists == 'true'
|
|
73
|
+
# jekyll-build-pages runs in a container and leaves _site owned by
|
|
74
|
+
# root, so the runner user needs sudo to write into it.
|
|
75
|
+
run: |
|
|
76
|
+
if [ -d index-branch/simple ]; then
|
|
77
|
+
sudo mkdir -p _site/simple
|
|
78
|
+
sudo cp -a index-branch/simple/. _site/simple/
|
|
79
|
+
else
|
|
80
|
+
echo "gh-pages has no simple/ tree; deploying the landing page only"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
84
|
+
with:
|
|
85
|
+
path: ./_site
|
|
86
|
+
|
|
87
|
+
deploy:
|
|
88
|
+
needs: build
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
permissions:
|
|
91
|
+
pages: write
|
|
92
|
+
id-token: write
|
|
93
|
+
contents: read
|
|
94
|
+
environment:
|
|
95
|
+
name: github-pages
|
|
96
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
97
|
+
steps:
|
|
98
|
+
- id: deployment
|
|
99
|
+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|