Skip to content

Commit 3d6aa67

Browse files
authored
Merge pull request DeusData#1048 from DeusData/fix/999-ci-config-infra-routes
fix(pipeline): stop CI/tooling config URLs from minting infra Route nodes
2 parents 90d7315 + 81e7b73 commit 3d6aa67

2 files changed

Lines changed: 139 additions & 1 deletion

File tree

‎src/pipeline/pipeline.c‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,47 @@ static bool is_infra_file(const char *fp) {
615615
strstr(fp, ".tf") != NULL || strstr(fp, ".hcl") != NULL || strstr(fp, ".toml") != NULL);
616616
}
617617

618+
/* CI/tooling configs describe the development TOOLCHAIN — their URLs are
619+
* repository/action/registry references, never endpoints this service
620+
* exposes. Minting infra Route nodes from them lets the route matcher's
621+
* root-service heuristic attach every handler of an ambiguous "/" route to
622+
* each tooling URL (junk HANDLES churn on plain pallets/flask, #999).
623+
* Deny by file identity, not URL shape: deployment configs (Cloud
624+
* Scheduler, compose) keep minting their genuine endpoints. */
625+
static bool is_ci_tooling_config(const char *fp) {
626+
if (!fp) {
627+
return false;
628+
}
629+
if (strstr(fp, ".github/") != NULL || strstr(fp, ".gitlab/") != NULL ||
630+
strstr(fp, ".circleci/") != NULL) {
631+
return true;
632+
}
633+
const char *slash = strrchr(fp, '/');
634+
const char *base = slash ? slash + 1 : fp;
635+
static const char *const tooling[] = {".pre-commit-config.yaml",
636+
".pre-commit-hooks.yaml",
637+
".gitlab-ci.yml",
638+
".travis.yml",
639+
"azure-pipelines.yml",
640+
"appveyor.yml",
641+
"bitbucket-pipelines.yml",
642+
".readthedocs.yaml",
643+
".readthedocs.yml",
644+
"codecov.yml",
645+
".codecov.yml",
646+
".goreleaser.yaml",
647+
".goreleaser.yml",
648+
".golangci.yml",
649+
".golangci.yaml",
650+
NULL};
651+
for (int i = 0; tooling[i]; i++) {
652+
if (strcmp(base, tooling[i]) == 0) {
653+
return true;
654+
}
655+
}
656+
return false;
657+
}
658+
618659
/* True when a YAML key path denotes an UPSTREAM dependency, CONFIG value, or
619660
* HEALTHCHECK target rather than an endpoint this service exposes. Such URLs
620661
* (auth JWKS, downstream service base URLs, package-registry URLs, healthcheck
@@ -683,7 +724,8 @@ static void cbm_pipeline_extract_infra_routes(cbm_gbuf_t *gbuf, const cbm_file_i
683724
CBMHashTable *denied = cbm_ht_create(16);
684725
for (int pass = 0; pass < 2; pass++) {
685726
for (int i = 0; i < file_count; i++) {
686-
if (!result_cache[i] || !is_infra_file(files[i].rel_path)) {
727+
if (!result_cache[i] || !is_infra_file(files[i].rel_path) ||
728+
is_ci_tooling_config(files[i].rel_path)) {
687729
continue;
688730
}
689731
for (int si = 0; si < result_cache[i]->string_refs.count; si++) {

‎tests/test_lang_contract.c‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,100 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) {
12071207
PASS();
12081208
}
12091209

1210+
/* #999: URLs in CI/tooling configs (.pre-commit-config.yaml, .github/
1211+
* workflows) are repository references for TOOLING, not endpoints this
1212+
* service exposes. They must not mint __route__infra__ Route nodes — the
1213+
* route matcher's root-service heuristic otherwise attaches every handler
1214+
* of an ambiguous "/" route to each junk URL (HANDLES churn on plain
1215+
* pallets/flask). */
1216+
static int count_infra_routes_matching(cbm_store_t *store, const char *project,
1217+
const char *substr) {
1218+
cbm_node_t *nodes = NULL;
1219+
int count = 0;
1220+
if (cbm_store_find_nodes_by_label(store, project, "Route", &nodes, &count) != CBM_STORE_OK)
1221+
return -1;
1222+
int hits = 0;
1223+
for (int i = 0; i < count; i++) {
1224+
const char *qn = nodes[i].qualified_name;
1225+
if (qn && strncmp(qn, "__route__infra__", (sizeof("__route__infra__") - 1)) == 0 &&
1226+
strstr(qn, substr))
1227+
hits++;
1228+
}
1229+
cbm_store_free_nodes(nodes, count);
1230+
return hits;
1231+
}
1232+
1233+
TEST(contract_edge_no_infra_routes_from_ci_configs_issue999) {
1234+
LangProj lp;
1235+
static const LangFile f[] = {
1236+
{"app.py", "from flask import Flask\n"
1237+
"app = Flask(__name__)\n"
1238+
"\n"
1239+
"@app.route(\"/\")\n"
1240+
"def index():\n"
1241+
" return \"hi\"\n"
1242+
"\n"
1243+
"@app.route(\"/auth/login\")\n"
1244+
"def login():\n"
1245+
" return \"login\"\n"},
1246+
{".pre-commit-config.yaml", "repos:\n"
1247+
" - repo: https://github.com/astral-sh/ruff-pre-commit\n"
1248+
" rev: v0.1.0\n"
1249+
" hooks:\n"
1250+
" - id: ruff\n"
1251+
" - repo: https://github.com/pre-commit/pre-commit-hooks\n"
1252+
" rev: v4.5.0\n"
1253+
" hooks:\n"
1254+
" - id: trailing-whitespace\n"},
1255+
{".github/workflows/ci.yml", "name: CI\n"
1256+
"on: [push]\n"
1257+
"jobs:\n"
1258+
" build:\n"
1259+
" runs-on: ubuntu-latest\n"
1260+
" steps:\n"
1261+
" - uses: actions/checkout@v4\n"
1262+
" - run: curl https://coverage.example.io/upload\n"}};
1263+
cbm_store_t *store = lang_index_files(&lp, f, 3);
1264+
ASSERT_NOT_NULL(store);
1265+
/* No tooling URL may materialize as an infra Route node. */
1266+
int gh_routes = count_infra_routes_matching(store, lp.project, "github.com");
1267+
int wf_routes = count_infra_routes_matching(store, lp.project, "coverage.example.io");
1268+
if (gh_routes != 0 || wf_routes != 0) {
1269+
fprintf(stderr,
1270+
" [999] FAIL infra routes from CI configs: github.com=%d workflows=%d "
1271+
"(expected 0/0)\n",
1272+
gh_routes, wf_routes);
1273+
}
1274+
ASSERT_EQ(gh_routes, 0);
1275+
ASSERT_EQ(wf_routes, 0);
1276+
lang_cleanup(&lp, store);
1277+
PASS();
1278+
}
1279+
1280+
/* #999 inverse guard: genuine infra endpoint URLs (Cloud Scheduler push
1281+
* targets and similar deployment configs) must STILL mint infra Route
1282+
* nodes — the CI/tooling deny must be file-scoped, not a blanket URL kill. */
1283+
TEST(contract_edge_infra_routes_from_deploy_configs_still_minted) {
1284+
LangProj lp;
1285+
static const LangFile f[] = {
1286+
{"scheduler.yaml",
1287+
"jobs:\n"
1288+
" - name: nightly-sync\n"
1289+
" schedule: \"0 3 * * *\"\n"
1290+
" push_endpoint: https://sync.internal.example/api/v1/sync\n"}};
1291+
cbm_store_t *store = lang_index_files(&lp, f, 1);
1292+
ASSERT_NOT_NULL(store);
1293+
int routes = count_infra_routes_matching(store, lp.project, "sync.internal.example");
1294+
if (routes < 1) {
1295+
fprintf(stderr, " [999] FAIL deploy-config endpoint minted %d infra routes "
1296+
"(expected >=1)\n",
1297+
routes);
1298+
}
1299+
ASSERT_TRUE(routes >= 1);
1300+
lang_cleanup(&lp, store);
1301+
PASS();
1302+
}
1303+
12101304
/* True if some CALLS edge's TARGET node carries `label` and a QN ending with
12111305
* `qn_suffix` — i.e. the call resolved to that specific definition. */
12121306
static int calls_edge_targets(cbm_store_t *store, const char *project, const char *label,
@@ -1478,6 +1572,8 @@ SUITE(lang_contract) {
14781572
RUN_TEST(contract_edge_workspaces_imports_issue408);
14791573
RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767);
14801574
RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767);
1575+
RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999);
1576+
RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted);
14811577
RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871);
14821578
RUN_TEST(contract_edge_depends_on);
14831579
RUN_TEST(contract_edge_parallel_service_edges);

0 commit comments

Comments
 (0)