Skip to content
Open
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
44 changes: 43 additions & 1 deletion src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,47 @@ static bool is_infra_file(const char *fp) {
strstr(fp, ".tf") != NULL || strstr(fp, ".hcl") != NULL || strstr(fp, ".toml") != NULL);
}

/* CI/tooling configs describe the development TOOLCHAIN — their URLs are
* repository/action/registry references, never endpoints this service
* exposes. Minting infra Route nodes from them lets the route matcher's
* root-service heuristic attach every handler of an ambiguous "/" route to
* each tooling URL (junk HANDLES churn on plain pallets/flask, #999).
* Deny by file identity, not URL shape: deployment configs (Cloud
* Scheduler, compose) keep minting their genuine endpoints. */
static bool is_ci_tooling_config(const char *fp) {
if (!fp) {
return false;
}
if (strstr(fp, ".github/") != NULL || strstr(fp, ".gitlab/") != NULL ||
strstr(fp, ".circleci/") != NULL) {
return true;
}
const char *slash = strrchr(fp, '/');
const char *base = slash ? slash + 1 : fp;
static const char *const tooling[] = {".pre-commit-config.yaml",
".pre-commit-hooks.yaml",
".gitlab-ci.yml",
".travis.yml",
"azure-pipelines.yml",
"appveyor.yml",
"bitbucket-pipelines.yml",
".readthedocs.yaml",
".readthedocs.yml",
"codecov.yml",
".codecov.yml",
".goreleaser.yaml",
".goreleaser.yml",
".golangci.yml",
".golangci.yaml",
NULL};
for (int i = 0; tooling[i]; i++) {
if (strcmp(base, tooling[i]) == 0) {
return true;
}
}
return false;
}

/* True when a YAML key path denotes an UPSTREAM dependency, CONFIG value, or
* HEALTHCHECK target rather than an endpoint this service exposes. Such URLs
* (auth JWKS, downstream service base URLs, package-registry URLs, healthcheck
Expand Down Expand Up @@ -681,7 +722,8 @@ static void cbm_pipeline_extract_infra_routes(cbm_gbuf_t *gbuf, const cbm_file_i
CBMHashTable *denied = cbm_ht_create(16);
for (int pass = 0; pass < 2; pass++) {
for (int i = 0; i < file_count; i++) {
if (!result_cache[i] || !is_infra_file(files[i].rel_path)) {
if (!result_cache[i] || !is_infra_file(files[i].rel_path) ||
is_ci_tooling_config(files[i].rel_path)) {
continue;
}
for (int si = 0; si < result_cache[i]->string_refs.count; si++) {
Expand Down
96 changes: 96 additions & 0 deletions tests/test_lang_contract.c
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,100 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) {
PASS();
}

/* #999: URLs in CI/tooling configs (.pre-commit-config.yaml, .github/
* workflows) are repository references for TOOLING, not endpoints this
* service exposes. They must not mint __route__infra__ Route nodes — the
* route matcher's root-service heuristic otherwise attaches every handler
* of an ambiguous "/" route to each junk URL (HANDLES churn on plain
* pallets/flask). */
static int count_infra_routes_matching(cbm_store_t *store, const char *project,
const char *substr) {
cbm_node_t *nodes = NULL;
int count = 0;
if (cbm_store_find_nodes_by_label(store, project, "Route", &nodes, &count) != CBM_STORE_OK)
return -1;
int hits = 0;
for (int i = 0; i < count; i++) {
const char *qn = nodes[i].qualified_name;
if (qn && strncmp(qn, "__route__infra__", (sizeof("__route__infra__") - 1)) == 0 &&
strstr(qn, substr))
hits++;
}
cbm_store_free_nodes(nodes, count);
return hits;
}

TEST(contract_edge_no_infra_routes_from_ci_configs_issue999) {
LangProj lp;
static const LangFile f[] = {
{"app.py", "from flask import Flask\n"
"app = Flask(__name__)\n"
"\n"
"@app.route(\"/\")\n"
"def index():\n"
" return \"hi\"\n"
"\n"
"@app.route(\"/auth/login\")\n"
"def login():\n"
" return \"login\"\n"},
{".pre-commit-config.yaml", "repos:\n"
" - repo: https://github.com/astral-sh/ruff-pre-commit\n"
" rev: v0.1.0\n"
" hooks:\n"
" - id: ruff\n"
" - repo: https://github.com/pre-commit/pre-commit-hooks\n"
" rev: v4.5.0\n"
" hooks:\n"
" - id: trailing-whitespace\n"},
{".github/workflows/ci.yml", "name: CI\n"
"on: [push]\n"
"jobs:\n"
" build:\n"
" runs-on: ubuntu-latest\n"
" steps:\n"
" - uses: actions/checkout@v4\n"
" - run: curl https://coverage.example.io/upload\n"}};
cbm_store_t *store = lang_index_files(&lp, f, 3);
ASSERT_NOT_NULL(store);
/* No tooling URL may materialize as an infra Route node. */
int gh_routes = count_infra_routes_matching(store, lp.project, "github.com");
int wf_routes = count_infra_routes_matching(store, lp.project, "coverage.example.io");
if (gh_routes != 0 || wf_routes != 0) {
fprintf(stderr,
" [999] FAIL infra routes from CI configs: github.com=%d workflows=%d "
"(expected 0/0)\n",
gh_routes, wf_routes);
}
ASSERT_EQ(gh_routes, 0);
ASSERT_EQ(wf_routes, 0);
lang_cleanup(&lp, store);
PASS();
}

/* #999 inverse guard: genuine infra endpoint URLs (Cloud Scheduler push
* targets and similar deployment configs) must STILL mint infra Route
* nodes — the CI/tooling deny must be file-scoped, not a blanket URL kill. */
TEST(contract_edge_infra_routes_from_deploy_configs_still_minted) {
LangProj lp;
static const LangFile f[] = {
{"scheduler.yaml",
"jobs:\n"
" - name: nightly-sync\n"
" schedule: \"0 3 * * *\"\n"
" push_endpoint: https://sync.internal.example/api/v1/sync\n"}};
cbm_store_t *store = lang_index_files(&lp, f, 1);
ASSERT_NOT_NULL(store);
int routes = count_infra_routes_matching(store, lp.project, "sync.internal.example");
if (routes < 1) {
fprintf(stderr, " [999] FAIL deploy-config endpoint minted %d infra routes "
"(expected >=1)\n",
routes);
}
ASSERT_TRUE(routes >= 1);
lang_cleanup(&lp, store);
PASS();
}

/* DEPENDS_ON — Helm Chart.yaml `dependencies:` -> per-dependency Chart node.
* Basename must be exactly "Chart.yaml"; pass_k8s runs in both pipeline paths. */
TEST(contract_edge_depends_on) {
Expand Down Expand Up @@ -1411,6 +1505,8 @@ SUITE(lang_contract) {
RUN_TEST(contract_edge_workspaces_imports_issue408);
RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767);
RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767);
RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999);
RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted);
RUN_TEST(contract_edge_depends_on);
RUN_TEST(contract_edge_parallel_service_edges);
RUN_TEST(contract_edge_file_changes_with);
Expand Down
Loading