diff --git a/.deploy/mta.yaml b/.deploy/mta.yaml index cafa70cd5..f71d5e5ab 100644 --- a/.deploy/mta.yaml +++ b/.deploy/mta.yaml @@ -10,7 +10,7 @@ ID: tutorials-ims # Bump this on each release you deploy — it's the version shown by `cf mtas` # and in the mtar filename (tutorials-ims_.mtar). Deploy is manual: # `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_.mtar -e ../deploy/.mtaext -f`. -version: 1.40.4 +version: 1.41.0 # Top-level parameters (overridable per-env via deploy/.mtaext). parameters: diff --git a/.github/workflows/rebuild-content-qa.yml b/.github/workflows/rebuild-content-qa.yml index ecd29f990..ead034b04 100644 --- a/.github/workflows/rebuild-content-qa.yml +++ b/.github/workflows/rebuild-content-qa.yml @@ -397,6 +397,39 @@ jobs: - name: Highlight CDS code blocks (Shiki) run: npm run build:highlight -- --dir hugo/public-qa + # [#2885] Pre-flight asset gate — the same guard rebuild-content.yml + # already runs (its "Guard - approuter serves referenced CSS + islands" + # step), now on the QA path — the surface stranded on 2026-10-22. Probes + # every /css + hashed island /js the rendered QA HTML references against + # the deployed QA approuter and FAILS before the HANA publish on any 404, + # so QA BLOBs are never poisoned with HTML the approuter can't dress. QA + # refreshes ONLY on its own tutorial-qa-updated dispatch (it does not + # self-heal like prod), so this gate is the only thing standing between a + # fingerprint-skewed deploy and unstyled /tutorials-qa/* pages. + - name: Guard - approuter serves referenced CSS + islands (QA) + # Share the publish step's skip condition (line ~405): an author + # source-error (#2384) leaves no rendered page for the slug, and + # check-approuter-assets.cjs resolvePages() die()s (exit 1) on a missing + # page — without this `if:`, a benign author typo would become a red + # infra run and file a false failure issue. + if: ${{ steps.source_errors.outputs.bad_slug == '' }} + env: + APPROUTER_URL: ${{ secrets.APPROUTER_URL_QA }} + # Match the publish step's PUBLISH_SLUG / PUBLISH_SLUGS expressions + # EXACTLY (lines 424/431) so the probe scope tracks the publish scope: + # a force-publish run ships the WHOLE catalog, so it must NOT pin the + # guard to one slug (that would gate 1 and strand ~1400 — the inverse + # of the bug this closes). check-approuter-assets.cjs reads both. + PUBLISH_SLUG: ${{ ((! inputs.force-publish) || inputs.allow-revert) && (github.event.client_payload.slug || inputs.slug) || '' }} + PUBLISH_SLUGS: ${{ (! inputs.force-publish) && github.event.client_payload.slugs || '' }} + run: | + SLUGS_ARG="" + [ -n "$PUBLISH_SLUGS" ] && SLUGS_ARG="--slugs $PUBLISH_SLUGS" + node scripts/check-approuter-assets.cjs \ + --approuter-url "$APPROUTER_URL" \ + --hugo-dir hugo/public-qa \ + --check-islands $SLUGS_ARG + - name: Publish QA content to HANA # Skip when the ONLY targeted slug failed to fetch (author source error, # #2384): nothing rendered for it, so a slug-scoped publish would throw diff --git a/.github/workflows/rebuild-content.yml b/.github/workflows/rebuild-content.yml index 48beaa632..68dc3dbc7 100644 --- a/.github/workflows/rebuild-content.yml +++ b/.github/workflows/rebuild-content.yml @@ -513,6 +513,10 @@ jobs: CONTENT_API_KEY: ${{ secrets.CONTENT_API_KEY }} TUTORIAL_SLUG: ${{ inputs.slug || steps.mode.outputs.dispatch_slug }} TUTORIAL_SLUGS: ${{ inputs.slugs || steps.mode.outputs.dispatch_slugs }} + # [retired-slug-noop] Lets fetch-tutorials distinguish a human manual + # dispatch (typo → fail-fast) from an auto-dispatched admin-write / + # scheduled run (a retired-slug filter is "nothing to publish", exit 0). + TRIGGER_SOURCE: ${{ inputs.trigger-source }} # [#208/#275/#312] AI-quiz authoring. Always-on post-#312 # (2026-06-27 — soak window completed clean). fetch-tutorials.ts # wakes the AI generator on tutorials carrying [AUTOAUTHOR_*] @@ -809,6 +813,21 @@ jobs: env: CAP_BASE_URL: ${{ steps.srv.outputs.srv_url }} + # Generated CSS (#2896 follow-up). sap-fundamental.css (build:css, postcss) + # and fundamental-styles-icon.css (build:icon-subset) are build outputs, no + # longer committed — they are .gitignored. This workflow drives /tmp/hugo + # directly rather than `npm run build:all`, so — exactly like the feed + # fetchers above — it must regenerate them explicitly BEFORE the Hugo build, + # or Hugo fingerprints a missing/stale file and the published content + # references a /css/.css the deployed approuter doesn't serve (the + # approuter-asset guard then blocks publish). build:css + build:icon-subset + # produce byte-identical output to the deploy's build:all, so both publish + # paths agree on the fingerprint. + - name: Build fingerprinted CSS (sap-fundamental + icon subset) + run: | + npm run build:icon-subset + npm run build:css + - name: Build Hugo site run: /tmp/hugo --source hugo --minify diff --git a/.gitignore b/.gitignore index ca2e51dc0..65b08b277 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,16 @@ test/a11y/axe-results.md srv-qa/lib/parsers.bundle.mjs .scratch/ +# Generated CSS — build outputs, NOT hand-edited sources (#2896 follow-up). +# sap-fundamental.css is emitted by `build:css` (postcss from sap-fundamental.src.css); +# fundamental-styles-icon.css by `build:icon-subset` (scripts/build-icon-subset.cjs). +# Committing them let the generated bytes drift from source: a deploy regenerates +# them (new fingerprint) while rebuild-content.yml used the stale committed copy +# (old fingerprint) → the approuter-asset guard blocked publish. Both build paths +# now regenerate from source; the outputs must never be committed again. +hugo/assets/css/sap-fundamental.css +hugo/assets/css/fundamental-styles-icon.css + # VitePress documentation site docs/.vitepress/dist/ docs/.vitepress/cache/ diff --git a/CLAUDE.md b/CLAUDE.md index 168cd58d1..24f83be8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,6 +136,7 @@ The load-bearing few. **Full detail for every relocated item → [tutorials-ims- - **Fresh worktree: run `npm run setup` after `npm install`** — global npmrc `ignore-scripts=true` skips native builds; without it `hugo-apps/node_modules` is empty and `better-sqlite3` won't build (tests hang / fail resolving `@mediapipe/tasks-vision`). - **`ignore-scripts=true` silences all `pre*`/`post*` hooks** — local `build:all` does NOT fire `postbuild:apps`; island-manifest + `build:page-fallback` are wired as explicit `build:all` steps. Rely on the hook and merged JS/CSS ships dead (unhashed island paths) → "not deployed" despite a green deploy. → gotchas.md "Build artifacts & lifecycle hooks". +- **Deploy refuses a dirty Hugo working tree (#2886)** — uncommitted edits under `hugo/assets/**` or `hugo/layouts/**` make Hugo fingerprint dirty CSS/JS, so the approuter serves a hash the committed-source content pipeline never bakes → 404 on every tutorial (the 2026-10-21 PROD-down). `deploy-mta.cjs` **Step 0.5** `die(1)`s on any tracked-modified build input (branch/SHA guards miss it — a dirty tree keeps the same HEAD SHA). Fix: commit or `git stash push -- `. → gotchas.md "Deploy bakes dirty working-tree CSS". - **`hugo/content/tutorials/` is entirely generated** — never edit; `fetch-tutorials` overwrites. Edit `scripts/parsers/` or source repos. - **Group/Mission completions are rollup-derived (#1934)** — `srv/lib/completion-rollup.js` recomputes parents after any TUTORIAL/PUZZLE/CHECKPOINT/PETOBERFEST completion; upserts on `(user_ID, taskLegacyId, taskType)`; NOT a `content-store.js` dep. → gotchas.md "Completions rollup". - **Never run `publish-content` from a workstation** — use `gh workflow run rebuild-content.yml`; workstation publishes skip CI validation. diff --git a/app/admin/pats/webapp/ext/MintDialog.fragment.xml b/app/admin/pats/webapp/ext/MintDialog.fragment.xml index b4a9671b3..2f4019573 100644 --- a/app/admin/pats/webapp/ext/MintDialog.fragment.xml +++ b/app/admin/pats/webapp/ext/MintDialog.fragment.xml @@ -21,7 +21,15 @@ placeholder="{i18n>mint.fieldScopesPlaceholder}"> + + + +