From 21c1f7f569ea9fc85517cfa8180bdecffe9dff69 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 06:53:22 +0000 Subject: [PATCH 1/4] deps: move the render gate onto the 0.2 line Same pairing as abap2UI5/samples: render-runtime 0.1.1 sat next to linter 0.2.1 because the published peer range refused the matching pair, not because 0.1 was wanted. Linter 0.2.2 widened the range, so both floors move to ^0.2.2, and @abaplint/cli joins the ^2.120.23 the corpus declares. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fv6JYeoMYTAfwEaLupTgz5 --- package-lock.json | 56 ++++++++++++++++++++++++++++++++++++++--------- package.json | 6 ++--- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1cc7783..25d2f44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@abap2ui5/linter": "^0.2.1", - "@abap2ui5/render-runtime": "^0.1.1", - "@abaplint/cli": "^2.119.66" + "@abap2ui5/linter": "^0.2.2", + "@abap2ui5/render-runtime": "^0.2.2", + "@abaplint/cli": "^2.120.23" }, "engines": { "node": ">=22" } }, "node_modules/@abap2ui5/linter": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@abap2ui5/linter/-/linter-0.2.1.tgz", - "integrity": "sha512-48uMFctR78nogNai1w6kE7Ho4IXLuEnGPq5BJkstpWL69ToL1G+OJ1t4fDOBmOcJ36uFejDn2ofnoBqEkt+9sA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@abap2ui5/linter/-/linter-0.2.2.tgz", + "integrity": "sha512-WegxIRN5M5gH13Ye5Mo13fbpjAZ183J0ei1iArCfEiKUPAGqHot20NnckpNnXHX+YPHCkehA+Z0QapXjaLK1Tw==", "dev": true, "license": "MIT", "workspaces": [ @@ -33,7 +33,7 @@ "node": ">=22" }, "peerDependencies": { - "@abap2ui5/render-runtime": "^0.1.0" + "@abap2ui5/render-runtime": "^0.1.0 || ^0.2.0" }, "peerDependenciesMeta": { "@abap2ui5/render-runtime": { @@ -42,9 +42,9 @@ } }, "node_modules/@abap2ui5/render-runtime": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@abap2ui5/render-runtime/-/render-runtime-0.1.1.tgz", - "integrity": "sha512-41O4amDSbHWynzXiTjWYp1XC+dVbWc58flTkTTCJUjXwYXW6mJBmMiLxBSpCz1pMXa4LL3QAZq8IXk7Yy2664g==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@abap2ui5/render-runtime/-/render-runtime-0.2.2.tgz", + "integrity": "sha512-6DTHLxaIUoFb1I7qPLqjFFBtLNbgOE7cFpAlWKRBLzM/x+3mmm6JG5uIAGJaS3AFmFMV/J9coAxCXez1hqL7Tw==", "dev": true, "license": "MIT", "dependencies": { @@ -59,6 +59,7 @@ "@openui5/sap.ui.unified": "1.151.0", "@openui5/sap.uxap": "1.151.0", "@openui5/themelib_sap_horizon": "1.151.0", + "less-openui5": "^0.12.0", "playwright": "^1.61.1" }, "engines": { @@ -81,6 +82,13 @@ "url": "https://github.com/sponsors/larshp" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@openui5/sap.f": { "version": "1.151.0", "resolved": "https://registry.npmjs.org/@openui5/sap.f/-/sap.f-1.151.0.tgz", @@ -213,6 +221,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/less-openui5": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/less-openui5/-/less-openui5-0.12.0.tgz", + "integrity": "sha512-7CvQYmuODBCWAh9RBKevdx2+aNIjVPaWpS/XxPfxe8hJNvOY5IcXdTchFahwhVmILzPMOqZDKykGQr5GMq6iJg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@adobe/css-tools": "^4.5.0", + "mime": "^1.6.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/playwright": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", diff --git a/package.json b/package.json index b6579cf..ff042bc 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ }, "homepage": "https://github.com/abap2UI5/samples-stack#readme", "devDependencies": { - "@abap2ui5/linter": "^0.2.1", - "@abap2ui5/render-runtime": "^0.1.1", - "@abaplint/cli": "^2.119.66" + "@abap2ui5/linter": "^0.2.2", + "@abap2ui5/render-runtime": "^0.2.2", + "@abaplint/cli": "^2.120.23" }, "engines": { "node": ">=22" From 6f64873cc501405358d8eb21d3c2a3733ec9a72b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:05:54 +0000 Subject: [PATCH 2/4] lint against the release, not against main Same unpinned git dependency as abap2UI5/samples: abaplint cloned the default branch, so the corpus was checked against the framework's development tip while its readers install a release. abaplint.jsonc now pins 1.143.0. Verified: abaplint reports 0 issues over 158 files at the pin. check:pin holds the pin to one release and runs before any install; bump-framework-pin moves it weekly and lints at the new release before opening the pull request. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fv6JYeoMYTAfwEaLupTgz5 --- .github/workflows/bump-framework-pin.yaml | 96 +++++++++++ .github/workflows/check-framework-pin.yaml | 45 +++++ abaplint.jsonc | 1 + package.json | 3 +- scripts/check-framework-pin.mjs | 187 +++++++++++++++++++++ 5 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/bump-framework-pin.yaml create mode 100644 .github/workflows/check-framework-pin.yaml create mode 100644 scripts/check-framework-pin.mjs diff --git a/.github/workflows/bump-framework-pin.yaml b/.github/workflows/bump-framework-pin.yaml new file mode 100644 index 0000000..86e0355 --- /dev/null +++ b/.github/workflows/bump-framework-pin.yaml @@ -0,0 +1,96 @@ +name: bump-framework-pin + +# The abaplint configs here pin abap2UI5 to a RELEASE tag, which is what makes +# this repository lint against the framework its readers actually installed. +# A pin nobody moves is a different defect from a pin nobody set, though: the +# samples would keep proving themselves against an ever-older release while the +# framework moves on, and the first person to notice would be a reader whose +# new install behaves differently from the corpus. +# +# So: weekly, resolve the newest release tag, move every config that carries +# one (check-framework-pin --set knows which those are - the 702 config keeps +# its branch), and PROVE the new pin by running the abaplint gates before the +# pull request exists. A framework release that breaks a sample fails this +# workflow instead of landing on main. +# +# A release is published twice - `X.Y.Z` and, minutes later, `X.Y.Z-702` for +# the downported distribution. Both are the same version of the framework and +# the tag list is sorted by version, so the -702 suffix is stripped and the +# highest remaining x.y.z wins. + +on: + schedule: + - cron: '17 5 * * 2' # Tuesdays 05:17 UTC — clear of the Monday linter bumps + workflow_dispatch: + +permissions: + contents: read + +jobs: + bump-framework-pin: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + + - name: Resolve the newest abap2UI5 release and move the pin + id: pin + run: | + set -euo pipefail + # --exit-code: `git ls-remote` prints nothing and still exits 0 when no + # ref matches, so a repository that stopped tagging releases would + # resolve to an EMPTY version and this step would happily write it. + TAG=$(git ls-remote --exit-code --tags --refs https://github.com/abap2UI5/abap2UI5 \ + | sed 's#.*refs/tags/##' \ + | sed 's/-702$//' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V | tail -1) + test -n "$TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + BEFORE=$(git diff --quiet && echo clean || echo dirty) + node scripts/check-framework-pin.mjs --set "$TAG" + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "already on $TAG ($BEFORE before)" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - if: steps.pin.outputs.changed == 'true' + run: node scripts/check-framework-pin.mjs + + # The proof. A release that dropped or changed API a sample uses fails + # HERE, where the diff is one line and the cause is obvious, instead of on + # somebody's unrelated pull request. + - if: steps.pin.outputs.changed == 'true' + run: npm ci + - if: steps.pin.outputs.changed == 'true' + name: Lint the corpus against the new release + run: | + set -euo pipefail + npx abaplint abaplint.jsonc + + - if: steps.pin.outputs.changed == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: bump-framework-pin + title: 'chore: lint against abap2UI5 ${{ steps.pin.outputs.tag }}' + commit-message: 'chore: pin the abaplint configs to abap2UI5 ${{ steps.pin.outputs.tag }}' + body: | + Weekly move of the framework pin in the abaplint configs to the newest + published release, `${{ steps.pin.outputs.tag }}`. + + The abaplint gates ran against the new release in the workflow that + opened this pull request, so a red check here means the release + changed something a sample depends on — worth reading before merging, + because a reader on that release hits the same thing. + add-paths: | + abaplint.jsonc + .github/abaplint + delete-branch: true diff --git a/.github/workflows/check-framework-pin.yaml b/.github/workflows/check-framework-pin.yaml new file mode 100644 index 0000000..18843ee --- /dev/null +++ b/.github/workflows/check-framework-pin.yaml @@ -0,0 +1,45 @@ +name: check-framework-pin + +# The abaplint configs here resolve abap2UI5 as a git dependency, and abaplint +# clones the DEFAULT branch unless a `"branch"` key says otherwise. So these +# samples used to be linted against the framework's development tip while every +# reader of them has a RELEASE installed - a gap that hides two defects, both +# silent: +# +# a sample uses API that exists only on main, this repository stays green, +# and the reader who copies it gets a syntax error. That is not theoretical: +# z2ui5_cl_ui5_view_builder was on main from 2026-08-12 and in no release +# until 1.143.0, three weeks later. +# +# the framework changes something on main and the abaplint workflows here go +# red overnight, with no commit in this repository to explain it. +# +# The configs now name a release tag, and this gate is what keeps them naming +# ONE - a bump has to move every config, and a forgotten file cannot quietly +# lint against a different framework than its neighbours. +# +# Plain node, no dependencies, so it stays a few seconds and can run before any +# install - which is also why `npm run check` puts it first. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: check-framework-pin-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-framework-pin: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + - run: node scripts/check-framework-pin.mjs diff --git a/abaplint.jsonc b/abaplint.jsonc index 2afe14b..5f5053f 100644 --- a/abaplint.jsonc +++ b/abaplint.jsonc @@ -25,6 +25,7 @@ }, { "url": "https://github.com/abap2UI5/abap2UI5", + "branch": "1.143.0", "folder": "/abap2UI5", "files": "/src/**/*.*" } diff --git a/package.json b/package.json index ff042bc..1d74f88 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "check:abap2ui5": "abap2ui5lint", "fmt:chains": "abap2ui5lint --fix", "check:overview": "node scripts/check-overview.mjs", - "check": "npm run lint && npm run check:abap2ui5 && npm run check:overview && npm run check:keywords && npm run check:abapdoc && npm run check:samples-md && npm run check:app-rules && npm run check:prose && npm run check:web && npm run check:family-nav", + "check": "npm run check:pin && npm run lint && npm run check:abap2ui5 && npm run check:overview && npm run check:keywords && npm run check:abapdoc && npm run check:samples-md && npm run check:app-rules && npm run check:prose && npm run check:web && npm run check:family-nav", "check:keywords": "node scripts/check-keywords.mjs", "check:abapdoc": "node scripts/check-abapdoc.mjs", "samples:md": "node scripts/generate-samples-md.mjs", "check:samples-md": "node scripts/generate-samples-md.mjs --check", "check:app-rules": "node scripts/check-app-rules.mjs", + "check:pin": "node scripts/check-framework-pin.mjs", "check:prose": "node scripts/check-prose-names.mjs", "check:family-nav": "node scripts/check-family-nav.mjs", "web:index": "node scripts/generate-web-index.mjs", diff --git a/scripts/check-framework-pin.mjs b/scripts/check-framework-pin.mjs new file mode 100644 index 0000000..b9731d5 --- /dev/null +++ b/scripts/check-framework-pin.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node +/* + * check-framework-pin — the sample repositories compile against a RELEASE of + * abap2UI5, and this is what says so. + * + * ONE SOURCE, and it is abap2UI5's `.github/shared/check-framework-pin.mjs`. + * The repositories that run it carry it byte-equal; abap2UI5's + * `npm run check:shared` is what notices when that did not happen. + * + * WHY: abaplint resolves a git dependency by URL, and without a `"branch"` key + * it clones the DEFAULT branch. So every one of these configs was linting the + * corpus against abap2UI5 main — the development tip — while every reader of + * these samples has a release installed. Two failure directions, both silent: + * + * a sample uses API that only exists on main, CI is green, and the reader + * who copies it gets a syntax error. z2ui5_cl_ui5_view_builder is the case + * that happened: on main from 2026-08-12, not in a release until 1.143.0 + * three weeks later. + * + * the framework changes something on main, and nine workflows across three + * repositories go red overnight without a single commit in any of them. + * + * A release tag fixes both: the gate answers the question a reader actually + * has. abaplint's `"branch"` is the only key it offers for this, and it feeds + * `git clone --branch`, which takes a tag just as happily as a branch name. + * (A `"tag"` key would be silently ignored — abaplint does not have one.) + * + * Policy: + * 1. Every abaplint config's abap2UI5 dependency carries a `"branch"` key. + * No key means the default branch, which is the drift this exists for. + * 2. Never two `"branch"` keys in one dependency entry. JSON parsing takes + * the LAST of duplicate keys, so a stale pin left NEXT to the intended + * one is invisible to every consumer that just parses the file. + * 3. All configs name the SAME release, so a bump moves them together and + * one forgotten file cannot lint against a different framework than its + * neighbours. ALLOWED_BRANCHES carries the exceptions. + * + * The 702 exception: the downported build must resolve the framework against + * its downported branch. abap2UI5 force-pushes `702` from main on every push + * (auto_downport), so it is not a release pin — it is the only branch whose + * content is v702-parseable at all, and main is not. + * + * Run: node scripts/check-framework-pin.mjs (offline, exit 1) + * node scripts/check-framework-pin.mjs --set 1.144.0 (move the pin) + * + * `--set` is here rather than in the bump workflow so the policy has ONE + * implementation: which configs carry the release, which carry an allowlisted + * branch, and what a release tag looks like are the same three answers whether + * they are being checked or written. A workflow doing it with sed would be a + * second, silent copy of that policy. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +// config path (repo-relative) -> the branch value its abap2UI5 dependency must +// carry INSTEAD of the release tag. Absent = must carry the release tag. +const ALLOWED_BRANCHES = new Map([ + ['.github/abaplint/abap_702.jsonc', '702'], +]); + +const A2UI5_URL_RE = /github\.com\/abap2UI5\/abap2UI5/i; +const RELEASE_RE = /^\d+\.\d+\.\d+$/; + +let errors = 0; +const err = (m) => { console.log(`ERROR ${m}`); errors++; }; + +/* Comment-stripping that leaves string contents alone — the configs are + * .jsonc and a `//` inside a URL must survive. */ +const stripComments = (s) => s + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/([,{[\s])\/\/.*$/gm, '$1'); + +// balanced top-level {...} slices of an array body (strings are quote-aware) +function objectSlices(body) { + const out = []; + let depth = 0; + let start = -1; + let inStr = false; + for (let i = 0; i < body.length; i++) { + const c = body[i]; + if (inStr) { + if (c === '\\') i++; + else if (c === '"') inStr = false; + continue; + } + if (c === '"') { inStr = true; continue; } + if (c === '{') { if (depth === 0) start = i; depth++; } + else if (c === '}') { depth--; if (depth === 0 && start >= 0) { out.push(body.slice(start, i + 1)); start = -1; } } + } + return out; +} + +function configFiles() { + const files = ['abaplint.jsonc']; + const dir = path.join(ROOT, '.github', 'abaplint'); + if (fs.existsSync(dir)) { + for (const f of fs.readdirSync(dir).sort()) { + if (f.endsWith('.jsonc') || f.endsWith('.json')) files.push(path.posix.join('.github/abaplint', f)); + } + } + return files.filter((f) => fs.existsSync(path.join(ROOT, f))); +} + +const SET = (() => { + const i = process.argv.indexOf('--set'); + return i !== -1 ? process.argv[i + 1] : null; +})(); + +if (SET !== null) { + if (!RELEASE_RE.test(SET || '')) { + console.log(`ERROR --set wants a release tag (x.y.z), got ${JSON.stringify(SET)}`); + process.exit(1); + } + let written = 0; + for (const rel of configFiles()) { + if (ALLOWED_BRANCHES.has(rel)) continue; + const full = path.join(ROOT, rel); + const raw = fs.readFileSync(full, 'utf8'); + /* Rewrite in the RAW text, not a parsed tree: these are .jsonc files whose + * comments carry the reasoning, and a parse/serialise round trip would + * drop every one of them. Anchored on the abap2UI5 url line so a `branch` + * belonging to another dependency is left alone. */ + const next = raw.replace( + /("url"\s*:\s*"https:\/\/github\.com\/abap2UI5\/abap2UI5"\s*,\s*\n\s*"branch"\s*:\s*")[^"]*(")/, + `$1${SET}$2`, + ); + if (next !== raw) { fs.writeFileSync(full, next); written++; } + } + console.log(`check-framework-pin: set ${written} config(s) to ${SET}`); +} + +const releases = new Map(); // config -> release tag it names +let checked = 0; + +for (const rel of configFiles()) { + const txt = stripComments(fs.readFileSync(path.join(ROOT, rel), 'utf8')); + const depsM = txt.match(/"dependencies"\s*:\s*\[([\s\S]*?)\]/); + if (!depsM) continue; + for (const entry of objectSlices(depsM[1])) { + if (!A2UI5_URL_RE.test(entry)) continue; + checked++; + const branches = [...entry.matchAll(/"branch"\s*:\s*"([^"]*)"/g)].map((m) => m[1]); + const expected = ALLOWED_BRANCHES.get(rel); + + if (branches.length > 1) { + err(`${rel}: abap2UI5 dependency carries ${branches.length} "branch" keys (${branches.map((b) => JSON.stringify(b)).join(', ')}) — duplicate keys silently shadow each other; keep exactly one`); + continue; + } + if (branches.length === 0) { + err(`${rel}: abap2UI5 dependency carries no "branch" key — abaplint then clones the DEFAULT branch, so this config lints against the framework's development tip instead of a release${expected ? `; this config wants "branch": ${JSON.stringify(expected)}` : ''}`); + continue; + } + + const [branch] = branches; + if (expected !== undefined) { + if (branch !== expected) { + err(`${rel}: abap2UI5 dependency carries "branch": ${JSON.stringify(branch)}, but this config is allowlisted for ${JSON.stringify(expected)} — change ALLOWED_BRANCHES in the same commit if that is deliberate`); + } + continue; + } + if (!RELEASE_RE.test(branch)) { + err(`${rel}: abap2UI5 dependency is pinned to ${JSON.stringify(branch)}, which is not a release tag (x.y.z) — only the allowlisted configs may name a branch`); + continue; + } + releases.set(rel, branch); + } +} + +if (!checked) { + err('no abap2UI5 dependency entry found in any abaplint config — did the dependency URL change? (this check would go blind)'); +} + +// --- 3. one release across the repository ----------------------------------- +const distinct = [...new Set(releases.values())]; +if (distinct.length > 1) { + err(`the configs name ${distinct.length} different releases (${distinct.join(', ')}) — a bump has to move all of them:\n` + + [...releases].map(([f, v]) => ` ${v} ${f}`).join('\n')); +} + +if (errors) { + console.log(`check-framework-pin: ${errors} error(s).`); + process.exit(1); +} +console.log(`check-framework-pin: ok (${checked} abap2UI5 dependency entr${checked === 1 ? 'y' : 'ies'}, release ${distinct[0] ?? 'none'}${ALLOWED_BRANCHES.size ? `, ${ALLOWED_BRANCHES.size} allowlisted branch pin(s)` : ''})`); From 497aadd986a7c19200238457ab7a834ad27a8ffa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 07:21:13 +0000 Subject: [PATCH 3/4] chore: pull the shared files from abap2UI5 The view-chain-layout skill now describes what all four repositories actually do, and sync-shared is the workflow that will keep these copies current. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fv6JYeoMYTAfwEaLupTgz5 --- .claude/skills/view-chain-layout/SKILL.md | 35 ++++--- .github/workflows/sync-shared.yaml | 110 ++++++++++++++++++++++ 2 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/sync-shared.yaml diff --git a/.claude/skills/view-chain-layout/SKILL.md b/.claude/skills/view-chain-layout/SKILL.md index 64129ab..15a07ff 100644 --- a/.claude/skills/view-chain-layout/SKILL.md +++ b/.claude/skills/view-chain-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: view-chain-layout -description: The layout rules for a z2ui5_cl_ui5_view_builder chain - one call per line, four spaces per tree level, the end( ) column, which factory( ) shape goes with which chain shape, blank lines, and the chain-format gate that checks them. Identical in abap2UI5, abap2UI5/samples and abap2UI5/samples-controls. Use when writing, reviewing or reformatting any view built with the builder, when a chain has drifted, and when chain-format, chain-indentation or chain-element-per-line fails. +description: The layout rules for a z2ui5_cl_ui5_view_builder chain - one call per line, four spaces per tree level, the end( ) column, which factory( ) shape goes with which chain shape, blank lines, and the linter rule that checks them. Identical in abap2UI5, abap2UI5/samples, abap2UI5/samples-controls and abap2UI5/samples-stack. Use when writing, reviewing or reformatting any view built with the builder, when a chain has drifted, and when chain-house-layout fails. --- # The layout of a view-builder chain @@ -122,23 +122,28 @@ not emitted at all until a config asks for it, because it encodes one house style — this one — and because its fixes span a whole chain, which would defer any other rule's fix inside the same chain to a second `--fix` pass. -How to run it depends on the repository, and they are not yet the same: +All four repositories run the rule itself. `scripts/chain-format.mjs`, which +used to be the same algorithm written a second time, is gone: | | how | |---|---| -| `abap2UI5` | `npm run check:abap2ui5` / `npm run fmt:chains` — the linter, enabled in `abap2ui5lint.jsonc` | -| `abap2UI5/samples` | `npm run check:chains` / `npm run fmt:chains` — still `scripts/chain-format.mjs` | -| `abap2UI5/samples-controls` | `npm run check:chains` / `npm run fmt:chains` — the same script; also the first step of `npm run gates` | - -`scripts/chain-format.mjs` is the same algorithm as the rule, kept -byte-identical in the two sample repositories. **It is meant to go away**: both -repos pin the linter by SHA at a version that predates the rule, and bumping -that pin also moves them onto the linter's new packaging, where the UI5 render -runtime is a separate optional peer (`@abap2ui5/render-runtime`) rather than an -optional dependency. Until that package resolves for them, bumping would take -their render gate — 172 and 416 documents — down with it. So the script stays -until the pin can move, and then it is deleted in the same change that adds -`"chain-house-layout": "warning"` to their configs. +| `abap2UI5` | `npm run check:abap2ui5` / `npm run fmt:chains` — enabled in `abap2ui5lint.jsonc` | +| `abap2UI5/samples` | `npm run check:abap2ui5` / `npm run fmt:chains` — enabled in `abap2ui5lint.jsonc` | +| `abap2UI5/samples-stack` | `npm run check:abap2ui5` / `npm run fmt:chains` — enabled in `abap2ui5lint.jsonc` | +| `abap2UI5/samples-controls` | `npm run check:chains` / `npm run fmt:chains` — `abap2ui5lint-chains.jsonc`; also the first step of `npm run gates` | + +samples-controls needs a config of its own because its corpus gate +(`view-gates.mjs`) only sees files that have a meta sidecar, while the layout +is a property of the source and has to cover the whole tree — including the +fourteen hand-written `src/03` classes and the generated overview app. That +config switches the property-gate rules off, because view-gates judges those +against the sidecars and a second opinion here would only be a worse one. + +Two things that config must NOT do, both measured rather than assumed: +`properties: false` looks like the obvious way to isolate the layout and takes +the chain rules down with it — the check then passes everything, silently. And +`chain-house-layout` is opt-in, so a `rules` entry is what turns it on; without +one the run is green no matter how mangled the chain. Nothing else covers this, which is why the rule exists: diff --git a/.github/workflows/sync-shared.yaml b/.github/workflows/sync-shared.yaml new file mode 100644 index 0000000..92a83f1 --- /dev/null +++ b/.github/workflows/sync-shared.yaml @@ -0,0 +1,110 @@ +name: sync-shared + +# The files this repository carries a COPY of, pulled from the repository that +# owns them. +# +# abap2UI5 declares itself the source of everything that exists byte-identical +# in several repositories here - the view-chain-layout skill, the shared gate +# scripts - and its `check:shared` notices when a copy stops matching. Noticing +# was as far as it went: the fix lived in another repository, so a change to a +# shared script meant somebody remembering to carry it into three others by +# hand, and the gate went red in the meantime in repositories that had done +# nothing wrong. The copies had already drifted that way once - chain-format +# differed between two of them while a skill in a third still called them +# byte-identical. +# +# So the copies are pulled rather than pushed. No cross-repository credentials +# exist anywhere in this: the repository updates its own files with its own +# token, which is the same shape as every other bump workflow in this +# organisation. The manifest comes from the source repository, so which files +# are shared is still decided in exactly one place. +# +# Section-shared content is deliberately NOT synced: the app rule block inside +# this repository's own abaplint.jsonc, the AGENTS.md sections. Those live +# inside files this repository owns the rest of, and copying a source over them +# would destroy what it owns. `check:shared` still reports them. + +on: + schedule: + - cron: '47 5 * * 2' # Tuesdays 05:47 UTC, after the framework pin bump + workflow_dispatch: + +permissions: + contents: read + +jobs: + sync: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check out the source repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: abap2UI5/abap2UI5 + ref: main + path: .abap2UI5 + sparse-checkout: | + .claude + .github + docs + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + + - name: Copy every shared file this repository carries + id: sync + run: | + set -euo pipefail + GATE=.abap2UI5/.github/scripts/shared-file-gate.mjs + test -f "$GATE" || { + echo "::error::$GATE is missing — the source repository moved the gate." + echo "::error::Fix the path here; a silent skip would let the copies drift again." + exit 1 + } + # Tab-separated: source path in abap2UI5, path in this repository. + node "$GATE" --manifest "${{ github.event.repository.name }}" > manifest.txt + test -s manifest.txt || { + echo "::error::the manifest is empty for ${{ github.event.repository.name }} —" + echo "::error::either this repository is no longer a consumer, or its name changed." + exit 1 + } + while IFS=$'\t' read -r src dst; do + [ -n "$src" ] || continue + test -f ".abap2UI5/$src" || { echo "::error::.abap2UI5/$src does not exist"; exit 1; } + mkdir -p "$(dirname "$dst")" + cp ".abap2UI5/$src" "$dst" + echo " $src -> $dst" + done < manifest.txt + rm -f manifest.txt + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "every copy already matches its source" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + git diff --stat + fi + + - if: steps.sync.outputs.changed == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: sync-shared + title: 'chore: pull the shared files from abap2UI5' + commit-message: 'chore: sync the shared files from abap2UI5/abap2UI5' + body: | + A file this repository carries a copy of moved in + [abap2UI5/abap2UI5](https://github.com/abap2UI5/abap2UI5), which owns + it. This pull request carries the change across. + + The copies are byte-equal by design and `check:shared` in the source + repository is what notices when they stop being — so this is the fix + for a gate that would otherwise be red there while nothing was wrong + here. Read the diff as you would any other change to these files: if + it looks wrong, the argument belongs in the source repository, not in + a local edit that will be pulled over next week. + delete-branch: true From 6cc810994e59f9405d489d792b9057d0fb4f806c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 09:10:40 +0000 Subject: [PATCH 4/4] sync-shared: read the consumer name from GITHUB_REPOSITORY The workflow asked `github.event.repository.name` for the repository it is running in, and its only triggers are `schedule` and `workflow_dispatch`. A scheduled run carries no repository object, so the name would have come out empty every Tuesday - the manifest call would have produced nothing and the guard right below it would have failed the run. Loud rather than silent, which is why the guard is there, but the feature would never have worked once. GITHUB_REPOSITORY is always set. Verified by running the whole loop by hand against abap2UI5/samples: five shared files resolved and copied, and the only resulting diff is this workflow updating itself - which is the self-syncing behaviour the shared-file entry describes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fv6JYeoMYTAfwEaLupTgz5 --- .github/workflows/sync-shared.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-shared.yaml b/.github/workflows/sync-shared.yaml index 92a83f1..afe1bf8 100644 --- a/.github/workflows/sync-shared.yaml +++ b/.github/workflows/sync-shared.yaml @@ -67,10 +67,15 @@ jobs: echo "::error::Fix the path here; a silent skip would let the copies drift again." exit 1 } + # The consumer name is this repository's own, taken from + # GITHUB_REPOSITORY rather than from the event payload: this workflow + # only ever runs on `schedule` and `workflow_dispatch`, and a scheduled + # run carries no repository object to read a name off. + NAME="${GITHUB_REPOSITORY#*/}" # Tab-separated: source path in abap2UI5, path in this repository. - node "$GATE" --manifest "${{ github.event.repository.name }}" > manifest.txt + node "$GATE" --manifest "$NAME" > manifest.txt test -s manifest.txt || { - echo "::error::the manifest is empty for ${{ github.event.repository.name }} —" + echo "::error::the manifest is empty for $NAME —" echo "::error::either this repository is no longer a consumer, or its name changed." exit 1 }