Skip to content

Commit c90259c

Browse files
dmealingclaude
andauthored
fix(cli): harden meta init scaffold and close the Java CI reactor gate (#82)
* fix: close the Java CI gate (#37) + harden meta init scaffold (#75, #77) #37 — Java CI ran `mvn install -DskipTests` (compile-only), so the maven-plugin mojo tests never ran and a red reactor sat on main while PRs showed green. The conformance `java-smoke-compile` job → `java-reactor` now runs `mvn clean install` (full reactor WITH tests, all 14 modules). integration-tests (Docker) + fatjar-smoke (on-demand) stay out, documented. NOTE: branch protection must add the required check `conformance / java-reactor` (job renamed). #75 — meta init's scaffolded .metaobjects/.gitignore ignored only .gen-state/, so a per-target outDir under .metaobjects/<target>/src/generated/ (regenerable) got committed by default. Added `*/src/generated/` + re-include guards (!migrations/, !config.json, !package.meta.json) so the shadow is ignored but tracked artifacts can't be swept up. #77 — meta init scaffolds skills relative to cwd; in a monorepo subdir they don't load from a root-launched session (Claude Code discovers skills from cwd+ancestors, not down into subdirs). Added a monorepo-subdir warning (findGitRoot) pointing to `cd <repo-root> && meta init --docs-only --server <lang>`, and surfaced result.warnings on the init output path (previously dropped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * no-mistakes(document): docs: changelog entries for meta init gitignore + monorepo-subdir fixes * no-mistakes: apply CI fixes --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 230b36f commit c90259c

5 files changed

Lines changed: 161 additions & 12 deletions

File tree

.github/workflows/conformance.yml

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,28 @@ jobs:
160160
# composes the metamodel provider set so codegen-base/om classpath SPI does not pollute it).
161161
mvn -pl codegen-kotlin test -Dtest='ObjectModelConformanceTest,OutputPromptConformanceTest,ValidationConformanceTest,GeneratorRegistryConformanceTest,RegistryManifestConformanceTest' -q
162162
163-
java-smoke-compile:
164-
# The scoped `mvn -pl ... test` jobs above compile only metadata / render /
165-
# codegen-spring / codegen-kotlin (+ their -am deps). Whole modules are
166-
# otherwise NEVER built by CI: codegen-mustache, codegen-plantuml,
167-
# maven-plugin, dynamic, core-spring, metadata-ktx, omdb-ktx,
168-
# spring-boot-starter, om, omdb. A compile break in any of them ships green.
169-
# A cheap reactor-wide `install -DskipTests` compiles + packages every module
170-
# in the parent reactor (integration-tests* are intentionally out-of-reactor —
171-
# they need Docker), so a break in the Maven plugin / Spring Boot starter /
172-
# mustache/plantuml/ktx facades fails CI here.
163+
java-reactor:
164+
# FULL-REACTOR build AND test of the Java parent reactor (server/java).
165+
#
166+
# The scoped `mvn -pl ... test` jobs above run ONLY a curated `-Dtest=` subset
167+
# in metadata / render / codegen-spring / codegen-kotlin (+ their -am deps).
168+
# Every other reactor module's tests are otherwise NEVER run by CI — most
169+
# notably the `maven-plugin` mojo tests (MetaDataGeneratorMojoTest, DocsMojoTest,
170+
# MetaDataVerifyMojoTest, ...). That gap let issue #37 ship: a mojo-test fixture
171+
# broke `mvn clean install` on `main` while every PR showed green, because this
172+
# job used to run `install -DskipTests` (compile only, no tests).
173+
#
174+
# Running a full-reactor `mvn install` (tests ON) compiles, packages, AND tests
175+
# every module in the parent reactor — so a red reactor can no longer slip past
176+
# green PR checks. No reactor module's tests need Docker/Testcontainers (verified:
177+
# the only Testcontainers suites are integration-tests* and integration-tests-kotlin,
178+
# which are INTENTIONALLY out-of-reactor and gated separately by integration-tests.yml).
179+
#
180+
# Intentionally still OUT of this gate (documented):
181+
# - integration-tests / integration-tests-kotlin — out-of-reactor Docker/
182+
# Testcontainers persistence + api-contract corpora; gated by integration-tests.yml.
183+
# - fatjar-smoke — standalone Spring Boot fat-jar bootstrap smoke (no tests);
184+
# out-of-reactor by design, run on demand via scripts/fatjar-smoke.sh.
173185
needs: fixture-lint
174186
runs-on: ubuntu-latest
175187
steps:
@@ -180,8 +192,8 @@ jobs:
180192
distribution: 'temurin'
181193
java-version: '21'
182194
cache: maven
183-
- name: Reactor-wide compile (all modules, no tests)
184-
run: cd server/java && mvn -q install -DskipTests
195+
- name: Full-reactor build + test (all modules, tests on)
196+
run: cd server/java && mvn -q clean install
185197

186198
completeness-gate:
187199
needs: [conformance, conformance-kotlin]

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **cli — `meta init` gitignore hardening:** the scaffolded
12+
`.metaobjects/.gitignore` previously ignored only `.gen-state/`, so a
13+
multi-target codegen config routing a target's `outDir` under
14+
`.metaobjects/<target>/src/generated/` let that regenerable generated shadow
15+
get committed by default. The scaffold now also ignores `*/src/generated/` and
16+
re-includes the tracked artifacts (`!migrations/`, `!config.json`,
17+
`!package.meta.json`) so they can never be swept up.
18+
- **cli — `meta init` monorepo-subdir warning:** scaffolding the agent-context
19+
`.claude/skills/` into a git subdirectory means a repo-root-launched Claude
20+
session won't discover the skills (discovery walks cwd + ancestors, never down
21+
into subdirs). `meta init` now warns when run inside a subdir of a git repo and
22+
points at `cd <repo-root> && meta init --docs-only --server <lang>`. Scaffold
23+
warnings are also now surfaced on the normal init output path (previously
24+
dropped).
25+
1026
## [0.12.5] — 2026-06-27
1127

1228
_npm `0.12.5` (full lockstep across all 13 `@metaobjectsdev/*` publish candidates)._

server/java/codegen-kotlin/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@
132132
<version>1.7.3</version>
133133
<scope>test</scope>
134134
</dependency>
135+
<!-- KotlinM2mTraversalCompileRunTest compiles the GENERATED Exposed Table + M:N relation
136+
helpers against exposed-core, resolving the jar from the local m2 repo. Declaring it
137+
test-scoped here forces Maven to resolve it into ~/.m2 during this module's build, so
138+
the test no longer depends on a prior build (e.g. the reactor-excluded
139+
integration-tests-kotlin) having populated the local repository first. -->
140+
<dependency>
141+
<groupId>org.jetbrains.exposed</groupId>
142+
<artifactId>exposed-core</artifactId>
143+
<version>0.55.0</version>
144+
<scope>test</scope>
145+
</dependency>
135146
</dependencies>
136147

137148
<build>

server/typescript/packages/cli/src/commands/init.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,24 @@ const META_COMMON_JSON = JSON.stringify(
2424
2,
2525
) + "\n";
2626

27+
// Issue #75 — a multi-target codegen config can route a target's outDir under
28+
// `.metaobjects/<targetName>/src/generated/`. That output is the regenerable
29+
// shadow (the canonical output lives at the configured outDir; re-running
30+
// `meta gen` recreates the shadow), so it must NOT be committed by default. We
31+
// ignore the per-target generated shadow with a narrow `*/src/generated/`
32+
// pattern, then explicitly re-include `migrations/` and `config.json` so the
33+
// tracked artifacts are never swept up even if a future broad pattern were added.
2734
const METAOBJECTS_GITIGNORE_BODY = `.gen-state/
35+
36+
# Per-target codegen output routed under .metaobjects/<target>/ is regenerable
37+
# (re-run \`meta gen\`); never commit it. The canonical output is your configured
38+
# outDir, not this shadow.
39+
*/src/generated/
40+
41+
# These ARE meant to be tracked — keep them even if a broad pattern matches.
42+
!migrations/
43+
!config.json
44+
!package.meta.json
2845
`;
2946

3047
function buildMetaobjectsConfigBody(dialect: "sqlite" | "postgres" | "d1" = "sqlite"): string {
@@ -96,7 +113,44 @@ async function readManifest(cwd: string): Promise<Manifest | undefined> {
96113
try { return JSON.parse(await readFile(p, "utf8")) as Manifest; } catch { return undefined; }
97114
}
98115

116+
/**
117+
* Walk up from `start` looking for a `.git` directory; return the repo root, or
118+
* undefined when `start` is not inside a git working tree. (`.git` can be a file
119+
* in worktrees/submodules — accept either a dir or a file.)
120+
*/
121+
function findGitRoot(start: string): string | undefined {
122+
let dir = start;
123+
// eslint-disable-next-line no-constant-condition
124+
while (true) {
125+
if (existsSyncWrap(join(dir, ".git"))) return dir;
126+
const parent = dirname(dir);
127+
if (parent === dir) return undefined; // reached filesystem root
128+
dir = parent;
129+
}
130+
}
131+
132+
/**
133+
* Issue #77 — Claude Code discovers `.claude/skills/` only from cwd + ANCESTOR
134+
* dirs + the user level; it never walks DOWN into subdirs. So scaffolding the
135+
* agent-context into a monorepo subdir means a root-launched session won't load
136+
* the skills (the common case). When the init dir is inside a git repo whose
137+
* root is an ANCESTOR (i.e. a subdir init), warn and point the user at the repo
138+
* root. The metadata/config/migrations correctly stay in the subdir regardless.
139+
*/
140+
function warnIfMonorepoSubdir(opts: InitOptions, result: InitResult): void {
141+
if (opts.noSkills) return; // no skills written → nothing to warn about
142+
const gitRoot = findGitRoot(opts.cwd);
143+
if (gitRoot === undefined || gitRoot === opts.cwd) return; // repo root or non-git → fine
144+
const lang = opts.servers && opts.servers.length > 0 ? opts.servers[0]! : "<lang>";
145+
result.warnings.push(
146+
"agent-context skills scaffolded into a monorepo subdir won't be discovered from a " +
147+
"root-launched session (Claude Code only walks cwd + ancestors). Scaffold the context " +
148+
`at the repo root instead: cd <repo-root> && meta init --docs-only --server ${lang}`,
149+
);
150+
}
151+
99152
async function writeAgentContext(opts: InitOptions, result: InitResult): Promise<void> {
153+
warnIfMonorepoSubdir(opts, result);
100154
const stack = resolveStack(opts.cwd, { servers: opts.servers ?? [], clients: opts.clients ?? [] });
101155
let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack });
102156
if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/"));
@@ -367,6 +421,9 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
367421
log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
368422
} else {
369423
log.info(nextStepsBlock());
424+
// Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
425+
// discovery warning) — these are otherwise dropped on the normal init path.
426+
for (const w of result.warnings) log.warn(w);
370427
}
371428
}
372429
return 0;

server/typescript/packages/cli/test/init.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ describe("init() — happy path", () => {
5050
expect(ignore).toContain(".gen-state/");
5151
});
5252

53+
// Issue #75 — a multi-target codegen config can route a target's outDir under
54+
// .metaobjects/<targetName>/src/generated/. That output is regenerable (re-run
55+
// `meta gen` recreates it) and must NOT be committed by default. The scaffolded
56+
// .gitignore must ignore the per-target shadow WITHOUT ignoring the tracked
57+
// migrations/ or config.json.
58+
test("scaffolded .gitignore ignores per-target generated shadow but tracks migrations/ and config.json", async () => {
59+
await init({ cwd });
60+
const ignore = readFileSync(join(cwd, ".metaobjects", ".gitignore"), "utf8");
61+
// The per-target generated shadow pattern is present.
62+
expect(ignore).toContain("*/src/generated/");
63+
// migrations/ and config.json are NOT ignored (they are meant to be tracked).
64+
const lines = ignore.split("\n").map((l) => l.trim());
65+
expect(lines).not.toContain("migrations/");
66+
expect(lines).not.toContain("migrations");
67+
expect(lines).not.toContain("config.json");
68+
// A negated re-include guard keeps migrations tracked even if a broad pattern
69+
// were ever to match.
70+
expect(ignore).toContain("!migrations/");
71+
});
72+
5373
test("does NOT create legacy .meta/ directory", async () => {
5474
await init({ cwd });
5575
expect(existsSync(join(cwd, ".meta"))).toBe(false);
@@ -134,6 +154,39 @@ describe("init() --force config preservation", () => {
134154
});
135155
});
136156

157+
// Issue #77 — `meta init` scaffolds agent-context skills relative to cwd. In a
158+
// monorepo subdir the skills land where Claude Code won't discover them (it only
159+
// walks cwd + ancestors + user level, never down into subdirs). Detect that case
160+
// and WARN, pointing the user at the repo root.
161+
describe("init() — monorepo-subdir agent-context warning (#77)", () => {
162+
test("warns when init runs from a subdir of a git repo (skills won't be discovered from root)", async () => {
163+
// cwd is a temp dir; make it a git repo root, then init from a nested subdir.
164+
mkdirSync(join(cwd, ".git"));
165+
const subdir = join(cwd, "packages", "api");
166+
mkdirSync(subdir, { recursive: true });
167+
168+
const result = await init({ cwd: subdir });
169+
const warned = result.warnings.some(
170+
(w) => /repo root/i.test(w) && /--docs-only/.test(w),
171+
);
172+
expect(warned).toBe(true);
173+
});
174+
175+
test("does NOT warn when init runs at the git repo root", async () => {
176+
mkdirSync(join(cwd, ".git"));
177+
const result = await init({ cwd });
178+
const warned = result.warnings.some((w) => /repo root/i.test(w) && /--docs-only/.test(w));
179+
expect(warned).toBe(false);
180+
});
181+
182+
test("does NOT warn when init runs in a non-git directory", async () => {
183+
// cwd has no .git anywhere up the tree (tmpdir).
184+
const result = await init({ cwd });
185+
const warned = result.warnings.some((w) => /repo root/i.test(w) && /--docs-only/.test(w));
186+
expect(warned).toBe(false);
187+
});
188+
});
189+
137190
describe("init --d1", () => {
138191
test("scaffolds config with migrate.dialect = 'd1' and prefilled binding from wrangler.toml", async () => {
139192
writeFileSync(join(cwd, "wrangler.toml"), [

0 commit comments

Comments
 (0)