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
16 changes: 16 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/update_cap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ concurrency:
jobs:
run:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
5 changes: 5 additions & 0 deletions scripts/mirror-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
16 changes: 16 additions & 0 deletions src/.gitattributes
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions src/test/webapp-overlay.test.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
46 changes: 46 additions & 0 deletions test/ecosystem-table.test.js
Original file line number Diff line number Diff line change
@@ -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());
});
});