Repository navigation
Release 1.14.1 — DEV → PROD - #1789
Merged
Merged
Conversation
The /admin/rebuild handler atomically replaces the whole static dir with the pushed tarball. rebuild-content.yml's 'Assemble static content' step does NOT include static/qa (the QA author-preview navigator, shipped only by the full MTA deploy via root mta.yaml's inline hugo/public-qa build), so every full/catalog-only content rebuild WIPED static/qa off the running pod -> /tutorials-qa/ 404s until the next deploy. This is the recurring 'no QA navigator on PROD' root cause. Carry deploy-only subtrees (REBUILD_PRESERVE_DIRS = ['qa']) forward from the previous static dir across the swap, but only when the incoming tarball did not ship them (so a future tarball that DOES include static/qa still wins). Verification requires a deploy + a content rebuild (cannot be exercised by unit tests / curl). Alternative considered: add a QA fetch+build+assemble to rebuild-content.yml (matches the #350 include-in-tarball precedent) — heavier (adds a full QA build to every prod rebuild); preserve is lighter and QA nav is content-rebuild-independent by design.
… mtar The local deploy path (npm run deploy -> build:deploy -> build:all) never built the QA channel: build:all omits build:qa, and .deploy/mta.yaml's approuter builder only COPIES a pre-built hugo/public-qa into static/qa (it does not render it). So local deploys shipped a stale/empty static/qa and /tutorials-qa/ 404'd. CI's root mta.yaml builds QA inline — this closes the divergence for the local path. - Step 2.7 (new): fetch-tutorials:qa + build:qa BEFORE mbt build, so mbt copies a fresh hugo/public-qa into static/qa. Needs a GitHub token (reuses NODE_AUTH_TOKEN/GITHUB_TOKEN) + CAP_BASE_URL; hugo/content-qa is generated by the fetch (not committed), so fetch must precede build. - Step 3.6 (new) + scripts/check-shipped-qa-bundle.cjs: mtar-parity guard that cracks the built mtar and asserts static/qa/index.html (+ tutorial-navigator) are present — fails the deploy loudly if the QA navigator didn't ship. Mirrors scripts/check-shipped-admin-bundle.cjs. curl cannot catch this (XSUAA-gated). UNVERIFIED: cannot be exercised without a deploy (build could not be run here). Companion to the approuter static/qa preserve PR (stops content-rebuild wiping it).
#1732 author pages, the 'more from author' rail, and byline eligibility were surfacing tutorials that are unpublished (status=INACTIVE / soft-deleted in HANA) or deleted from the source repo (stale .tutorial-cache entries). Root cause: the author-index builder grouped the RAW GitHub discovery set (authorRows, pushed for every parsed tutorial in fetch-tutorials.ts) and never intersected it with the active/published catalog set that the main navigator/ browse already use (tutorialMetas = status='ACTIVE' or null, per srv/lib/build-catalog.js). Fix at the single source (buildAuthorIndex → author_index.json, which drives page + rail + byline): - buildAuthorIndex(rows, advocates, activeSlugs?): skip rows whose slug.toLowerCase() is not in the active set. Fail-open when the set is empty/undefined (degraded/ALLOW_EMPTY_CAP builds). Lowercase compare — row slugs are mixed-case source dirs, catalog slugs are lowercase-canonical. - writeAuthorPages: thread activeSlugs through (all-inactive authors then drop out of the index and their stale .md page is pruned). - fetch-tutorials.ts: pass new Set(tutorialMetas.map(m => m.slug.toLowerCase())). - tests: exclusion, case-insensitive match, all-inactive author dropped, fail-open (undefined/empty). All 15 author-index unit tests pass. Takes effect on the next content rebuild / fetch-tutorials run.
…unpublished fix(authors): exclude unpublished/deleted tutorials from author pages, rail & byline
…d-guard fix(deploy): build QA channel on local deploy + guard QA navigator in mtar
…ntent-rebuild fix(approuter): preserve static/qa across content-rebuild swap (recurring QA navigator wipe)
The Link health override field rendered as free text: a bare String-enum
(HomepageLinkStatus) does not reliably materialise a Fiori dropdown from
@Common.ValueListWithFixedValues alone. Back it with an explicit in-memory
code list (LinkStatusChoices, {code,label}) served like VerbChoices/
ShelfChoices, and wire @Common.ValueList CollectionPath onto linkStatusOverride
on both HomepageShelves and HomepageForYouCandidates.
- Picker offers OK/SLOW/BROKEN only; UNKNOWN is a job sentinel and blank
clears the override (auto-detect resumes next run).
- @UI.RecommendationState: 0 opts the field out of RPT-1/@cap-js/ai, which
the new CollectionPath would otherwise auto-hook (AICore Internal Server
Error hazard; recommendations are meaningless for a 3-value picker).
- Integration test locks the READ handler rows + $metadata ValueList wiring.
…ride-valuehelp fix(admin): value help for linkStatusOverride (Shelf Entries + For-You)
…ution When a tutorial has no `authorProfile` frontmatter, the index-builder and both Hugo templates (byline + "More from author" rail) now fall back to the top git-contributor login — but ONLY link/attribute when that login ALREADY has an established author page (resolved via `authorProfile` on at least one other active tutorial). Changes: - `AuthorTutorialRow` gains optional `topContributorLogin` field - `buildAuthorIndex`: pre-scans active rows for established logins, then uses `effectiveLogin()` which checks contributor against that set. Mints NO new author pages from contributors. Refactors duplicate `isActive` guard into a helper to remove the repeated expression. - `fetch-tutorials.ts`: threads `contributors[0]?.login` into `authorRows.push` - `tutorial-author.html`: falls back to `$lastCommitterLogin` for the author_index lookup when `authorProfile` is absent - `more-from-author.html`: same fallback via `contributors` frontmatter param Result: the danielva-class case (contributor has an existing author page but the tutorial lacked `authorProfile`) now links internally. Author pages with established logins pick up their contributor-only tutorials on next content rebuild. Orphan contributors keep the existing GitHub-link byline unchanged.
…or-fallback feat(#1758): conservative contributor fallback for author-page attribution
The #1732 'More from this author' rail is cross-tutorial navigation the host page owns. In any embed mode (none/minimal/reader) an embedded tutorial should stay focused on its own content, so hide the rail via html[data-embed] .more-from-author — mirroring the existing footer / breadcrumbs / feedback-share embed-hide rules. Adds an e2e guard that the rail is present without embed and hidden under ?embed=minimal.
…r-rail fix(embed): hide More-from-author rail in embed mode
…(tracking parity)
Replace per-row SELECT.one(Users) with a two-pass approach: pass 1 applies cheap in-process gates (migration/epoch/ceiling/null user_ID), pass 2 bulk-fetches distinct user IDs in chunks of 500 (HANA packet-cap safety) and builds a canonical-sapId Set, pass 3 filters survivors against the Set. O(N) queries → O(ceil(distinct_users/500)) queries.
…ch note - selectResendCandidates: push submissionIdCompleted!=null and completionDate>=epochMs into the WHERE clause; export NGDS_ELIGIBLE for reuse by the CLI (#1+#2) - CLI: refuse --execute when unstamped COMPLETED rows exist, enforcing backfill-first ordering (preflight exits 2 with a clear message) - Add comment explaining kill-switch bypass is intentional (#3) - Test: seed row 6107 (COMPLETED+canonical+post-epoch, no submissionId) and assert it is excluded from candidates
Alerts is @odata.draft.enabled, so ctaUrl's @Common.ValueList field was auto-hooked by the @cap-js/ai RPT-1 recommendation plugin (it keys on @Common.ValueList.CollectionPath). That turns the field into a recommendation field which validates input against the AlertCtaTargets value-list and discards free-typed URLs (e.g. /whats-new/) on Enter/commit, despite @Common.ValueListWithFixedValues:false. ctaUrl was the only such field on a draft-enabled admin entity missing the @UI.RecommendationState:0 opt-out that every sibling carries. Add it to restore plain free-text entry (value-help remains as quick-pick suggestions). Also add /whats-new/ to the CTA_TARGETS suggestion list. Annotation/data only; no schema or DB change.
…ired No active Akamai Fast-Purge hook exists today, so s-maxage=86400 meant published content could stay stale at the CDN for up to 24h. Bring it down to s-maxage=600 (10 min worst-case staleness) across both the CAP content-store header helper and the approuter catch-all static route. Also adds a cacheControl directive to the xs-app.json catch-all route so homepage HTML and other static pages get the same modest TTL instead of no Cache-Control at all. Edge-Cache-Tag continues to be emitted so a future purge hook requires no changes here beyond raising the TTL back toward a day. - srv/lib/edge-cache-headers.js: s-maxage 86400 → 600, updated comment - approuter/xs-app.json: add cacheControl to catch-all static route - 4 test files: update s-maxage assertions to match new constant
fix: allow free-text CTA URL on Alerts (opt out of @cap-js/ai RPT hook)
Cold-load CLS at 1280x720 was 0.829 (Poor). Root cause: the two island
mount divs that Vue fills at ~2.5s were empty (0px) at SSR, so the
featured-topics-carousel below them was in the viewport when the video
band grew from 0->500px -- a shift of ~500px pushing all below-fold
content down.
Fix: add min-height CSS matching the post-hydration desktop heights:
[data-island=videos] { min-height: 500px; }
[data-island=community] { min-height: 260px; }
Measured with injected CSS on live PROD (Playwright, cold context,
1280x720, no scroll): CLS 0.829 -> 0.067 (-92%). Remaining 0.067 is an
early shift at ~1.3s unrelated to islands (below 0.1 Good threshold).
On mobile (390px) both islands are below-fold at first paint so their
hydration shifts do not score against CLS; no mobile override needed.
fix(ngds): trackingInfo.tracking parity + backfill + resend scripts
…i5slim perf(cache): reduce edge TTL from 1d to 10min until purge-by-tag is wired
Catalog-only content rebuilds skip "Fetch tutorials", so fetch-tutorials.ts
never emits the per-login author stubs or author_index.json — but the run
still assembles and atomically pushes the approuter static tarball, wiping
every /authors/{login}/ page a prior full rebuild shipped. Same wipe class as
the 2026-08-07 /browse/ incident (and the earlier /explore-ui/ one). Reported
as a PROD 404 on /authors/<login>/ after overnight catalog-only rebuilds.
Author pages can't be rebuilt in catalog-only (buildAuthorIndex needs
per-tutorial author attribution only the skipped GitHub fetch provides), so
mirror the browse fix and re-hydrate from the deployed site:
- Publish author_index.json as a served static asset (/author_index.json) on
full builds so it can be recovered later (prod channel; QA rebuild is separate).
- scripts/seed-authors-from-deployed.ts fetches that copy in catalog-only and
regenerates stubs + data + the served copy (so consecutive catalog-only
rebuilds keep working). Fail-safe: exits non-zero if the deployed index is
unreachable/empty rather than shipping a wiped /authors/*.
- New catalog-only-gated "Preserve author pages" workflow step, beside the
existing browse-preservation step.
- Extract shared writeAuthorPagesFromIndex helper (used by both the full-build
and re-hydration paths); unit tests for parseAuthorIndex + the helper.
Bootstrap: the first catalog-only run after this ships but before a full
rebuild has published /author_index.json fails-safe; run mode=full once.
…log-only-wipe fix(authors): preserve /authors/* across catalog-only rebuilds
…3.48MB) The global ui5-bootstrap.js shipped 16.25MB uncompressed on every page. Measured composition: 63% CLDR locale data (76 locales), 16% all-locale i18n bundles (57 locales), ~25% theme params, ~11% actual component/icon code. Root cause: the blanket "@ui5/webcomponents/dist/Assets.js" transitively imports the localization (CLDR) + all-locale i18n loaders. Those are authored to lazy-load one locale via dynamic import(), but Hugo's js.Build runs esbuild with splitting disabled, so every per-locale branch is inlined statically - ~13MB of locale data baked into a single file. This site imports no date/calendar/number components (no DatePicker/Calendar), so CLDR is dead weight, and the platform is English-first, so non-English message bundles are unused (components fall back to compiled English defaults). Fix: import only the theming json-imports (sap_horizon + sap_horizon_dark), dropping CLDR and non-EN i18n. Verified with a real esbuild build (reproduces Hugo js.Build byte-for-byte) + Playwright runtime test of the trimmed bundle: - bundle 16.25MB -> 3.48MB (-79%) - all representative components upgrade (Button/Dialog/RatingIndicator/ Timeline/Select/ShellBar/MessageStrip) - both themes apply (light bg #f5f6f7, dark bg #12171c) via setTheme + observer - component text renders in English; no CLDR/locale/icon console errors If a date component or non-English UI is ever added, restore the blanket Assets.js import, or move this bootstrap to the Vite pipeline so the per-locale chunks lazy-load as intended (tracked as the Tier 2 follow-up).
perf(ui5): drop unused CLDR + non-EN i18n from bootstrap (16.25MB → 3.48MB, −79%)
…strict
The primary-tree guard blocked ALL env deploys from a worktree. Per policy
update: that strictness should apply only to PROD. DEV/QA integration deploys
may run from a worktree as long as it is a FRESH checkout at the origin/DEV tip
(never stale or ahead), so mbt bakes exactly the integration-branch content.
- prod: unchanged — must run from the primary checkout on `main`.
- dev/qa: worktree allowed; guard now fetches origin/<branch> and asserts
HEAD === origin/<branch>, failing loudly with a reset hint if divergent.
Verified live: the DEV deploy that ships this change ran from a worktree and
passed the new guard ("fresh DEV checkout @ <sha> (worktree)").
…blish) Removes the job-level `if:` gate that force-SKIPPED every dev-targeted run. With the DEV approuter Credential Store seeded (CONTENT_API_KEY + REBUILD_API_KEY), `gh workflow run rebuild-content.yml -f environment=dev` now publishes DEV content server-side — no local CONTENT_API_KEY paste, matching how PROD/QA already work. PREREQUISITE (one-time, before relying on this): seed the DEV approuter Credential Store with CONTENT_API_KEY + REBUILD_API_KEY (matching the GH secrets) via /admin-ui/#secrets on the DEV approuter. Without it, a dev run 401s at the "Push content to AppRouter" step. repository_dispatch behaviour is unchanged (Determine-environment still defaults absent/unknown env to prod).
…tree build(deploy): allow DEV/QA deploys from a fresh worktree; keep PROD strict
…orkflow ci(rebuild-content): enable keyless DEV content publishing (DRAFT — needs DEV approuter key seed)
…n.css Two cheap render-path wins surfaced by the DEV Lighthouse pass: 1. Minify (| minify) the Hugo-managed fingerprinted stylesheets that were served unminified — homepage.css (index.html) + chroma-light/-dark, print, ui5-overrides, view-transitions, scroll-animations, os-toggle (head.html). Addresses Lighthouse "unminified-css" (~170ms). homepage.css 23,060 -> 14,363 bytes (-38%); the #1767 island min-height reservation survives minification (verified). Left untouched: sap-theme-vars.css and sap-horizon-dark.css (referenced by BARE path from the CAP renderers, so their RelPermalink must not change) and sap-fundamental.css (postcss-built, special .Publish handling). 2. Self-host the fundamental-styles icon.css instead of loading it cross-origin from unpkg.com@0.41.4. Removes a render-blocking third-party request (part of the ~561ms render-blocking-resources), eliminates a prod runtime dependency (an unpkg outage would break every sap-icon-- glyph), and fixes the version drift (unpkg 0.41.4 vs the pinned fundamental-styles@0.41.7 dep). Vendored from node_modules/fundamental-styles/dist/icon.css; fingerprinted + minified by Hugo. Regenerate on dep bump (command in the head.html comment). Verified via a local Hugo --minify build + Playwright load of the built site: unpkg fully removed (0 refs), fundamental-styles-icon.min.<hash>.css serves 200, all target CSS emits as .min.<hash>.css, no CSS/icon console errors. The heavier Lighthouse opportunities (Tier-2 UI5 code-split for ~362 KiB unused JS, oversized homepage images, unused-CSS purge, TrustArc consent banner being the homepage LCP element) are logged as separate issues.
perf(css): minify Hugo-managed CSS + self-host fundamental-styles icon.css
The multi-select/radio quiz options bound `:checked` directly. On UI5 web components attribute *presence* is truthy, so Vue's `:checked="false"` serialized `checked=""` and every option rendered pre-selected while the answers model was empty. Submitting the as-rendered form graded the full option set against the correct set and always returned "Not quite". Route `checked` through the same v-bind presence-guard already used for `disabled` on these elements, merged into one object, so a false value emits no attribute. Adds a real-DOM regression test mirroring the existing disabled-coercion test. Verified live with Playwright on hana-cloud-mission-trial-1 step 4: reproduced all-boxes-checked -> "Not quite"; confirmed against the real ui5-checkbox that no attribute renders unchecked.
…cked-coercion fix(validation): MCQ options render pre-selected -> quiz always grades wrong (#1774)
At the last tutorial of a group, the Next button was missing because all three navigation surfaces computed prev/next strictly within a single group/path/container. A multi-group mission therefore broke the chain at every group boundary (next=null at each group's last tutorial). Treat a mission as one continuous ordered sequence across its groups (ordered by CompletionPath legacyId / missionGroupSeq, which the build and the feed already share): - scripts/parsers/nav-owner.ts: computeCanonicalNav chains prev/next across same-mission containers for baked frontmatter (the reported no-?from= path). Standalone groups never chain. Skips not-present boundary neighbours. - srv/lib/navigator-catalog.js: /build/navigator tutorialMappings stitch path boundaries within a mission; emit nextGroupSlug/prevGroupSlug only at a boundary so the island can carry the correct ?from= across it. - tutorial-group-nav island + shared group-nav-context: carry ?from= to the crossed-into group; in-group neighbours keep the current ?from=. Verified: hana-cloud-mission-trial-4 (last of group 15066, mission 15069) now links Next -> hana-cloud-mission-trial-5 (first of group 15067). Tests: nav-owner unit (cross-group + standalone bounded + absent-skip), navigator-groups integration (cross-path chain + boundary slugs), island happy-dom (boundary ?from= carry both directions), e2e group-nav-ordering.
fix(nav): continuous Next/Prev across groups within a mission (#1775)
#1783) Drop genuinely ancient tutorials from the Featured-missions eligibility set in loadInputs so they can't be PageRank-ranked onto the homepage carousel — quality hygiene, not a fix for any specific tutorial. - Signal: TutorialMeta.reviewedDate (git last-commit date, monotonic-guarded), NOT Tutorials.modifiedAt (churns catalog-wide on every --force rebuild). - NULL reviewedDate / missing meta row -> KEEP (fail-open). Missions have no reviewedDate -> always kept. Most-recent reviewedDate wins across dup rows. - Cutoff via ImsConfig key 'featured.freshness.maxAgeDays'; missing/blank/ non-numeric -> generous default 730d (~24mo, so the 344-day-old #1771 tutorial is NOT dropped). Explicit 0/negative disables the floor. Read/query faults fail open. - Sole chokepoint is the tutorialsBySlug set; pure selectFeaturedTopics unchanged. Unbounded TutorialMeta read (no WHERE-IN) keeps HANA packet-safe. Refs #1771
…ss-cutoff-1783 feat(featured): loose DB-configurable freshness floor for the PageRank carousel (#1783)
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PROD release 1.14.1
Ships the accumulated DEV integration work to
mainfor the PROD blue-green deploy..deploy/mta.yamlbumped1.14.0 → 1.14.1(patch). PROD/main/DEV were all still at 1.14.0; this gives PROD a distinct version string.origin/DEVahead ofmain(features + fixes), e.g. featured-carousel freshness floor (Loose freshness cutoff for the Featured (PageRank) carousel #1783), continuous group Next/Prev (Group Navigation button #1775), MCQ pre-select fix (Multiple Choice Quiz not grading correctly #1774), UI5 bootstrap trim, DEV fresh-worktree deploy guard (build(deploy): allow DEV/QA deploys from a fresh worktree; keep PROD strict #1772), author-page fixes, etc. — plus this bump commit.Use a merge commit (NOT squash). A squash would collapse DEV's history into one commit and make
main/DEVdiverge; the DEV→main flow relies on a real two-parent merge.Post-merge
Deploy to PROD from a fresh primary
mainin blue-green (pauses before swap for green verification), then publish content + content-qa into PROD HANA.