feat(glline): declare fiscalYearId, and backfill it from the parent t… #2033
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Code Quality | |
| on: | |
| push: | |
| # An ALLOW-LIST of branch prefixes is a gate with a hole in it, and the | |
| # hole is SILENT: a branch matching nothing gets no CI at all, and its last | |
| # visible status is whatever it inherited — indistinguishable, on every | |
| # dashboard, from a branch that passed. | |
| # | |
| # Two live examples, both found 2026-08-14: `perf/**` was uncovered in | |
| # openconnector, where a merge carrying unresolved conflict markers and 84 | |
| # failing tests was pushed and nothing ran; and `feat/**` was uncovered in | |
| # openregister — note the list said `feature/**`, so every branch anyone | |
| # named `feat/...` had been running unchecked. | |
| # | |
| # Prefixes are added rather than replaced with `**` because this workflow is | |
| # expensive (PHPUnit matrix, Newman, Playwright). The fast structural checks | |
| # DO run on `**` — see merge-hygiene.yml, added in the same change. | |
| # | |
| # ⚠️ Adding prefixes is not the durable fix; the next invented one is | |
| # uncovered again. The durable fix is branch protection requiring a PR into | |
| # development, which the pull_request trigger below already gates correctly. | |
| branches: | |
| - main | |
| - development | |
| - feature/** | |
| - feat/** | |
| - bugfix/** | |
| - hotfix/** | |
| - perf/** | |
| - refactor/** | |
| - chore/** | |
| - fix/** | |
| pull_request: | |
| branches: [main, master, development, beta] | |
| workflow_dispatch: | |
| # Deduplicating a `push` run against the `pull_request` run for the SAME head | |
| # ref is the point of this block, and for a feature branch it is exactly right: | |
| # two runs of identical jobs, one of them wasted. | |
| # | |
| # It is wrong for `main` and `development`, because the push run there is NOT a | |
| # duplicate — it is the only carrier of the push-only jobs: "Coverage Baseline | |
| # Check" (`github.event_name == 'push'`), "SBOM" and "Features Extract". And | |
| # those two branches always have an open PR whose `head_ref` IS the branch | |
| # name: the standing "Release: merge development into beta" (#63 here). | |
| # `github.head_ref` on that PR run and `github.ref_name` on the push run both | |
| # render `development`, so both landed in the identical group | |
| # `quality-development`, and `cancel-in-progress` killed whichever started | |
| # first — always the push run, by a few seconds. | |
| # | |
| # Measured on this repo: 3 of the last 20 `development` push runs were | |
| # cancelled within ~80s of starting — 31045614488 (79s), 30880585906 (37s), | |
| # 30822103065 (51s). That duration is the discriminator: the shared workflow's | |
| # `timeout-minutes: 45` cancellation lands at 45m16s–45m28s, so these are | |
| # concurrency kills. | |
| # | |
| # On the surviving PR run "Coverage Baseline Check" reports `skipped`, which is | |
| # CORRECT for a pull_request event and renders exactly like a pass. So the gate | |
| # appears on both runs and executes on neither — a dead gate of the | |
| # permanently-pending shape. | |
| # | |
| # Suffixing only the default-branch push keeps feature-branch dedup untouched | |
| # (`quality-feature/x` for both events, exactly as before) and gives the two | |
| # default branches' push runs a lane of their own. | |
| # | |
| # Proven in openconnector#1158: its first-ever completed `development` push run | |
| # (31048998594) executed Coverage Baseline Check, SBOM and Features Extract. | |
| concurrency: | |
| # SUFFIXED BY EVENT NAME, not just by `-push`. | |
| # | |
| # The previous expression gave a push on `development` its own lane | |
| # (`-push`) but left EVERYTHING ELSE sharing `quality-development`. On this | |
| # repo that is not a quiet lane: `Sync to Beta` keeps a PR open whose | |
| # head_ref IS `development`, so its run computes `quality-development` too | |
| # and is re-triggered on every merge. | |
| # | |
| # A `workflow_dispatch` therefore landed in the same group as that PR and | |
| # was cancelled by it. Observed 2026-08-21: dispatch 32487948678 on | |
| # `development` cancelled by pull_request run 32490160836, head_branch | |
| # `development`, 25 minutes later. A run someone deliberately asked for | |
| # could essentially never complete on this repo. | |
| # | |
| # That matters beyond ad-hoc verification: the fleet gate-drift sweep | |
| # (.github#523) dispatches per app with `--ref development`, so under the | |
| # old group its runs would be cancelled here and report neither pass nor | |
| # fail — a routine that produces no verdict is indistinguishable from one | |
| # that never ran. | |
| # | |
| # This is hermiq's form, already in use there. Pull requests keep the bare | |
| # group (so a PR still supersedes its own earlier run); push, dispatch and | |
| # schedule each get their own lane. | |
| # | |
| # THE BRANCH RESTRICTION IS GONE, because it contradicted the sentence above. | |
| # | |
| # The suffix used to apply only when `ref_name` was `main` or `development`, | |
| # so on every OTHER branch push and pull_request computed the SAME group — | |
| # and `cancel-in-progress` made them kill each other. That became reachable | |
| # when the push allow-list widened on 2026-08-14 to include `feat/**`, | |
| # `fix/**`, `perf/**`, `refactor/**` and `chore/**`: those branches now get | |
| # both a push run and a pull_request run for one commit. | |
| # | |
| # `quality / Quality Report` is a `needs:`-gated aggregator and reports | |
| # FAILURE when its dependencies are CANCELLED, so the collision shows up as a | |
| # red gate on a PR that was never actually evaluated — and re-running collides | |
| # the same way. Measured on openregister#2821: a push run left queued and a | |
| # pull_request run cancelled, 18 seconds apart, on one commit. | |
| # | |
| # A branch name is not a unique lane when two event types can each produce a | |
| # run for it, so the event is now always part of the key. | |
| group: quality-${{ github.head_ref || github.ref_name }}${{ github.event_name != 'pull_request' && format('-{0}', github.event_name) || '' }} | |
| cancel-in-progress: true | |
| # Permission CEILING for the called quality pipeline. GitHub statically | |
| # validates the called workflow's declared job permissions against this | |
| # grant — even for jobs that are disabled — so it must cover the maximum | |
| # any nested job declares: journeydoc-capture (contents+actions write), | |
| # update-baseline / features-extract (contents write), and the Quality | |
| # Report PR comment (issues / pull-requests write). | |
| permissions: | |
| contents: write | |
| actions: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| quality: | |
| uses: ConductionNL/.github/.github/workflows/quality.yml@main | |
| with: | |
| app-name: shillinq | |
| php-version: "8.3" | |
| php-test-versions: '["8.3", "8.4"]' | |
| # stable31 is REMOVED, not "dropped for coverage". `additional-apps` below | |
| # installs openregister, which declares min-version="32" and therefore | |
| # cannot install on NC31 — `occ app:enable openregister` fails with only a | |
| # ::warning::, so the run continued WITHOUT its data layer and every | |
| # /apps/openregister/... call returned Nextcloud's HTML 404 page. A stable31 | |
| # leg was testing an impossible configuration, so its red said nothing. | |
| # | |
| # Order mattered as much as membership: newman, playwright and | |
| # journeydoc-capture all pin `fromJSON(inputs.nextcloud-test-refs)[0]`, so | |
| # stable31 sitting FIRST meant all three ran entirely on the version | |
| # openregister cannot load. | |
| # | |
| # THE LIST IS THE WHOLE DECLARED RANGE. appinfo/info.xml declares | |
| # <nextcloud min-version="32" max-version="34"/>, so 32, 33 and 34 each get | |
| # a leg. Adopting NC 34 by REPLACING the list left 32 and 33 advertised to | |
| # the App Store with no job touching them — the declared floor became the | |
| # untested end, which is the same drift as never testing 34, reversed. | |
| nextcloud-test-refs: '["stable34", "stable32", "stable33"]' | |
| enable-psalm: true | |
| enable-phpstan: true | |
| enable-phpmetrics: true | |
| enable-frontend: true | |
| enable-eslint: true | |
| enable-phpunit: true | |
| # ── E2E browser tests ──────────────────────────────────────────────── | |
| # `enable-playwright` / `playwright-test-path` are set ONCE, further down | |
| # next to `playwright-seed-command` — the three belong together. They | |
| # were briefly declared here as well; YAML allows no duplicate keys, and | |
| # GitHub rejected the whole file for it (see the note on that block). | |
| enable-newman: true | |
| # Shillinq delegates ALL object CRUD to OpenRegister (@conduction/nextcloud-vue | |
| # store factories hit /apps/openregister/api/objects), so a Nextcloud without | |
| # OpenRegister answers every integration request with a 404. That is why the | |
| # Newman leg failed 48 of its 51 assertions the first time this workflow was | |
| # able to resolve at all. NOTE: the example this replaced named the org | |
| # `Conduction`, which does not exist — the same wrong-org bug that kept the | |
| # whole workflow from resolving, hiding in a comment where a `uses:` grep | |
| # could not see it. | |
| # | |
| # `ref` MUST be `development`, not `main`. shillinq's appinfo/routes.php | |
| # is `return \OCA\OpenRegister\AppHost\Routes::standard(...)` — an | |
| # unguarded static call to a class that lives ONLY on OpenRegister | |
| # `development`. OpenRegister `main` is 0.2.19 and ships no `lib/AppHost` | |
| # at all, so Nextcloud's router fataled while *including* routes.php and | |
| # answered EVERY shillinq endpoint with 500 — "Class | |
| # \"OCA\\OpenRegister\\AppHost\\Routes\" not found", thrown from | |
| # appinfo/routes.php:39. That is the whole of the 45-of-51 Newman failure | |
| # in run 30896861325: not one of those endpoints was ever reached. | |
| # `development` is also what the rest of the fleet pins (opencatalogi, | |
| # openconnector, procest, softwarecatalog, scholiq, pipelinq). | |
| additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]' | |
| enable-sbom: true | |
| # ── E2E browser tests ──────────────────────────────────────────────── | |
| # `enable-playwright` was not present in this block at all, so it took | |
| # its `false` default and "E2E Tests (Playwright)" has reported `skipped` | |
| # on every run this repo has ever produced — rendered in the Quality | |
| # Report identically to a pass. Meanwhile the repo ships 53 *.spec.ts | |
| # files under tests/e2e/ and a root playwright.config.ts. None of it had | |
| # ever executed in CI. There was no recorded reason; it was simply never | |
| # wired. | |
| # | |
| # `playwright-test-path` does double duty in the shared workflow: | |
| # 1. it is the directory the "Validate Playwright tests exist" step | |
| # counts *.spec.ts in; | |
| # 2. it is the FIRST place the run step looks for a config — | |
| # `${playwright-test-path}/playwright.config.ts`, falling back to | |
| # the repo root only if that file is absent. | |
| # We ship tests/e2e/playwright.config.ts precisely so lookup (2) hits it. | |
| # The run step passes no `--project`, so the ROOT config would run all | |
| # three of its projects — including `visual` (pixel baselines whose own | |
| # header says a CI Linux runner cannot byte-match a dev-container | |
| # baseline) and `docs-capture` (screenshot re-shoots, which have their | |
| # own dedicated Journeydoc job). The tests/e2e config declares only the | |
| # regression project. Its report/output stay under `tests/e2e/`, which the | |
| # shared workflow's upload steps list explicitly | |
| # (`server/apps/<app>/tests/e2e/playwright-report/` and | |
| # `.../tests/e2e/test-results/`) alongside the app-root paths, so both are | |
| # downloadable from the run. | |
| # | |
| # `additional-apps` above already pins OpenRegister @ development, which | |
| # these specs need for the same reason the Newman leg does: shillinq | |
| # delegates all object CRUD to /apps/openregister/api/objects. | |
| # | |
| # ⚠️ SET EXACTLY ONCE — here, and nowhere else in this file. | |
| # | |
| # These two keys have now been lost twice in one day, in opposite | |
| # directions, by changes that could not see each other: | |
| # | |
| # 1. #564 and #440 each ADDED them to this block independently. YAML | |
| # forbids duplicate keys, so GitHub could not parse the file at all: | |
| # it created a run, marked it `failure`, and ran ZERO jobs. Fixed by | |
| # #856, which removed the copy further up. | |
| # 2. #853 had branched before that fix, hit the same duplicate, and | |
| # resolved it by removing THIS copy instead. Net result: zero | |
| # declarations, `enable-playwright` fell back to its `false` default, | |
| # and `E2E Tests (Playwright)` reported **skipped** on development. | |
| # | |
| # Case 2 is the more dangerous one. A workflow that will not parse is at | |
| # least loudly red; a skipped job renders in the Quality Report exactly | |
| # like a passing one, so the 53 specs silently stopped running again with | |
| # nothing on any dashboard to say so. | |
| # | |
| # `playwright-test-path` does double duty in the shared workflow: it is | |
| # both the spec directory AND the first place the run step looks for a | |
| # config (`${playwright-test-path}/playwright.config.ts`, falling back to | |
| # the repo root). We ship tests/e2e/playwright.config.ts precisely so that | |
| # lookup hits it — the run step passes no `--project`, so the ROOT config | |
| # would additionally run `visual` (pixel baselines a CI runner cannot | |
| # byte-match) and `docs-capture` (screenshot re-shoots with their own job). | |
| enable-playwright: true | |
| playwright-test-path: tests/e2e | |
| # Runs after `php -S` is up and before the first spec, with cwd set to the | |
| # Nextcloud server ROOT (hence the `apps/shillinq/` prefix). | |
| # | |
| # It is not optional decoration. Three things are broken on a freshly | |
| # installed CI instance and each of them presents as "the specs are just | |
| # failing", not as a setup fault: | |
| # | |
| # 1. NO REGISTER. `occ app:enable shillinq` runs an IRepairStep, which | |
| # has NO user session, so OpenRegister's RBAC denies the import | |
| # ("User 'Anonymous' does not have permission to 'create' objects"). | |
| # `InitializeSettings::run()` catches \Throwable and downgrades it to | |
| # a warning, so `app:enable` still exits 0 with no register. The seed | |
| # forces the import over `POST /apps/shillinq/api/settings/load` as | |
| # the admin, then VERIFIES the register + schema slugs are readable | |
| # rather than trusting the 200. | |
| # 2. NO PRETTY URLS. Without `htaccess.IgnoreFrontController=true` the | |
| # SPA's vue-router base is `/index.php/apps/shillinq` while the specs | |
| # navigate to `/apps/shillinq/…`; every deep link then falls through | |
| # `src/main.js`'s `/:pathMatch(.*)*` catch-all and silently lands on | |
| # the DASHBOARD. Nothing 404s, so the specs assert against the wrong | |
| # page and pass or fail for reasons unrelated to what they name. | |
| # 3. THE FIRST-TIME SETUP WIZARD. `src/manifest.json` declares | |
| # `setup.enabled: true`; on a virgin instance the SPA renders a | |
| # "Set up this app" dialog OVER every page, so no spec can reach | |
| # `main`. It appeared in 138 of the 166 error contexts of run | |
| # 30858234537. The specs' own `dismissWizard()` only closes | |
| # Nextcloud's `#firstrunwizard`, which is a DIFFERENT dialog. The seed | |
| # completes shillinq's wizard over its own admin API and gates on | |
| # `setup/status` reporting `completed: true`. | |
| # | |
| # ⚠️ The verification deliberately checks PascalCase schema slugs | |
| # (`Account`, `GLTransaction`, …). All 493 shillinq schema slugs are | |
| # PascalCase and OpenRegister matches on `LOWER(slug)`; kebab-casing them | |
| # would be wrong. | |
| playwright-seed-command: 'bash apps/shillinq/tests/e2e/ci-seed.sh' | |
| # ── Frontend Check legs ────────────────────────────────────────────── | |
| # `frontend-checks` defaults to `[]`, and an empty list means the shared | |
| # workflow emits NO "Frontend Check" job at all — so this repo's five | |
| # validators ran nowhere while the run still looked complete. All are | |
| # self-contained `node` scripts, which is what a leg has to be (each leg | |
| # is a fresh job with its own checkout + `npm ci`). | |
| # Measured on this tree before enabling: | |
| # check:manifest-budget PASSES (1090951B against a 1100000B budget — | |
| # 9049 bytes of headroom, which is itself worth | |
| # having a gate on) | |
| # test:l10n PASSES | |
| # check:markers FAILS — a bare/malformed x-schema-org marker in | |
| # lib/Settings/register.d/60-bookings-depth.json | |
| # (AppointmentSeries: "https://schema.org/Schedule") | |
| # check:registers FAILS — a schema fragment missing its | |
| # x-openregister-audit-trail declaration | |
| # check:manifest passed locally only through its no-Ajv | |
| # structural fallback ("Ajv not installed in | |
| # node_modules"); CI runs `npm ci` first, so CI is | |
| # the first place the real schema validation | |
| # happens. Its verdict there may differ. | |
| # `test:unit` is NOT listed: "Frontend Tests (unit)" already runs it. | |
| # check:seeds NEW. Ratchet on seed/schema integrity: every | |
| # object shipped as register seed data must | |
| # satisfy the `required` list of the schema it | |
| # names. OpenRegister's ImportHandler only WARNS | |
| # on a violation and `occ app:enable` still exits | |
| # 0, so a broken seed is invisible until an | |
| # endpoint returns an empty list or a 404 and | |
| # looks like an application bug. Measured at 89 | |
| # before this commit, 81 after; the baseline is | |
| # a hard ceiling that may only be lowered. | |
| # check:fragment-required NEW. Ratchet on the CAUSE that check:seeds | |
| # measures the damage of. ADR-037 merges register | |
| # fragments by schema key and CONCATENATES list | |
| # values, so two fragments declaring `required` | |
| # for one schema make the effective list the | |
| # UNION of both — and no payload of either model | |
| # can then satisfy it. Measured at 47 conflicting | |
| # schema keys on `development` (plus 16 that | |
| # declare the SAME set twice, which is malformed | |
| # per JSON Schema but still satisfiable and is | |
| # reported without failing). Compares as SETS, | |
| # not sequences: an order-sensitive compare | |
| # reports 49 here, and the 2 extra are schemas | |
| # declaring the same names in a different order, | |
| # which merge fine. The baseline is a hard | |
| # ceiling that may only be lowered. | |
| # check:manifest-shell NEW. `src/manifest.d.shell.json` is GENERATED | |
| # by `scripts/generate-manifest-shell.js` and yet | |
| # COMMITTED, and every local consumer regenerates | |
| # it first (`prebuild`, `predev`, `prewatch`, | |
| # `pretest:unit`), so the committed copy could | |
| # drift arbitrarily far from `src/manifest.d/*` | |
| # with NOTHING going red. The regeneration that | |
| # keeps the build correct is exactly what made | |
| # the drift invisible. Observed twice on | |
| # 2026-08-20: `feat/budget-known-costs` shipped a | |
| # shell with ZERO occurrences of | |
| # `BudgetLineDerivations`/`AnnualBudgets` while | |
| # both fragments were on the branch, and | |
| # `development` itself was stale by +77 lines — a | |
| # whole `Budgets` menu group plus 6 | |
| # `budget-core-schema` pages. It is load-bearing: | |
| # `src/main.js` builds the vue-router table AND | |
| # the sidebar from `manifestShell.fragments`, and | |
| # the lazy `require.context` only backfills each | |
| # page's `config` — it never adds a page or a | |
| # menu entry. The check runs the REAL generator | |
| # CLI in a throwaway sandbox (never the working | |
| # tree) and compares byte-for-byte; it FAILS | |
| # rather than passes if the regenerated document | |
| # is empty or covers fewer fragments than | |
| # `src/manifest.d/` holds, so a generator that | |
| # stopped seeing its subject cannot report | |
| # success. Measured on this branch: FAILS on the | |
| # inherited `development` shell, PASSES after | |
| # regenerating it (the +77 lines are in this | |
| # commit). | |
| # | |
| # `format` (prettier --check) is listed because the shared workflow has NO | |
| # prettier job of its own — `quality.yml` runs eslint and stylelint and | |
| # mentions prettier ZERO times. This repo already carries | |
| # `@nextcloud/prettier-config` and a `format` script, so without this leg | |
| # `npm run format` never runs outside a developer's editor and the tree | |
| # drifts straight back out of format between merges — the same inert- | |
| # formatter failure mode that made the old `.prettierrc` worth deleting. | |
| # Centralising the config never stopped drift; the gate does. | |
| # Measured on this tree before enabling: PASSES, 216 of 222 tracked | |
| # frontend files in scope (docs/ excluded via .prettierignore; build | |
| # output via .gitignore, which prettier 3 also reads). | |
| # `check:l10n-js` regenerates l10n/<locale>.js from the JSON catalogue and | |
| # fails when the committed file is stale. Nextcloud serves ONLY the JS half | |
| # to a browser — raw JSON out of an app directory is a 404 — so a catalogue | |
| # that exists only as JSON renders English in the entire UI while every | |
| # server-rendered string is translated, with no error anywhere. | |
| # `check:schema-l10n` is a RATCHET, not a gate. Every string inside a form | |
| # comes from the schema and is a key in THIS app's catalogue; an absent key | |
| # renders the English source inside an otherwise translated form, silently. | |
| # The fleet had 30,459 such strings, so this records the current count and | |
| # fails only when it GROWS — burning it down stays an ordinary PR. | |
| frontend-checks: '["check:manifest", "check:manifest-shell", "check:manifest-budget", "check:markers", "check:registers", "check:seeds", "check:fragment-required", "check:nav-reachability", "check:job-registration", "test:l10n", "format", "check:l10n-js", "check:schema-l10n"]' | |
| # ── Coverage ratchet ───────────────────────────────────────────────── | |
| # `enable-coverage-guard` defaults to FALSE, which is why both | |
| # "Coverage Baseline Protection" and "Coverage Baseline Check" have only | |
| # ever reported `skipped`. It needs two inputs this repo did not have, | |
| # both added in this commit: `scripts/coverage-guard.php` (byte-identical | |
| # to openregister's) and `.coverage-baseline` = 57.71, this repo's own | |
| # measured coverage (38189 of 66179 statements) read from clover.xml in | |
| # the `coverage-report` artifact of run 30912740193. | |
| enable-coverage-guard: true | |
| # ── Hydra mechanical gates ─────────────────────────────────────────── | |
| # `enable-hydra-gates` defaults to FALSE, so this tier had never executed | |
| # here — the job reported `skipped`, which the Quality Report renders | |
| # identically to a pass. | |
| # `enable-axe` deliberately NOT set: a vanilla Nextcloud 34 already carries | |
| # serious/critical violations from core's own UI. | |
| enable-hydra-gates: true | |
| # No `hydra-gates-ref` here on purpose. The shared workflow defaults it | |
| # to @main, and this workflow is itself consumed at @main, so the two | |
| # sides move together and a gate fix reaches this repo without a commit | |
| # in this repo. A pin is a silent expiry date: 22 repos sat on v1.0.1 and | |
| # 16 gates were dead fleet-wide while every one reported PASS (.github#159), | |
| # and a default flipped at @main later reached those old runners and made | |
| # them red on gates they had no subject matter for (.github#173) — this | |
| # repo felt both, the second one blocking #441. | |
| # To hold this repo still for a specific reason, set the input explicitly | |
| # and say why — it is still honoured. To roll back for everyone, revert on | |
| # ConductionNL/.github main. |