diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1e797e9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Normalize line endings to LF for all text files. +# +# This repository commits the mirrored core package (run/input/core/) - a +# generated tree in the sense of CONVENTIONS §6. A CRLF checkout that +# re-commits the mirror would turn every line into a diff. The assembled +# app gets its own copy from src/.gitattributes. +* text=auto eol=lf +*.abap text eol=lf +*.xml text eol=lf +*.json text eol=lf +*.jsonc text eol=lf +*.md text eol=lf +*.mjs text eol=lf +*.js text eol=lf +*.yaml text eol=lf +*.yml text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bc8946c..5b606fc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -50,3 +50,15 @@ updates: # compatibility question attached, not a routine dependency update -- # see the pin policy in builder-abap2UI5-js:AGENTS.md. - dependency-name: openui5-dist + + # The html5 module the mta.yaml `abap2UI5` module builds (`ui5 build`). + # Same story as the root manifest above, one level down: `directory` is + # not recursive, no ecosystem covered this manifest, and its @ui5/cli + # never saw a proposed update. It publishes through to + # cap2UI5/app/z2ui5/package.json, where (as with every app dependency) + # a bump merged over there is reverted by the next publish - here is + # the only place it survives. + - package-ecosystem: npm + directory: /src/app/z2ui5 + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8ef15f..2622283 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ permissions: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/update_cap.yml b/.github/workflows/update_cap.yml index 7a7b006..6360d36 100644 --- a/.github/workflows/update_cap.yml +++ b/.github/workflows/update_cap.yml @@ -35,6 +35,7 @@ concurrency: jobs: run: runs-on: ubuntu-latest + timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/scripts/mirror-core.js b/scripts/mirror-core.js index 4ec4f11..b2d4715 100644 --- a/scripts/mirror-core.js +++ b/scripts/mirror-core.js @@ -20,6 +20,11 @@ * must not pin the nightly to an old core, so the newer of the two commits * wins (committer date; equal-or-older slot ⇒ HEAD). If the slot sha cannot * be fetched at all, the mirror falls back to HEAD with a warning. + * + * The same arbitration exists a second time in builder-cap2UI5-web's + * mirror.mjs (its trigger slot points at cap2UI5) — two implementations of + * one algorithm, kept in step by hand; whoever changes the rule here + * carries it there too. */ "use strict"; diff --git a/src/.gitattributes b/src/.gitattributes new file mode 100644 index 0000000..88a80d4 --- /dev/null +++ b/src/.gitattributes @@ -0,0 +1,16 @@ +# Normalize line endings to LF for all text files. +# +# Ships into the published cap2UI5 app, which vendors the generated core/ +# tree - CONVENTIONS §6 asks every repository carrying a generated tree for +# this file, and the app repo's content outside .github/ can only come from +# here. +* text=auto eol=lf +*.abap text eol=lf +*.xml text eol=lf +*.json text eol=lf +*.jsonc text eol=lf +*.md text eol=lf +*.mjs text eol=lf +*.js text eol=lf +*.yaml text eol=lf +*.yml text eol=lf diff --git a/src/test/webapp-overlay.test.js b/src/test/webapp-overlay.test.js new file mode 100644 index 0000000..4961981 --- /dev/null +++ b/src/test/webapp-overlay.test.js @@ -0,0 +1,77 @@ +// The same 61 webapp files exist twice in the published app, on purpose — +// and this is the gate that keeps "twice" from becoming "two versions". +// +// WHY THE COPY EXISTS +// ------------------- +// The UI5 frontend ships inside the vendored core package +// (core/app/z2ui5/webapp — mirrored 1:1 from upstream abap2UI5), and +// assemble-cap.js overlays it into app/z2ui5/webapp so the tree is a real +// CAP app folder: `cds watch` serves it, the approuter routes to it, and +// `ui5 build` in an MTA pipeline finds it where every UI5 toolchain looks. +// A symlink would not survive a Windows checkout or an mbt archive, and +// serving core/ directly would put deployment paths (xs-app.json, ui5.yaml) +// inside a vendored package nobody may edit. So: a copy, made by the +// assemble step, never by hand. +// +// WHY THE GATE EXISTS +// ------------------- +// Nothing checked the copy. The overlay is only correct as long as every +// publish reruns the assemble; an overlay edited in place (the obvious place +// to "fix the frontend") or a stale overlay after a partial run is byte-drift +// between what the browser gets (app/) and what the framework serves its +// embedded assets from (core/) — the split-brain that is invisible until a +// user reports it. This test makes the drift a failing suite instead: it +// runs in the assembled app (the publish gate) and again in the published +// repository's own CI, so both sides prove the two trees are byte-identical. +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); + +// The vendored core is wherever package.json says it is - "file:./core" in +// the assembled/published app, the builder's input mirror here in src (the +// path is deliberately not spelled out: assemble validates that no output +// file references the builder-side location). +const spec = require(path.join(ROOT, "package.json")).dependencies.abap2UI5; +const CORE = path.resolve(ROOT, spec.replace(/^file:/, "")); + +const OVERLAY = path.join(ROOT, "app", "z2ui5", "webapp"); +const SOURCE = path.join(CORE, "app", "z2ui5", "webapp"); + +const walk = (dir, base = dir) => { + const out = []; + for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const p = path.join(dir, e.name); + if (e.isDirectory()) out.push(...walk(p, base)); + else out.push(path.relative(base, p)); + } + return out; +}; + +// In the builder checkout the overlay does not exist yet — assemble creates +// it. There the test proves only that the SOURCE side is present; the +// byte comparison runs where the overlay does exist: in the assembled app +// (npm test is the publish gate) and in the published repository. +const assembled = fs.existsSync(OVERLAY); + +describe("the webapp overlay", () => { + test("the vendored core carries the webapp the overlay is copied from", () => { + expect(fs.existsSync(SOURCE)).toBe(true); + expect(walk(SOURCE).length).toBeGreaterThan(0); + }); + + (assembled ? describe : describe.skip)("in the assembled app", () => { + test("carries exactly the files the core webapp has", () => { + expect(walk(OVERLAY)).toEqual(walk(SOURCE)); + }); + + test("is byte-identical to the core webapp", () => { + for (const rel of walk(SOURCE)) { + const a = fs.readFileSync(path.join(OVERLAY, rel)); + const b = fs.readFileSync(path.join(SOURCE, rel)); + // name the file, not just "buffers differ" + expect(a.equals(b) ? rel : `${rel} differs`).toBe(rel); + } + }); + }); +}); diff --git a/test/ecosystem-table.test.js b/test/ecosystem-table.test.js new file mode 100644 index 0000000..8229762 --- /dev/null +++ b/test/ecosystem-table.test.js @@ -0,0 +1,46 @@ +// AGENTS.md and README.md each open with the ecosystem table - the same six +// repositories, worded for different readers (roles and generated-ness for +// an agent, what-it-is and how-to-run for a human). Two tables of one set is +// fine; two tables of two DIFFERENT sets is how a renamed or added +// repository ends up documented in one file and missing from the other, and +// nothing noticed. This holds the two sets equal; the wording stays free. +// +// The same test file exists in builder-cap2UI5 - it reads only the repo's +// own two files, so the copies carry nothing repository-specific. +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); + +/** The OTHER cap2UI5-org repositories a file's ecosystem table links. The + * row for the repository itself is dropped (README links it, AGENTS bolds + * it without a link - a wording convention, not a disagreement), so each + * file yields the same thing: everyone else. */ +const SELF = require(path.join(ROOT, "package.json")).name.toLowerCase(); + +function tableRepoSet(file) { + const lines = fs.readFileSync(path.join(ROOT, file), "utf8").split("\n"); + const start = lines.findIndex((l) => /^\| Repo/i.test(l)); + if (start === -1) throw new Error(`${file}: no ecosystem table (header row "| Repo...")`); + const set = new Set(); + for (let i = start; i < lines.length && lines[i].startsWith("|"); i++) { + for (const m of lines[i].matchAll(/github\.com\/cap2UI5\/([\w-]+)/g)) { + if (m[1].toLowerCase() !== SELF) set.add(m[1]); + } + } + return set; +} + +describe("the ecosystem table", () => { + const agents = tableRepoSet("AGENTS.md"); + const readme = tableRepoSet("README.md"); + + test("names a real set in both files", () => { + expect(agents.size).toBeGreaterThanOrEqual(4); + expect(readme.size).toBeGreaterThanOrEqual(4); + }); + + test("AGENTS.md and README.md agree on which repositories exist", () => { + expect([...agents].sort()).toEqual([...readme].sort()); + }); +});