Skip to content

Release 1.33.0 → main (production) - #2578

Merged
jung-thomas merged 157 commits into
mainfrom
release-1.33.0
Oct 1, 2026
Merged

jung-thomas merged 157 commits into
mainfrom
release-1.33.0

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Production release 1.33.0

Promotes DEV → main for the next PROD deploy. Version 1.32.0 → 1.33.0 (minor — ships new features).

Release commit: chore(release): 1.33.0 — bumps .deploy/mta.yaml and adds 42 user-facing whats_new.json entries (generated via the whats-new skill: merged PRs #2451–#2576 since 1.32.0, release-plumbing/CI/boot-fix PRs excluded).

Changelog breakdown (42 entries)

Scope

  • 157 commits ahead of main (all of origin/DEV + the release commit). main is fully contained in DEV — clean merge, no divergence.
  • Release touches only .deploy/mta.yaml + hugo/data/whats_new.json (the srv snapshot is gitignored, rebuilt at build time).

Deploy

Merge does not auto-deploy. PROD deploy is a separate, explicitly-authorized step: deploy from a fresh origin/main after merge, cf target checked, sap-devs scheduler paused.

…2515)

The inline-glossary walker scopes over the whole `.tutorial-steps`
subtree, which contains the Vue-mounted question islands (Validation
quiz, Challenge, tutorial-branches). When a question's text contained a
known term (e.g. "MCP"), the walker replaced the text node with a
`<span class="glossary-term">`, injecting an inline element into the
Validation legend's `inline-flex` layout — splitting the question into
broken columns (see #2515 screenshot) — and racing Vue's vdom over DOM
it owns.

`shouldSkip` now bails on any ancestor that is an island mount host
(class ending `-mount`, the markers emitted by
`shortcodes/tutorial-step.html`), so question text is never rewritten.
Ordinary tutorial prose is unaffected.

Adds `test/unit/glossary.test.ts` (happy-dom) covering: prose still
wraps; Validation and Challenge islands are skipped.
…out-2515

fix(glossary): skip Vue question islands so terms don't break layout (#2515)
Authors can put multiple [VALIDATE_N] blocks in one step (the number keys
the STEP, not the question) and the parser already accumulated them into
one array. But every question was assigned the same `validate-${stepNum}`
id, so client-side answer/verdict state (keyed by id) collided across the
questions: only one could hold an answer, MCQ selections appeared stuck,
and the all-or-nothing grader failed the step. The parser's own sibling
maps (`${stepNum}:${q.id}`) also overwrote each other, collapsing AI-graded
specs to one row per step.

Assign `validate-N` to the first question in a step and
`validate-N-<ordinal>` to each subsequent one. First-question id is
unchanged, so single-question steps and all published content are
byte-for-byte identical. The Validation.vue component and grading already
key everything by id and need no change.

Adds rules-multi-question-per-step.test.ts covering distinct ids,
backward-compat, and no sibling-map overwrite for AI-graded blocks.
…ep-2514

fix(validation): unique id per question for multi-question steps (#2514)
Public XSUAA instance + dedicated tutorials-srv-mcp module so mcp-remote
authenticates via PKCE (no client secret). Retire srv-qa copied-lib drift
tax via a core/content/mcp/kg/channels npm workspace. Approuter login flow
unchanged.
Plain npm workspaces don't survive CF/MTA packaging (buildpack npm install
inside packed gen/<module> can't resolve file:/symlinked sibling packages).
Consume shared packages via per-module esbuild bundle (mirrors existing
parsers-bundle). P1 proves the mechanism on DEV before other packages ride
on it; add R0.
17 tasks across 5 phases: workspace+core+bundling harness (proved on DEV),
carve content/mcp/kg/channels, srv-mcp module + public XSUAA + mta-mcp.yaml,
retire srv-qa cp block, PR to DEV. PKCE no-secret connect is the acceptance test.
- Fix T2 skeleton defect: packages/core/package.json type commonjs → module
- Add exports map: { ".": "./index.js", "./legacy-id.js": "./legacy-id.js" }
- git mv srv/lib/legacy-id.js → packages/core/legacy-id.js (preserves history)
- Replace srv/lib/legacy-id.js with ESM re-export shim:
    export { getNextLegacyId, resetCounters } from '@tutorials/core/legacy-id.js'
- Update packages/core/index.js to ESM barrel re-exporting both symbols
- Verified: boundary lint OK, core subpath resolves, shim resolves, 20 test-file
  failures are all pre-existing (srv-qa port-3000 + MediaPipe + Vite browser issues)
…eploy

Adds the bundling harness (Task 4) that packages @tutorials/core into
a self-contained ESM artifact for CF/MTA deployments where npm workspace
symlinks do not survive packaging.

- scripts/bundle-shared.cjs: esbuild bundles @tutorials/core → srv/lib/_shared/core.bundle.mjs
  (ESM, format:esm, with createRequire banner for CJS interop; @sap/* @cap-js/* node:* external)
- srv/lib/_shared/ gitignored (generated artifact)
- package.json: adds prebuild:shared-bundle + chains into prebuild before parsers-bundle
- .deploy/mta.yaml before-all: runs bundle before cds build --production
- srv/lib/legacy-id.js: TLA shim prefers CF bundle, falls back to workspace package in local dev
- test/unit/shared-bundle.test.js: 3 vitest tests verifying bundle creation + exports
Devtoberfest leaderboard improvements (#2510): shared competition ranks with
stable tie order, prev/next paging, participant counter, and personal standing
(highlighted + sticky "you" row, rank #N of M). Pairs with gameboard-srv V2
endpoint + communityUrl caller-match, already on gameboard-srv main.

PRs: #2511 (feature), #2512 (you-row communityUrl match fix).
…rface (#2517)

- Move 5 rank test cases from srv/lib/kg-explore-data.test.js into the
  existing test/unit/srv/kg-explore-data.test.js (correct home for this
  module's unit tests); add a mission-node test asserting rank is never
  stamped on non-tutorial/concept types.
- Delete srv/lib/kg-explore-data.test.js (stray, would duplicate coverage).
- Revert srv/lib/**/*.test.{js,ts} addition in vitest.config.ts (policy
  change not needed once tests live in test/unit/srv/).
- Revert ExploreNode interface added to hugo-apps/src/related-graph/types.ts
  (dead code — nothing in related-graph consumes ExploreNode; rank? stays
  only in app/explore/src/types.ts where it is used).
…raph-2517

feat(explore): opt-in 3D KG view (#2517)
Repo is ESM (type:module); packages/shims/bundles are ESM (.mjs, format:esm,
createRequire banner), not the CJS shown in the original snippets. Verified in
T3/T4.
…ckage

MCP auth modularization — foundation (workspace + core + esbuild bundling)
…core (ESM)

Moves 27 core files from srv/lib/ (and srv/jobs/job-lock.js) into
packages/core/, preserving subdirectory structure (feature-flags/,
runtime-config/, branch/, jobs/). Replaces each original path with an
ESM shim using the bundle-first/workspace-fallback pattern established
in Task 4.

Changes:
- git mv all 27 files to packages/core/<same-relative-path>
- Write ESM shims at each old srv/lib/ (and srv/jobs/) path
- packages/core/package.json: wildcard subpath export "./*": "./*"
- packages/core/index.js: barrel re-exports full moved surface
  (test helpers and FLAG_TTL_MS duplicate omitted from barrel to
  avoid export-name conflicts; available via subpath imports)
- page-fallback.js: probe-both-candidates path resolution for the
  srv/page-fallback/ directory (works from both package and bundle)

Verified:
- node scripts/lint-package-boundaries.cjs -> OK
- node scripts/bundle-shared.cjs -> 82 keys bundled
- @tutorials/core/metrics.js + shim both resolve 7 exports
- srv/developer-service.js loads OK
- npm test: 65 failed | 1335 passed (same 65 pre-existing failures)
… 2 more test files (T6 fix-2)

mail-client-credstore.test.js and rebuild-trigger.test.js both mocked
'../../srv/lib/credstore.js' (the shim).  After the T6 carve the real
consumer path is packages/core/secret-resolver.js → ./credstore.js
(package-relative), which bypasses the shim entirely, so Vitest's mock
never intercepts the readSecret call.

Fix: change every vi.mock / dynamic import target in both files from
the shim path to '@tutorials/core/credstore.js' — matching the module
instance that secret-resolver.js actually resolves.

Affected-suites result: 2 passed / 27 tests passed
The MCP public-PKCE token from IAS carries a VERIFIED email (sub) but NO
I-number — its user_uuid is a SCIM UUID, not the SAP ID (decoded live from
atxgsg7zi 2026-10-01). The existing resolveUserSapId XSUAA chain would wrongly
return that SCIM UUID as a bogus sapId. This adds the IAS resolution path:

- packages/core/resolve-db-user.js:
  - isIasToken(user) — detects IAS via iss ~ *.accounts.ondemand.com
  - iasEmailFromToken(user) — verified email from sub, else email claim,
    lowercased (fixes a sub||email short-circuit found by TDD)
  - resolveIasSapId(user) — async email → Users.email → sapId join. Fetches ALL
    matches (not SELECT.one) and deterministically PREFERS the real I-number over
    a SCIM-UUID-shaped sapId: a live DB check found thomas.jung@sap.com maps to
    BOTH I809764 AND a stray SCIM-UUID row (fbf099e2-…) auto-provisioned by an
    earlier IAS login. Without the preference the join was order-nondeterministic.
    Fail-closed (null when non-IAS / no email / no row); never fabricates an id.
  - pinIasSapId(user) — pins the resolved I-number onto authInfo.token.userId so
    the SYNCHRONOUS resolveUserSapId (and every handler) returns it via branch #1.
    XSUAA path untouched.
- srv-mcp/developer-service.js: this.before('*') calls pinIasSapId once per
  request (after CAP auth, before handlers; non-fatal on error).
- packages/core/index.js + srv/lib shim: re-export the 4 new names.

Tests: 29/29 (resolve-db-user + resolve-ias-sapid incl. email-collision cases).
Email verification is enforced IAS-side, so the email join is spoof-resistant.

Follow-up (data op, not code): 6 stray SCIM-UUID-keyed Users rows exist from
pre-fix IAS logins; harmless now the resolver prefers the I-number, cleanable
separately. core.bundle.mjs is a gitignored artifact rebuilt at deploy.

Refs #2550, #2551.
…r-2550

feat(mcp-auth): resolve IAS MCP identity via verified-email join (#2550)
The deployed srv-mcp served DeveloperService as a GENERIC ApplicationService
with none of McpDeveloperService's on-handlers → every MCP tools/call returned
'Service "DeveloperService" has no handler for "<tool>"'. The MCP tools never
worked over HTTP on any deploy (local/hybrid worked, masking it).

Root cause: CAP factory.js _sibling resolves a service's handler via its
@source dir (srv-mcp/developer-service.cds → probes srv-mcp/developer-service.js)
but cds build nests handlers under gen/srv-mcp/srv/ (cds.env.folders.srv='srv/').
@source dir (srv-mcp/) != gen nesting (srv/) → sibling miss → generic service,
no handlers. The main srv module only works because its folder IS named 'srv'.

Fix (build-only): scripts/patch-mcp-impl.cjs runs in mta-mcp.yaml before-all
after cds build and injects @impl='srv/developer-service.js' into the baked csn.
A bare (non-'./') path resolves against cds.root → gen/srv-mcp/srv/developer-
service.js (the real file). Never touches source/hybrid, where auto-discovery
already works. Verified: @impl set, target present.

This is independent of the #2550 IAS identity work — it was blocking its
acceptance test. Structural follow-up (rename module folder to the srv/
convention; also affects srv-qa) tracked in #2569.

Refs #2550, #2569.
…ding

fix(srv-mcp): bind DeveloperService impl in production build
…er (#2552)

Approved design: switch the durable identity key from Users.sapId (SAP employee
ID) to an (issuer, subject) linkage layer so IAS and future social logins —
which carry no P/S/I number — resolve correctly. Keep sapId as an NGDS-only
attribute (carve-out is a structural no-op). Email stays an attribute, never a
key (PROD: 97.6% null, non-unique).

Grounded in live PROD/DEV data probes + a full blast-radius audit (sapId is the
universal per-user key via one chokepoint; ~40 callsites stay unchanged via the
pin pattern). Supersedes the unsafe #2550 email-join, which this build removes.

5 sections approved: schema (UserIdentities), 3-tier resolver, provisioning +
no-sapId seam, NGDS carve-out, scope/ktt-fix/migration/testing.

Refs #2552, #2550.
…for no-sapId users (#2552)

Both row-resolvers now honor the Users.ID pin set by pinResolvedUser:
- resolveDbUser: checks attr.dbUserId first (fast-path for pinned users),
  falls back to sapId-based WHERE, returns null when neither is present.
- resolveDbUserId: same priority — attr.dbUserId cache sets user.__dbUserId
  directly without a DB round-trip; then sapId path; then null.

This carries social/IAS users (no canonical sapId) through all ~40
downstream callsites without per-callsite changes.
)

Test fix: update Users-row-miss scenario in author-service.test.js to use
a SCIM-UUID-shaped userId (unprovisionable under the new provisionDbUser
rules from Task 7) instead of a canonical SAP ID (I999999) which the new
get-or-create path now provisions. The miss guard still fires for SCIM UUIDs
with no email/name claims.
…er-2552

feat(identity): (issuer,subject) identity-linkage layer (#2552)
The #2552 UserIdentities entity shipped as a plain .hdbtable but was NEVER
created in the DEV HANA container: this project's db/src/.hdiconfig maps ONLY
the hdbmigrationtable plugin (com.sap.hana.di.table.migration) — there is NO
plain-hdbtable plugin — so a non-journaled entity's .hdbtable artifact is
packaged in the mtar but silently never applied by HDI. Confirmed live: Users
(journaled) has 816k rows in the container; COM_SAP_DEVELOPERS_IMS_USERIDENTITIES
did not exist after the first full deploy.

Every one of the other 55 persisted IMS tables is journaled via
@cds.persistence.journal in db/persistence.cds; UserIdentities was missed. The
design spec's 'additive .hdbtable, no ALTER' guidance was wrong for this HDI
container type.

Fix: annotate ims.UserIdentities with @cds.persistence.journal; cds build then
emits a clean version=1 .hdbmigrationtable (fresh CREATE, no migration steps)
instead of the unused .hdbtable. Committing the generated migration source +
updated last-dev/csn.json as required (migration sources are version-controlled).

Still additive — a brand-new empty table; no ALTER on the 816k-row Users.

Refs #2552.
…l-2552

fix(identity): journal UserIdentities so it deploys to HANA (#2552)
…able transition (#2552)

The journal fix (#2572) added a .hdbmigrationtable for UserIdentities, but the
FIRST deploy had already registered the plain .hdbtable design-time artifact in
the DEV HDI container. HDI then refused the make with:

  db://COM_SAP_DEVELOPERS_IMS_USERIDENTITIES: the object cannot be provided more
  than once — the .hdbmigrationtable would provide it; the deployed .hdbtable
  already provides it — Could not compose a consistent make graph [8212012]

So the make FAILED and rolled back → table still absent. This is the CAP-
documented hdbtable→hdbmigrationtable transition rule: the old .hdbtable must be
on the HDI undeploy allowlist so HDI drops it as the migration table takes over.
Every other IMS table that made this transition is already listed in
db/undeploy.json (Users, Tutorials, Steps, …); UserIdentities was missing.

Fix: add src/gen/com.sap.developers.ims.UserIdentities.hdbtable to
db/undeploy.json. Next deploy: HDI undeploys the stale .hdbtable and deploys the
v1 migration table cleanly, creating the (empty) table.

Refs #2552.
…oy-stale-hdbtable

fix(identity): undeploy stale UserIdentities.hdbtable for migration-table transition (#2552)
#2539)

Adds docs/end-users/api-consumption.md: the map of all API surfaces (MCP
anon/PAT/OAuth, Admin OData, GraphQL, A2A), which are headless-capable vs
browser-only, and how to authenticate each — cross-linking the MCP/A2A
quickstarts rather than duplicating.

Documents DJ Adams' use case (#2539) honestly: his /admin/Tutorials OData query
is valid, but admin OData is BROWSER-SESSION-ONLY today — no PAT or
client-credentials token can carry the Admin scope (Admin is a named-user role,
not in xs-security authorities; PAT is /mcp-pat/* only). So the supported paths
are: browser for ad-hoc admin OData, MCP PAT tier for scripted per-user data.

Also links the previously-unlinked MCP + A2A quickstarts from the end-users
index (new 'APIs & programmatic access' section).

Gaps filed as future work: #2574 (headless admin OData access — capability +
security decision), #2575 (public api-docs portal restructure).

Docs-only; no code/config change.

Refs #2539, #2574, #2575.
docs(api): API consumption guide + honest auth/gaps (#2539)
The 'Deploy Docs to GitHub Pages' CI job failed on the #2576 merge:
  Unregistered pages (on disk but not in sidebar): /end-users/api-consumption
docs:build enforces that every page on disk is in the sidebar config. The new
api-consumption.md was linked from the README but not added to
docs/.vitepress/config.ts. Added it to the End Users sidebar (after the MCP/A2A
quickstarts). Verified: npm run docs:build completes clean locally.

Refs #2539.
…umption

fix(docs): register api-consumption in VitePress sidebar (#2539)
42 user-facing changes since 1.32.0 (12 features, 24 fixes, 3 performance,
3 docs). Highlights:

- Identity: more reliable account matching across login providers via an
  (issuer,subject) identity-linkage layer (#2552), so progress stays attached
  to the right profile even without an SAP employee ID.
- MCP: secretless MCP OAuth via IAS public-PKCE (#2550), MCP developer tools
  now respond over HTTP (#2570), and a new programmatic API-consumption guide
  (#2539).
- Real-time: WebSocket sticky sessions + Redis adapter for cross-instance
  fan-out (#2524).
- Content/learning: randomized quiz answer order (#2558), opt-in 3D knowledge-
  graph Explore view (#2517), tutorial parser/heading fixes (#2555), faster
  content publish (#2462/#2463/#2464) and Concepts page (#2532).
- Devtoberfest: scoring-correctness fixes and admin Semaphore taxonomy sync
  (#2477/#2479).

Release files only: .deploy/mta.yaml version bump + hugo/data/whats_new.json
digest entries (generated via the whats-new skill). srv snapshot is gitignored,
rebuilt at build time.
@jung-thomas
jung-thomas merged commit 4167ef1 into main Oct 1, 2026
9 of 10 checks passed
@jung-thomas
jung-thomas deleted the release-1.33.0 branch October 1, 2026 21:52
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