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
36 changes: 24 additions & 12 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,28 @@ jobs:
# composes the metamodel provider set so codegen-base/om classpath SPI does not pollute it).
mvn -pl codegen-kotlin test -Dtest='ObjectModelConformanceTest,OutputPromptConformanceTest,ValidationConformanceTest,GeneratorRegistryConformanceTest,RegistryManifestConformanceTest' -q

java-smoke-compile:
# The scoped `mvn -pl ... test` jobs above compile only metadata / render /
# codegen-spring / codegen-kotlin (+ their -am deps). Whole modules are
# otherwise NEVER built by CI: codegen-mustache, codegen-plantuml,
# maven-plugin, dynamic, core-spring, metadata-ktx, omdb-ktx,
# spring-boot-starter, om, omdb. A compile break in any of them ships green.
# A cheap reactor-wide `install -DskipTests` compiles + packages every module
# in the parent reactor (integration-tests* are intentionally out-of-reactor —
# they need Docker), so a break in the Maven plugin / Spring Boot starter /
# mustache/plantuml/ktx facades fails CI here.
java-reactor:
# FULL-REACTOR build AND test of the Java parent reactor (server/java).
#
# The scoped `mvn -pl ... test` jobs above run ONLY a curated `-Dtest=` subset
# in metadata / render / codegen-spring / codegen-kotlin (+ their -am deps).
# Every other reactor module's tests are otherwise NEVER run by CI — most
# notably the `maven-plugin` mojo tests (MetaDataGeneratorMojoTest, DocsMojoTest,
# MetaDataVerifyMojoTest, ...). That gap let issue #37 ship: a mojo-test fixture
# broke `mvn clean install` on `main` while every PR showed green, because this
# job used to run `install -DskipTests` (compile only, no tests).
#
# Running a full-reactor `mvn install` (tests ON) compiles, packages, AND tests
# every module in the parent reactor — so a red reactor can no longer slip past
# green PR checks. No reactor module's tests need Docker/Testcontainers (verified:
# the only Testcontainers suites are integration-tests* and integration-tests-kotlin,
# which are INTENTIONALLY out-of-reactor and gated separately by integration-tests.yml).
#
# Intentionally still OUT of this gate (documented):
# - integration-tests / integration-tests-kotlin — out-of-reactor Docker/
# Testcontainers persistence + api-contract corpora; gated by integration-tests.yml.
# - fatjar-smoke — standalone Spring Boot fat-jar bootstrap smoke (no tests);
# out-of-reactor by design, run on demand via scripts/fatjar-smoke.sh.
needs: fixture-lint
runs-on: ubuntu-latest
steps:
Expand All @@ -180,8 +192,8 @@ jobs:
distribution: 'temurin'
java-version: '21'
cache: maven
- name: Reactor-wide compile (all modules, no tests)
run: cd server/java && mvn -q install -DskipTests
- name: Full-reactor build + test (all modules, tests on)
run: cd server/java && mvn -q clean install

completeness-gate:
needs: [conformance, conformance-kotlin]
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

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

## [0.12.5] — 2026-06-27

_npm `0.12.5` (full lockstep across all 13 `@metaobjectsdev/*` publish candidates)._
Expand Down
11 changes: 11 additions & 0 deletions server/java/codegen-kotlin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,17 @@
<version>1.7.3</version>
<scope>test</scope>
</dependency>
<!-- KotlinM2mTraversalCompileRunTest compiles the GENERATED Exposed Table + M:N relation
helpers against exposed-core, resolving the jar from the local m2 repo. Declaring it
test-scoped here forces Maven to resolve it into ~/.m2 during this module's build, so
the test no longer depends on a prior build (e.g. the reactor-excluded
integration-tests-kotlin) having populated the local repository first. -->
<dependency>
<groupId>org.jetbrains.exposed</groupId>
<artifactId>exposed-core</artifactId>
<version>0.55.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
57 changes: 57 additions & 0 deletions server/typescript/packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,24 @@ const META_COMMON_JSON = JSON.stringify(
2,
) + "\n";

// Issue #75 — a multi-target codegen config can route a target's outDir under
// `.metaobjects/<targetName>/src/generated/`. That output is the regenerable
// shadow (the canonical output lives at the configured outDir; re-running
// `meta gen` recreates the shadow), so it must NOT be committed by default. We
// ignore the per-target generated shadow with a narrow `*/src/generated/`
// pattern, then explicitly re-include `migrations/` and `config.json` so the
// tracked artifacts are never swept up even if a future broad pattern were added.
const METAOBJECTS_GITIGNORE_BODY = `.gen-state/

# Per-target codegen output routed under .metaobjects/<target>/ is regenerable
# (re-run \`meta gen\`); never commit it. The canonical output is your configured
# outDir, not this shadow.
*/src/generated/

# These ARE meant to be tracked — keep them even if a broad pattern matches.
!migrations/
!config.json
!package.meta.json
`;

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

/**
* Walk up from `start` looking for a `.git` directory; return the repo root, or
* undefined when `start` is not inside a git working tree. (`.git` can be a file
* in worktrees/submodules — accept either a dir or a file.)
*/
function findGitRoot(start: string): string | undefined {
let dir = start;
// eslint-disable-next-line no-constant-condition
while (true) {
if (existsSyncWrap(join(dir, ".git"))) return dir;
const parent = dirname(dir);
if (parent === dir) return undefined; // reached filesystem root
dir = parent;
}
}

/**
* Issue #77 — Claude Code discovers `.claude/skills/` only from cwd + ANCESTOR
* dirs + the user level; it never walks DOWN into subdirs. So scaffolding the
* agent-context into a monorepo subdir means a root-launched session won't load
* the skills (the common case). When the init dir is inside a git repo whose
* root is an ANCESTOR (i.e. a subdir init), warn and point the user at the repo
* root. The metadata/config/migrations correctly stay in the subdir regardless.
*/
function warnIfMonorepoSubdir(opts: InitOptions, result: InitResult): void {
if (opts.noSkills) return; // no skills written → nothing to warn about
const gitRoot = findGitRoot(opts.cwd);
if (gitRoot === undefined || gitRoot === opts.cwd) return; // repo root or non-git → fine
const lang = opts.servers && opts.servers.length > 0 ? opts.servers[0]! : "<lang>";
result.warnings.push(
"agent-context skills scaffolded into a monorepo subdir won't be discovered from a " +
"root-launched session (Claude Code only walks cwd + ancestors). Scaffold the context " +
`at the repo root instead: cd <repo-root> && meta init --docs-only --server ${lang}`,
);
}

async function writeAgentContext(opts: InitOptions, result: InitResult): Promise<void> {
warnIfMonorepoSubdir(opts, result);
const stack = resolveStack(opts.cwd, { servers: opts.servers ?? [], clients: opts.clients ?? [] });
let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack });
if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/"));
Expand Down Expand Up @@ -367,6 +421,9 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.");
} else {
log.info(nextStepsBlock());
// Surface any scaffold warnings (e.g. the #77 monorepo-subdir agent-context
// discovery warning) — these are otherwise dropped on the normal init path.
for (const w of result.warnings) log.warn(w);
}
}
return 0;
Expand Down
53 changes: 53 additions & 0 deletions server/typescript/packages/cli/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ describe("init() — happy path", () => {
expect(ignore).toContain(".gen-state/");
});

// Issue #75 — a multi-target codegen config can route a target's outDir under
// .metaobjects/<targetName>/src/generated/. That output is regenerable (re-run
// `meta gen` recreates it) and must NOT be committed by default. The scaffolded
// .gitignore must ignore the per-target shadow WITHOUT ignoring the tracked
// migrations/ or config.json.
test("scaffolded .gitignore ignores per-target generated shadow but tracks migrations/ and config.json", async () => {
await init({ cwd });
const ignore = readFileSync(join(cwd, ".metaobjects", ".gitignore"), "utf8");
// The per-target generated shadow pattern is present.
expect(ignore).toContain("*/src/generated/");
// migrations/ and config.json are NOT ignored (they are meant to be tracked).
const lines = ignore.split("\n").map((l) => l.trim());
expect(lines).not.toContain("migrations/");
expect(lines).not.toContain("migrations");
expect(lines).not.toContain("config.json");
// A negated re-include guard keeps migrations tracked even if a broad pattern
// were ever to match.
expect(ignore).toContain("!migrations/");
});

test("does NOT create legacy .meta/ directory", async () => {
await init({ cwd });
expect(existsSync(join(cwd, ".meta"))).toBe(false);
Expand Down Expand Up @@ -134,6 +154,39 @@ describe("init() --force config preservation", () => {
});
});

// Issue #77 — `meta init` scaffolds agent-context skills relative to cwd. In a
// monorepo subdir the skills land where Claude Code won't discover them (it only
// walks cwd + ancestors + user level, never down into subdirs). Detect that case
// and WARN, pointing the user at the repo root.
describe("init() — monorepo-subdir agent-context warning (#77)", () => {
test("warns when init runs from a subdir of a git repo (skills won't be discovered from root)", async () => {
// cwd is a temp dir; make it a git repo root, then init from a nested subdir.
mkdirSync(join(cwd, ".git"));
const subdir = join(cwd, "packages", "api");
mkdirSync(subdir, { recursive: true });

const result = await init({ cwd: subdir });
const warned = result.warnings.some(
(w) => /repo root/i.test(w) && /--docs-only/.test(w),
);
expect(warned).toBe(true);
});

test("does NOT warn when init runs at the git repo root", async () => {
mkdirSync(join(cwd, ".git"));
const result = await init({ cwd });
const warned = result.warnings.some((w) => /repo root/i.test(w) && /--docs-only/.test(w));
expect(warned).toBe(false);
});

test("does NOT warn when init runs in a non-git directory", async () => {
// cwd has no .git anywhere up the tree (tmpdir).
const result = await init({ cwd });
const warned = result.warnings.some((w) => /repo root/i.test(w) && /--docs-only/.test(w));
expect(warned).toBe(false);
});
});

describe("init --d1", () => {
test("scaffolds config with migrate.dialect = 'd1' and prefilled binding from wrangler.toml", async () => {
writeFileSync(join(cwd, "wrangler.toml"), [
Expand Down
Loading