Skip to content

Release 1.20.1 → PROD - #2080

Merged
jung-thomas merged 39 commits into
mainfrom
DEV
Aug 28, 2026
Merged

Release 1.20.1 → PROD#2080
jung-thomas merged 39 commits into
mainfrom
DEV

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

DEV → main promotion for the 1.20.1 PROD deploy. 39 commits ahead of main; clean merge (nothing on main is missing from DEV, so no sync-main-into-DEV reconcile needed this cycle).

Version

  • .deploy/mta.yaml 1.20.0 → 1.20.1 (patch). PROD is currently on 1.19.0; main's 1.20.0 was never PROD-shipped, so this ships all DEV work since 1.19.0.

Highlights since 1.20.0 (main)

Verified on DEV: v1.20.1 deployed, content published (v237), live tutorial asset hashes match the shipped build. Smoke green except the 3 documented-benign stale-assertion/data-gated checks.

After merge: PROD deploy is a separate manual step (primary tree, order planner→tutorials-ims→gameboard).

…rrent (QA)

Mirrors rebuild-content.yml's force-publish. Needed to re-seed the QA
ContentCurrent after the delta flags were toggled (a normal delta publish
skips unchanged slugs vs ContentFiles, leaving ContentCurrent stale). One-time
seed tool; recurring QA rebuilds dual-write deltas normally when WRITE is on.
…nfig

Move the three Content Option-B feature flags (CONTENT_DELTA_WRITE_ENABLED,
CONTENT_DELTA_READ_ENABLED, CONTENT_DELTA_SKIP_CARRYFORWARD) off process.env
onto ImsConfig key/value rows (content.delta.write / .read / .skipCarryForward),
read through a new cached, fail-safe resolver.

- srv/lib/content-delta-flags.js: 60s-TTL warm cache (mirrors ngds-autosend),
  synchronous fail-open getters isDeltaWrite/Read/SkipCarryForward (default
  false, never block/throw on the hot path), async refreshContentDeltaFlags()
  plus bustContentDeltaFlagsCache().
- Warm on boot in srv/server.js (fail-open).
- Replace all process.env.CONTENT_DELTA_* reads across content-store,
  content-publish-session, chrome-shell, admin-service, jobs/cleanup,
  jobs/embedding-reconciliation, embedding-pipeline, embedding-stats.
- registry.js: add kind:'db' (ImsConfig-backed); resolve.js resolves it live.
- AdminService.setContentDeltaFlags / getContentDeltaFlags actions (busts cache).
- srv-qa cp list: add srv/lib/content-delta-flags.js.
- Tests: new content-delta-flags.test.js; convert env-setting delta tests to
  seed ImsConfig plus warm cache.
…db-config

fix(content): move Option-B fast-path flags from env vars to DB config (ImsConfig)
ci(rebuild-qa): add force-publish input to re-seed Option B ContentCurrent (QA)
…' if absent on boot

Follow-up to #2061. Ensures the content fast path defaults ON and survives every
deploy: on boot, ensureContentDeltaDefaults() INSERTs content.delta.write/read/
skipCarryForward = 'true' for any key whose ImsConfig row is absent, BEFORE the
warm refresh. Only fills missing keys — an admin-set value (true OR false) has a
row and is left untouched, so a deliberate disable still survives deploys. The
resolver keeps its fail-safe-false default for the cold-cache/DB-error TRANSIENT
(never serve fast-path on a DB fault); steady state is governed by the seeded
data. Fixes the class where the fast path silently reverted on deploy.
…t-on

feat(content): default Option-B fast-path flags ON (seed ImsConfig if absent)
Move the remaining 14 on/off `kind:'env'` feature flags from process.env
to the ImsConfig key/value table, read through ONE generic, registry-driven
cached resolver — mirroring the just-merged CONTENT_DELTA migration.

- New srv/lib/feature-flags/db-flags.js: warm 60s cache (globalThis-backed so
  all module instances share it), synchronous fail-safe isFlagEnabled() that
  returns each flag's DECLARED registry default on a cold cache / DB error
  (NOT blanket false), ensureFeatureFlagDefaults() (seed-if-absent to declared
  default), refreshFeatureFlags(), bustFeatureFlagsCache().
- registry.js: flip the 14 entries kind:'env'→kind:'db' with dotted-lowercase
  imsConfigKey (flag.*), preserving each declared default; drop envVar/envRule.
- server.js served block: seed defaults + warm the cache at boot (fail-open).
- Replace every env read at the call sites with isFlagEnabled(<KEY>), preserving
  default semantics (false-disables and true-enables both collapse to the
  resolver's encoded default).
- Generic AdminService.setFeatureFlag(flag, enabled) + getFeatureFlags().
- Remove the now-inert env props from .deploy/mta.yaml + deploy/dev.mtaext.
- srv-qa cp list: add db-flags.js + registry.js (imported by copied metrics.js
  and community-blogs-classifier.js).
- Tests: new test/unit/db-flags.test.js; update tests that toggled the 14 env
  vars to use the DB-flag test helpers.
FeatureFlags is a @readonly/@cds.persistence.skip viewer and the setters were
unbound, so Fiori Elements rendered no toggle control. Add BOUND row actions so
an admin can flip each DB flag from the List Report / Object Page.

- admin-service.cds: add `actions { enable(); disable() }` to FeatureFlags,
  each returning FeatureFlags.
- admin-service.js: implement bound enable/disable. Resolve the row key from
  req.params, require a kind:'db' flag (else reject 400 "Only DB feature flags
  are togglable here"). Upsert the flag's backing ImsConfig row to 'true'/'false'
  and dispatch the cache bust to the owning module — content.delta.* flags
  (flagMeta null, db-flags.js excludes them) bust content-delta-flags.js; every
  other managed flag busts db-flags.js. Return the freshly re-resolved row so FE
  refreshes.
- admin-annotations.cds: add UI.DataFieldForAction enable/disable to the
  FeatureFlags LineItem and Identification (Object Page header).
- featureFlags i18n: add enableFlag/disableFlag (annotations use inline labels).
- test/unit/admin-feature-flags-toggle.test.js: enable/disable on a generic
  flag.* flag, routing of a content.delta.* flag, and 400 for db-setting +
  constant flags, plus 401 unauth (6 tests).
feat(feature-flags): migrate remaining 14 env flags to DB config (#2060)
The default-ON boot seed (#2064) runs in the cds.test('serve') harness too,
inserting content.delta.write/read/skipCarryForward='true' into the in-memory
ImsConfig. That flipped the fast path ON for 21 pre-existing content-store /
content-pipeline / carryforward / publish-guard / source-hashes tests written
against the resolver's fail-safe-OFF (legacy ContentFiles) default:
nav/hashes/catalog reads hit the empty ContentCurrent and rollback replayed
ContentHistory instead of clearing.

Gate ensureContentDeltaDefaults() behind !process.env.VITEST. VITEST is set
only by the test runner (unit + hybrid), never in CF or 'cds watch', so
production and local dev still default-ON. The seed function stays exported and
directly callable, so content-delta-flags.test.js is unaffected.
…ed-vitest

fix(content): skip Option-B delta flag boot-seed under vitest
The Rebuild Content (QA) workflow intermittently failed at the publish commit
with gorouter 502→503 (run 33164190492). Root cause: every QA rebuild — even a
single-tutorial tutorial-qa-updated dispatch — republished all 2211 slugs in
force mode, making the commit a ~35s single request that is fragile to a
transient routing blip on the single srv-qa instance; the client's 3 retries
fell inside a ~4s window and couldn't ride it out.

#2062 was a half-done version of this fix: it added a force-publish input and
wired `--channel qa ${force-publish && '--force'}` intending delta-by-default,
but never removed the force:true hardcode in resolvePublishConfig, so the config
override still forced a full publish and no PR ever wired PUBLISH_SLUG. Option B
is now fully active on QA (DB-config delta flags default ON, #2061/#2064), so a
slug-scoped delta QA publish is safe.

Fix #1 — slug-scoped QA publish:
- publish-content.ts resolvePublishConfig qa: force now honors --force (mirrors
  prod) instead of hardcoded true.
- rebuild-content-qa.yml publish step: forward the dispatched slug as
  PUBLISH_SLUG (empty on force-publish re-seed). Commit-triggered rebuild → one
  slug, sub-second commit; full rebuild → whole-catalog delta; force-publish →
  full re-seed.
- fetchRemoteHashes/fetchRemoteSourceHashes: send Bearer when apiKey is set —
  srv-qa gates /content/hashes (verified 401 unauthenticated), unlike public
  prod route; without this, QA delta would 401.

Fix #2 — resilient commit retry:
- withRetry: optional ±jitter via computeBackoff (default 0, callers unchanged).
- commit call: attempts 3→5, backoff [1s,3s,9s]→[2s,5s,10s,20s]+20% jitter so a
  brief blip is ridden out instead of failing in ~4s.

Fix #3 — release publish lock on commit failure:
- On permanent commit failure the client now aborts the session (mirrors the
  append-failure path) which marks the manifest FAILED and releases the lock,
  instead of stranding it for the 30-min TTL (which 409'd every QA rebuild in
  that window). Abort gets its own transient retry; reaper remains the backstop.
  (Server-side auto-abort deliberately omitted: it would turn the client's
  commit retries into 409s and defeat retry of a transient internal error.)

Docs: correct "QA always --force" in build.md + qa-endpoint-design.md.
Tests: publish-content-qa force/delta + workflow-wiring guard; withRetry jitter;
fetchRemoteHashes auth header. 50 publish-layer tests pass.
fix(qa): slug-scope QA content publish + resilient commit + lock release
fix(test): repair unit-suite fallout from flag→DB migration (WIP)
The approuter serves a lapsed/anonymous session as HTTP 200 + an XSUAA
login-redirect HTML page (not 401), and Akamai can serve a cached anon
/auth/user to a signed-in browser. The /me/tokens island gated on
resp.ok alone, so it never set needsLogin, fell through to loadTokens(),
and resp.json() threw on the HTML body -> the page showed
"Couldn't load your tokens. Try refreshing." instead of a sign-in prompt.

- Gate via isSignedIn(): require ok + JSON content-type + body.authenticated
  (mirrors homepage-personalizer/coordinator.ts).
- Harden loadTokens(): a non-JSON 200 (HTML login page) now falls back to
  the sign-in prompt, not the generic load error.
- Add regression tests for 200+HTML on /auth/user and /pats/MyPATs, and
  authenticated:false.
fix(tokens): /me/tokens sign-in gate must check /auth/user JSON, not resp.ok
…user JSON

Same class as the /me/tokens fix (PR #2071): the approuter serves a
lapsed/anonymous session as HTTP 200 + an XSUAA login-redirect HTML page
(not 401), and Akamai can serve a cached anon /auth/user to a signed-in
browser. Both islands gated on authRes.ok alone, so a lapsed session set
isLoggedIn=true, fell through to /api/getMyCompletions(), and json()
threw on the HTML body -> a 'Network error' / 'Failed to load' message
instead of the sign-in prompt.

- Gate via isSignedIn(): ok + JSON content-type + body.authenticated
  (mirrors homepage-personalizer/coordinator.ts).
- Harden the data fetch: a non-JSON 200 (session lapsed mid-flow) sets
  isLoggedIn=false, not a data error.
- Add regression tests for both islands (200+HTML on /auth/user and the
  data endpoint, and authenticated:false).
fix(me): AllCompletions/RecentActivity sign-in gate must check /auth/user JSON, not resp.ok
…2066 unit regression)

#2066 migrated on/off feature flags from env vars to ImsConfig-backed DB
rows. The generic resolver srv/lib/feature-flags/db-flags.js exposes a
synchronous isFlagEnabled() that, on a cold cache, fires a background
refreshFeatureFlags() via scheduleRefresh().

server.js reads isFlagEnabled('MCP_AUTH_ENABLED') inside cds.on('bootstrap')
to install the MCP kill switch. At bootstrap time cds.model is not yet
linked, so the fire-and-forget scheduleRefresh() called cds.connect.to('db')
prematurely. cds.connect caches cds.db eagerly (cds.services.db = the pending
promise) and resolves the db service's model from the partial, service-less
model that exists mid-bootstrap. That poisoned primary-db instance then
persists for the process, so cqn4sql can no longer resolve service-qualified
targets (SearchService.SearchableItems etc.), leaving '*' unexpanded and
every later service read throwing 500 "Query was not inferred and includes
'*' in the columns" — ~205 unit failures. content-delta-flags.js was
unaffected only because none of its getters run during bootstrap.

Fix: scheduleRefresh() no-ops while cds.model is unset. The bootstrap read
then returns the flag's declared registry default (the intended cold-cache
behavior, per the server.js comment) and the real warm-up runs in the
'served' handler once the model is fully loaded. Runtime-safe on every
dialect: the poison is model resolution, not SQLite-specific rendering.

Also update two stale kg-path tests (kg-path-v2-or-v1, kg-path-between-handler)
that still toggled the now-DB-driven KG_PATH_V2_ENABLED via vi.stubEnv; switch
them to __setFlagForTest/__resetFlagsForTest, matching the pattern #2066
already applied to the other migrated-flag tests.
…-regression

fix(feature-flags): don't connect to db before model is loaded (fixes #2066 unit regression + latent boot bug)
The 08-25 release (bundled prompt tightening) shifted checkFreshness to
'prefer reporting NOTHING over a speculative one', which suppressed
essentially all findings — the LLM returned 0 raw findings even with
grounding present, so the admin action appeared to 'return nothing'.

Recalibrate PRECISION by severity: keep High/Medium strict (confidence-
first, prefer omitting speculative ones) but treat Low as ADVISORY so
cosmetic/dated-style staleness surfaces instead of an empty report. All
other guardrails (CONTEXT, OUTPUT vs CODE, RESPECT INTENT, SAP
CONVENTIONS, SCOPE) are unchanged.

DEV A/B (real grounding, claude-4.6-sonnet): tutorials that returned 0
under the tightened prompt now return 1-3 findings (mix of Medium + Low),
including a genuinely useful Medium.

Updates the prompt-guard unit test to assert the new severity-calibrated
intent.
…-calibration

fix(freshness): calibrate prompt by severity so Check Freshness surfaces findings again
…cp-auth 401

Spike findings (Task 3 Step 1):
- xs-app.json confirms /mcp-auth/* and /mcp-admin/* are XSUAA routes
  (authenticationType: "xsuaa", scope: "$XSAPPNAME.Tutorial.MCP") that
  proxy to srv-api. Without a bearer token the approuter currently
  302-redirects to the XSUAA login page with no discovery pointer.
- insertMiddleware.first runs before the approuter's built-in XSUAA auth
  middleware, so short-circuiting no-Authorization-header requests there
  is safe and the handler never interferes with a real bearer token.
- Browser-cookie sessions hitting /mcp-auth (no bearer) are out of scope:
  this namespace is programmatic MCP-only; noted and proceeding.

Implementation:
- approuter/lib/mcp-auth-challenge.js: new middleware that fires only on
  /mcp-auth and /mcp-admin prefixes with no Authorization: Bearer header.
  Calls resolveBaseUrl(req) + resolveScope() from ./well-known-oauth (no
  changes to that module — Option A preserved), sets
  WWW-Authenticate: Bearer resource_metadata="…", scope="…", then
  responds 401 with a JSON error body. Valid bearer always calls next().
- approuter/server.js: require + wire mcpAuthChallengeHandler into
  insertMiddleware.first after the well-known handlers and before
  devtoberfestCspHandler/staticHandler/proxyHandler.
- test/unit/mcp-auth-challenge.test.js: 3 unit tests (no-bearer 401,
  bearer pass-through, unrelated path pass-through); all pass.
…very

feat(approuter): complete .well-known MCP discovery surface (openid-config, mcp.json, WWW-Authenticate pointer)
docs: register akamai-well-known-forward in sidebar (fix docs:build)
…ed:thirdparty

HomepageShelves kept losing its curated/third-party rows (Prior Labs, Dremio,
Reltio, n8n, …) on deploy. Root cause: while the seed CSV lived in db/data it
compiled to an .hdbtabledata with include_filter:[] that HDI full-replaced the
table from on every deploy whose CSV hash changed, wiping runtime-seeded rows
and admin edits. Commit 1404c4c moved the CSV to test/data (stopping the wipe
on PROD) but never re-seeded the lost rows, and the manual `npm run
seed:thirdparty` promotion was DEV/PROD-ambiguous (it followed the CDS binding,
so it could silently target the wrong environment).

Replace both mechanisms with an idempotent, non-destructive boot seed:

- srv/lib/homepage/homepage-shelves-defaults.js — single inline canonical
  baseline (SAP entries + third-party links); ships with the srv bundle, no
  runtime file-read / packaging risk.
- srv/lib/homepage/seed-homepage-shelves.js — insert-if-missing on (verb,url),
  called from cds.on('served'). Never overwrites admin edits, self-heals a
  deleted baseline row on next restart, non-fatal, runs in every profile
  (cds watch, unit tests, hybrid, production).
- Retire test/data seed CSV, db/data/staging/homepage-thirdparty.json,
  scripts/seed-thirdparty.js + its two tests, and the seed:thirdparty npm
  script (now a helpful error stub pointing at the new path).
- admin-managed-seed-csvs guard test asserts the CSV is gone from BOTH data dirs.
- Docs: new "HomepageShelves seeding" section in homepage.md.

Tests: new defaults-validation + seed-behavior unit tests added; homepage-seed,
homepage-shelves-crud, homepage-link-health still green (9078 unit tests pass).
…ble-seed

fix(homepage): boot-seed HomepageShelves; retire seed CSV + manual seed:thirdparty
chore(release): bump MTA 1.20.0 → 1.20.1 + refresh What's New digest
@jung-thomas
jung-thomas merged commit 2dd1111 into main Aug 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant