Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .deploy/mta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>.mtar). Deploy is manual:
# `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_<version>.mtar -e ../deploy/<env>.mtaext -f`.
version: 1.41.3
version: 1.41.4

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
28 changes: 15 additions & 13 deletions .github/workflows/rebuild-content-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -381,21 +381,23 @@ jobs:
- name: Build island fingerprint manifest
run: npm run build:island-manifest

# Generated CSS (#2896 follow-up, mirrors rebuild-content.yml). Commit
# ac6b7e39f stopped committing sap-fundamental.css (build:css, postcss)
# and fundamental-styles-icon.css (build:icon-subset) — both are now
# .gitignored build outputs. head.html does `resources.Get | minify` on
# the icon subset, so on a clean CI checkout where the file is absent the
# QA Hugo build fails at head.html:27 ("wrong type ...; got
# resource.Resource"). This workflow drives /tmp/hugo directly rather than
# `npm run build:all`, so — like the feed fetchers above — it must
# regenerate them BEFORE the Hugo build. build:css + build:icon-subset
# produce byte-identical output to the deploy's build:all, so QA and prod
# agree on the fingerprint. (QA regression #2912.)
- name: Build fingerprinted CSS (sap-fundamental + icon subset)
# [#2920] CSS on the REBUILD path must FOLLOW the deployed approuter (mirrors
# rebuild-content.yml). sap-fundamental.css is the one fingerprinted stylesheet
# NOT Hugo-minified (head.html:73), so its /css/<hash>.css URL == sha256 of the
# on-disk bytes; building it from HEAD source here made QA bake a hash the
# DEPLOYED approuter doesn't serve whenever CSS source changed since the last
# deploy → froze QA publishing. QA is a feature of PRODUCTION: /tutorials-qa/*
# is dressed by the MAIN (prod) approuter (xs-app.json → srv-qa-api), so the
# sync fetches from the PROD approuter (secrets.APPROUTER_URL_PROD), matching
# the asset guard below (#2923).
# fundamental-styles-icon.css stays a source build: Hugo `| minify`s it so it
# self-heals. Fail-open to build:css from source if the approuter is unreachable.
- name: Sync sap-fundamental CSS from deployed approuter (+ icon subset)
run: |
npm run build:icon-subset
npm run build:css
node scripts/sync-approuter-css.cjs --approuter-url "$APPROUTER_URL"
env:
APPROUTER_URL: ${{ secrets.APPROUTER_URL_PROD }}

- name: Build QA Hugo site
# Hugo was downloaded to /tmp/hugo (line 44). The build:qa npm script
Expand Down
33 changes: 21 additions & 12 deletions .github/workflows/rebuild-content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -813,20 +813,29 @@ 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/<hash>.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)
# [#2920] CSS on the REBUILD path must FOLLOW the deployed approuter, not
# race ahead of it. sap-fundamental.css (build:css, postcss) is the ONE
# fingerprinted stylesheet that is NOT Hugo-minified (head.html:73 is a bare
# `| fingerprint`), so its /css/<hash>.css URL == sha256 of the on-disk bytes.
# Post-#1659 a content rebuild never ships CSS to the approuter (static only
# changes on a full MTA deploy), so building sap-fundamental.css from HEAD
# source here made the baked HTML reference a hash the DEPLOYED approuter does
# not serve whenever CSS source changed since the last deploy → the
# approuter-asset guard (below) blocked publish, freezing all content
# publishing (the recurring 2026-10 saga, #2920). Fix: fetch the bytes the
# approuter CURRENTLY serves so Hugo fingerprints to the SAME hash → guard
# always passes, pages always styled, regardless of deploy lag. The DEPLOY
# path (build:all → build:css from source) is unchanged and still rotates the
# hash on deploy; this makes content-publish follow it. Fail-open: a brand-new
# env with nothing deployed falls back to build:css from source.
# fundamental-styles-icon.css stays a source build: Hugo `| minify`s it
# (head.html:27), so it self-heals from whatever on-disk bytes are present.
- name: Sync sap-fundamental CSS from deployed approuter (+ icon subset)
run: |
npm run build:icon-subset
npm run build:css
node scripts/sync-approuter-css.cjs --approuter-url "$APPROUTER_URL"
env:
APPROUTER_URL: ${{ steps.url.outputs.approuter_url }}

- name: Build Hugo site
run: /tmp/hugo --source hugo --minify
Expand Down
67 changes: 66 additions & 1 deletion hugo/data/whats_new.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-25T13:42:26.548Z",
"generatedAt": "2026-10-26T08:50:18.626Z",
"repos": [
{
"key": "tutorials-ims",
Expand All @@ -8,6 +8,71 @@
}
],
"entries": [
{
"id": "tutorials-ims#2928",
"repo": "tutorials-ims",
"label": "Developer Portal",
"number": 2928,
"title": "fix(tutorials): tag/level chips link to /tutorial-navigator/ not /tutorials/",
"summary": "Tag and experience-level chips on tutorial pages now correctly link to the Tutorial Navigator, resolving 404 errors when filtering by tag or level from a tutorial.",
"category": "Fix",
"mergedAt": "2026-10-25T17:15:57Z",
"week": "2026-W43",
"weekStart": "2026-10-19",
"url": "https://github.com/sap-tutorials/tutorials-ims/pull/2928"
},
{
"id": "tutorials-ims#2927",
"repo": "tutorials-ims",
"label": "Developer Portal",
"number": 2927,
"title": "fix(ci): remove duplicate env: key in QA rebuild workflow",
"summary": "Fixed a YAML validation error in the QA content rebuild workflow that prevented any QA rebuilds from starting.",
"category": "Maintenance",
"mergedAt": "2026-10-25T14:46:34Z",
"week": "2026-W43",
"weekStart": "2026-10-19",
"url": "https://github.com/sap-tutorials/tutorials-ims/pull/2927"
},
{
"id": "tutorials-ims#2926",
"repo": "tutorials-ims",
"label": "Developer Portal",
"number": 2926,
"title": "fix(ci): prune legacy assets/-prefixed entries from asset retention",
"summary": "Cleared stale legacy CSS entries from the asset-retention list, stopping the post-deploy asset smoke gate from falsely flagging healthy deployments.",
"category": "Fix",
"mergedAt": "2026-10-25T14:13:15Z",
"week": "2026-W43",
"weekStart": "2026-10-19",
"url": "https://github.com/sap-tutorials/tutorials-ims/pull/2926"
},
{
"id": "tutorials-ims#2924",
"repo": "tutorials-ims",
"label": "Developer Portal",
"number": 2924,
"title": "Content rebuild no longer freezes after a CSS source change",
"summary": "Content rebuilds now fetch the stylesheet bytes the live approuter serves rather than rebuilding from HEAD, so a CSS source change between deploys no longer blocks tutorial publishing.",
"category": "Fix",
"mergedAt": "2026-10-25T13:54:22Z",
"week": "2026-W43",
"weekStart": "2026-10-19",
"url": "https://github.com/sap-tutorials/tutorials-ims/pull/2924"
},
{
"id": "tutorials-ims#2925",
"repo": "tutorials-ims",
"label": "Developer Portal",
"number": 2925,
"title": "release v1.41.3 DEV to main promotion",
"summary": "Patch release 1.41.3 promoting CI fixes from DEV to main.",
"category": "Maintenance",
"mergedAt": "2026-10-25T13:45:20Z",
"week": "2026-W43",
"weekStart": "2026-10-19",
"url": "https://github.com/sap-tutorials/tutorials-ims/pull/2925"
},
{
"id": "tutorials-ims#2923",
"repo": "tutorials-ims",
Expand Down
4 changes: 2 additions & 2 deletions hugo/layouts/partials/tutorial-meta.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
{{ with .Params.displayTags }}
<span class="meta-tags">
<svg class="meta-icon-svg" viewBox="0 0 16 16" width="14" height="14"><path d="M2 2h5.5l6.5 6.5-5.5 5.5L2 7.5V2zm3 1.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z" fill="currentColor" opacity="0.6"/></svg>
{{/* #2112 — Overview tags now deep-link to /tutorials/?tag=<slug> for parity with the
{{/* #2112 — Overview tags now deep-link to /tutorial-navigator/?tag=<slug> for parity with the
header chips (u1-object-page.html). Zip displayTags with displayTagSlugs; fall back
to a plain span when a slug is missing. "License" stays label-matched + skipped. */}}
{{ $slugs := $.Params.displayTagSlugs }}
{{ range $i, $label := . }}{{ if ne $label "License" }}{{ $slug := "" }}{{ if and $slugs (lt $i (len $slugs)) }}{{ $slug = index $slugs $i }}{{ end }}{{ if $slug }}<a class="tutorial-tag tutorial-tag--link" href="{{ "/tutorials/" | relURL }}?tag={{ $slug | urlquery }}" aria-label="Filter tutorials by {{ $label }}">{{ $label }}</a>{{ else }}<span class="tutorial-tag">{{ $label }}</span>{{ end }}{{ end }}{{ end }}
{{ range $i, $label := . }}{{ if ne $label "License" }}{{ $slug := "" }}{{ if and $slugs (lt $i (len $slugs)) }}{{ $slug = index $slugs $i }}{{ end }}{{ if $slug }}<a class="tutorial-tag tutorial-tag--link" href="{{ "/tutorial-navigator/" | relURL }}?tag={{ $slug | urlquery }}" aria-label="Filter tutorials by {{ $label }}">{{ $label }}</a>{{ else }}<span class="tutorial-tag">{{ $label }}</span>{{ end }}{{ end }}{{ end }}
</span>
{{ end }}
{{ if in .Params.tags "tutorial>license" }}
Expand Down
10 changes: 5 additions & 5 deletions hugo/layouts/tutorials/u1-object-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ <h1 class="op-header__title tutorial-hero-title">{{ .Title }}{{ if in .Params.ta
</div>
{{ with .Params.description }}<p class="op-header__description">{{ . }}</p>{{ end }}
<div class="op-header__chips">
{{ with .Params.level }}<a class="op-chip op-chip--link" href="{{ "/tutorials/" | relURL }}?level={{ . | lower | urlquery }}" aria-label="Filter tutorials by experience: {{ . }}"><span class="op-chip__icon">🎓</span>{{ . }}</a>{{ end }}
{{ with .Params.experienceLevel }}<a class="op-chip op-chip--link" href="{{ "/tutorials/" | relURL }}?level={{ . | lower | urlquery }}" aria-label="Filter tutorials by experience: {{ . }}"><span class="op-chip__icon">🎓</span>{{ . }}</a>{{ end }}
{{ with .Params.level }}<a class="op-chip op-chip--link" href="{{ "/tutorial-navigator/" | relURL }}?level={{ . | lower | urlquery }}" aria-label="Filter tutorials by experience: {{ . }}"><span class="op-chip__icon">🎓</span>{{ . }}</a>{{ end }}
{{ with .Params.experienceLevel }}<a class="op-chip op-chip--link" href="{{ "/tutorial-navigator/" | relURL }}?level={{ . | lower | urlquery }}" aria-label="Filter tutorials by experience: {{ . }}"><span class="op-chip__icon">🎓</span>{{ . }}</a>{{ end }}
{{ with .Params.time }}<span class="op-chip"><span class="op-chip__icon">⏱</span>{{ . }} min</span>{{ end }}
{{/* Prefer human-readable displayTags zipped with displayTagSlugs; fall back to raw primaryTag.
#161 — chips are <a> anchors that deep-link to /tutorials/?tag=<slug>. The "License" label
#161 — chips are <a> anchors that deep-link to /tutorial-navigator/?tag=<slug>. The "License" label
is intentionally label-matched (not slug-matched) to preserve existing chip-strip behaviour;
see docs/superpowers/specs/2026-06-01-clickable-tutorial-tags-design.md. */}}
{{ if .Params.displayTags }}
Expand All @@ -266,14 +266,14 @@ <h1 class="op-header__title tutorial-hero-title">{{ .Title }}{{ if in .Params.ta
{{ $slug := "" }}
{{ if and $slugs (lt $i (len $slugs)) }}{{ $slug = index $slugs $i }}{{ end }}
{{ if $slug }}
<a class="op-chip op-chip--tag op-chip--link" href="{{ "/tutorials/" | relURL }}?tag={{ $slug | urlquery }}" aria-label="Filter tutorials by {{ $label }}">{{ $label }}</a>
<a class="op-chip op-chip--tag op-chip--link" href="{{ "/tutorial-navigator/" | relURL }}?tag={{ $slug | urlquery }}" aria-label="Filter tutorials by {{ $label }}">{{ $label }}</a>
{{ else }}
<span class="op-chip op-chip--tag">{{ $label }}</span>
{{ end }}
{{ end }}
{{ end }}
{{ else if .Params.primaryTag }}
<a class="op-chip op-chip--tag op-chip--link" href="{{ "/tutorials/" | relURL }}?tag={{ .Params.primaryTag | urlquery }}" aria-label="Filter tutorials by {{ .Params.primaryTag }}">{{ .Params.primaryTag }}</a>
<a class="op-chip op-chip--tag op-chip--link" href="{{ "/tutorial-navigator/" | relURL }}?tag={{ .Params.primaryTag | urlquery }}" aria-label="Filter tutorials by {{ .Params.primaryTag }}">{{ .Params.primaryTag }}</a>
{{ end }}
{{/* No progress ring for step-less tutorials (e.g. Devtoberfest validation
tutorials, #2127) — nothing to complete, so "0/0" would be misleading. */}}
Expand Down
2 changes: 1 addition & 1 deletion mta-mcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ _schema-version: 3.3.0
# This file lives at repo root → path: gen/srv-mcp (root-relative, no ../).
# before-all runs from ROOT (no cd ..) — mbt uses the descriptor's directory as cwd.
ID: tutorials-mcp
version: 1.1.3
version: 1.1.4

# Destinations are subaccount-level, so DEV and PROD (sharing one subaccount)
# need DISTINCT destination names. `dest-main-srv` defaults to the PROD name;
Expand Down
9 changes: 9 additions & 0 deletions scripts/lib/asset-retention.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ function mergeRetention({ currentFiles, retainedManifest, nowMs, windowMs }) {
const toDownload = [];
for (const e of prior) {
if (current.has(e.file)) continue; // already in this build
// Legacy carry-forward prune (2026-10-25). Old Vite layouts emitted island
// CSS as separate `assets/<name>-<hash>.css` files; the current pipeline
// injects island CSS via JS (vite-plugin-css-injected-by-js) and emits NO
// `assets/`-prefixed bundles. Such an entry can never match a current file
// and its carry-forward download (`/css/assets/…` or `/js/assets/…`) always
// 404s, so it lingers forever as a prior-build entry and the post-deploy
// asset-retention smoke gate flaps on it. Drop these permanently — nothing
// the current build produces or references lives under `assets/`.
if (e.file.startsWith('assets/')) continue;
const last = lastOf(e);
const inPriorBuild = hasLast(e) && priorBuildMs !== null && e.lastSeenMs === priorBuildMs;
// Keep if it was current in the immediately-prior build (unconditional), or
Expand Down
Loading
Loading