Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
3c17258
fix(admin-health): make URL Health tile preload-buildable, not CDN-de…
jung-thomas Oct 15, 2026
d492347
Merge pull request #2796 from sap-tutorials/worktree-fix-health-ui5-p…
jung-thomas Oct 15, 2026
66d914f
fix(admin-health): replace illegal new-Date expression binding with a…
jung-thomas Oct 15, 2026
fe5f004
Merge pull request #2798 from sap-tutorials/worktree-fix-health-view-…
jung-thomas Oct 15, 2026
522164c
fix(admin-ui): wire Rebuild-tutorial button to its handler + feedback
jung-thomas Oct 15, 2026
8f2116d
Merge pull request #2799 from sap-tutorials/fix/admin-rebuild-button-…
jung-thomas Oct 15, 2026
bc52a94
fix(admin-ui): resolve Rebuild-button press to a plain .js module
jung-thomas Oct 16, 2026
ffa1e30
Merge pull request #2804 from sap-tutorials/fix/rebuild-button-press-…
jung-thomas Oct 16, 2026
a8c3973
fix(rebuild): keep multi-tutorial pushes slug-targeted instead of full
jung-thomas Oct 16, 2026
6e18369
test(rebuild): accept generalized !isScoped render-concepts gate (#2801)
jung-thomas Oct 16, 2026
dbaa3ba
Merge pull request #2801 from sap-tutorials/fix/rebuild-multislug-dis…
jung-thomas Oct 16, 2026
501ce6c
chore(release): 1.39.0
jung-thomas Oct 15, 2026
3b85384
chore(release): 1.39.1
jung-thomas Oct 15, 2026
a85bd7d
docs(publish): deep-dive findings for #2802 publish-to-HANA throughput
jung-thomas Oct 16, 2026
4d9eaa8
docs(publish): design spec for #2802 throughput optimizations
jung-thomas Oct 16, 2026
d6fede1
docs(publish): implementation plan for #2802 throughput optimizations
jung-thomas Oct 16, 2026
da12480
feat(content): add bodyHash + conceptPhaseToken columns (#2802)
jung-thomas Oct 16, 2026
0c888bb
feat(publish): compute per-concept bodyHash in render phase (#2802)
jung-thomas Oct 16, 2026
ffa7e7e
feat(publish): add conceptInputDigest change signal (#2802)
jung-thomas Oct 16, 2026
5528c45
feat(publish): phase-level concept skip + shell-aware guard (#2802)
jung-thomas Oct 16, 2026
6d552a8
test(publish): cover organic shellHash, NULL-token, fail-open paths (…
jung-thomas Oct 16, 2026
ea1de03
feat(publish): persist bodyHash + conceptPhaseToken (#2802)
jung-thomas Oct 16, 2026
5cfd318
perf(publish): batch upsertBodyTexts + upsertBranchSpecs reads/writes…
jung-thomas Oct 16, 2026
7f5931f
perf(publish): batch upsertTutorialMetadata (#2802)
jung-thomas Oct 16, 2026
e6b4660
fix(publish): wire insertedTutorialIds guard for failed tutorial-inse…
jung-thomas Oct 16, 2026
2aaf442
perf(publish): batch linkTutorialAuthorship reads (#2802)
jung-thomas Oct 16, 2026
aeaacea
perf(publish): fold pipeline-token into the hashes GET (#2802)
jung-thomas Oct 16, 2026
7052d68
fix(publish): wire conceptPhaseToken through commit path + test/clean…
jung-thomas Oct 16, 2026
24f05cb
fix(ci): regenerate CDS build artifacts + fix publish-client header r…
jung-thomas Oct 16, 2026
068944d
Merge pull request #2806 from sap-tutorials/worktree-publish-perf-2802
jung-thomas Oct 16, 2026
95c9c6e
fix(docs): register publish-performance-spike-2802 in sidebar
jung-thomas Oct 16, 2026
4e42039
Merge pull request #2807 from sap-tutorials/worktree-fix-docs-sidebar…
jung-thomas Oct 16, 2026
29a31e5
fix(dispatch): emit client_payload.slugs for multi-tutorial pushes (#…
jung-thomas Oct 16, 2026
48da7d8
fix(#2805): backfill-images.ts accepts comma-list --slug/--slugs
jung-thomas Oct 16, 2026
123c6d4
fix(#2805): image-refresh step honours plural slugs input (no whole-c…
jung-thomas Oct 16, 2026
bb7dd36
docs(#2805): record image-refresh root cause + fix in spike Finding 3
jung-thomas Oct 16, 2026
45c2c0a
docs(#2805): design + implementation plan for plural-slugs image fix
jung-thomas Oct 16, 2026
020ea2f
Merge pull request #2809 from sap-tutorials/worktree-fix-2805-plural-…
jung-thomas Oct 16, 2026
4e9674b
Merge pull request #2808 from sap-tutorials/fix/multislug-dispatch-2803
jung-thomas Oct 16, 2026
3860b71
test(backfill-images): make slug-filter test hermetic to kill CI fork…
jung-thomas Oct 16, 2026
fdceca1
Merge pull request #2812 from sap-tutorials/worktree-flake-backfill-i…
jung-thomas Oct 16, 2026
b44f773
fix(scheduler): reaper PipelineLog cross-check for HANA-HA-failover t…
jung-thomas Oct 17, 2026
7115c48
test(backfill-images): fix the real signature regression #2812 misdia…
jung-thomas Oct 17, 2026
f8856e7
Merge pull request #2816 from sap-tutorials/worktree-flake-audit-2813
jung-thomas Oct 17, 2026
2aa44d9
Merge pull request #2815 from sap-tutorials/worktree-reaper-pipelinel…
jung-thomas Oct 17, 2026
91515fc
feat(admin-health): sort + filter on URL Status table
jung-thomas Oct 17, 2026
04ccf1c
Merge pull request #2817 from sap-tutorials/feat/admin-health-sort-fi…
jung-thomas Oct 17, 2026
8b9bcb8
docs: design for tutorial variables (#2811)
jung-thomas Oct 17, 2026
b7d9c93
docs(plan): refine fenced-token approach to sentinel after Hugo spike…
jung-thomas Oct 17, 2026
82aa471
feat(parser): declare + validate + wrap tutorial variables (prose/inl…
jung-thomas Oct 17, 2026
64ef54c
feat(hugo): wrap variable tokens inside fenced code blocks
jung-thomas Oct 17, 2026
0086e8c
fix(hugo): use lazy quantifier so adjacent sentinels don't merge
jung-thomas Oct 17, 2026
726aaa7
feat(hugo): emit variables config + island mount in tutorial layout
jung-thomas Oct 17, 2026
b3b22d4
feat(island): tutorial-variables live substitution + default filler
jung-thomas Oct 17, 2026
959e848
feat(island): persistence + mission carry-over for tutorial variables
jung-thomas Oct 17, 2026
6bf6299
feat(cap): persist tutorial variables to HANA + admin projection (#2811)
jung-thomas Oct 17, 2026
a1656c2
feat(admin,e2e): variables admin list + live-substitution e2e (#2811)
jung-thomas Oct 17, 2026
a16e9f6
fix: inline-code variable substitution via sentinel + client expansio…
jung-thomas Oct 17, 2026
64eb417
fix(ci): mark slug-canonical lookup + commit CDS build artifacts for …
jung-thomas Oct 17, 2026
c3d8dc8
Merge pull request #2818 from sap-tutorials/worktree-tutorial-variabl…
jung-thomas Oct 17, 2026
0ffbda0
docs(#2800): design for official-API video transcripts
jung-thomas Oct 17, 2026
d11125e
docs(#2800): implementation plan for official-API video transcripts
jung-thomas Oct 17, 2026
96b2718
feat(#2800): YouTube OAuth token minting for official transcript API
jung-thomas Oct 17, 2026
0e9b1b8
feat(#2800): channel gate + video snippet fetch
jung-thomas Oct 17, 2026
248005e
feat(#2800): caption list/select/download + SBV parse
jung-thomas Oct 17, 2026
145cf1c
feat(#2800): three-tier official transcript orchestrator
jung-thomas Oct 17, 2026
fbd4150
feat(#2800): build uses official transcript API in-process + loud dro…
jung-thomas Oct 17, 2026
f20c309
feat(#2800): learner-facing source notice under video quizzes
jung-thomas Oct 17, 2026
cee725d
chore(#2800): surface empty-quiz drops in rebuild summary + document …
jung-thomas Oct 17, 2026
2dabbd3
fix(#2800): correct youtube module path in secrets-tracking doc
jung-thomas Oct 17, 2026
a374454
fix(#2800): add [ai-author][EMPTY-QUIZ] marker to all video-quiz drop…
jung-thomas Oct 17, 2026
cf0c74a
Merge pull request #2821 from sap-tutorials/autoauthor-video-oauth
jung-thomas Oct 17, 2026
e82393c
fix(options): honest missing-OS placeholder + left-align figures afte…
jung-thomas Oct 17, 2026
ccf4c56
Merge pull request #2822 from sap-tutorials/worktree-options-renderin…
jung-thomas Oct 17, 2026
1b0855e
fix(rebuild-content): bash case syntax error in multi-slug validation
jung-thomas Oct 17, 2026
fb0837a
Merge pull request #2824 from sap-tutorials/fix/rebuild-case-syntax
jung-thomas Oct 17, 2026
22ee9af
chore(release): 1.40.0
jung-thomas Oct 17, 2026
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.39.1
version: 1.40.0

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
40 changes: 26 additions & 14 deletions .github/workflows/notify-qa.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,36 @@ jobs:
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Determine changed slug
- name: Determine changed slug(s)
id: slug
run: |
changed=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} \
| awk -F/ '/^tutorials\//{print $2}' | sort -u)
count=$(echo "$changed" | wc -l)
# Only emit a slug when EXACTLY one tutorial changed AND it matches the
# strict slug charset (whole-value POSIX case match); anything else →
# empty slug → rebuild-content-qa.yml runs a full rebuild. Mirrors
# docs/authors/tutorial-repo-dispatch.yml.
if [ "$count" = "1" ] && [ -n "$changed" ]; then
case "$changed" in
*[!a-z0-9-]* | -* ) echo "slug=" >> "$GITHUB_OUTPUT" ;;
*) echo "slug=$changed" >> "$GITHUB_OUTPUT" ;;
esac
else
echo "slug=" >> "$GITHUB_OUTPUT"
count=$(echo "$changed" | grep -c . || true)
# Emit a scoped payload so the rebuild stays slug-targeted (~2 min)
# instead of demoting to a full-corpus rebuild (~10 min):
# - exactly 1 changed tutorial → slug=<slug>
# - 2..CAP changed tutorials → slugs=<csv>
# - 0, >CAP, or any slug failing the strict charset → both empty
# (full rebuild — a huge push is effectively full anyway, and an
# invalid slug must never reach the dispatch payload).
# Mirrors the receiver validation in rebuild-content.yml's mode step.
SLUG_CAP=20
slug=""
slugs=""
valid=1
for s in $changed; do
case "$s" in *[!a-z0-9-]* | -* ) valid=0 ;; esac
done
if [ "$valid" = "1" ] && [ "$count" -ge 1 ] && [ "$count" -le "$SLUG_CAP" ]; then
if [ "$count" = "1" ]; then
slug="$changed"
else
slugs=$(echo "$changed" | paste -sd, -)
fi
fi
echo "slug=$slug" >> "$GITHUB_OUTPUT"
echo "slugs=$slugs" >> "$GITHUB_OUTPUT"
# GitHub App token (preferred). Activates when this Contribution repo has
# USE_GITHUB_APP=true and the TUTORIALS_APP_* secrets populated. Falls
# back to the classic TUTORIALS_POC_DISPATCH_TOKEN PAT while unset.
Expand All @@ -62,4 +74,4 @@ jobs:
token: ${{ steps.app-token.outputs.token || secrets.TUTORIALS_POC_DISPATCH_TOKEN }}
repository: sap-tutorials/tutorials-ims
event-type: tutorial-qa-updated
client-payload: '{"repo": "${{ github.repository }}", "slug": "${{ steps.slug.outputs.slug }}", "sha": "${{ github.sha }}"}'
client-payload: '{"repo": "${{ github.repository }}", "slug": "${{ steps.slug.outputs.slug }}", "slugs": "${{ steps.slug.outputs.slugs }}", "sha": "${{ github.sha }}"}'
13 changes: 13 additions & 0 deletions .github/workflows/rebuild-content-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ jobs:
# source repos via ONLY_CONTRIBUTION_REPOS=true).
CAP_BASE_URL: ${{ secrets.CAP_SRV_URL_DEV }}
TUTORIAL_SLUG: ${{ github.event.client_payload.slug || inputs.slug }}
# Multi-slug (N-tutorial push) companion to TUTORIAL_SLUG. The notify
# template emits a validated comma-separated client_payload.slugs when
# 2..CAP tutorials changed; fetch-tutorials parses TUTORIAL_SLUG +
# TUTORIAL_SLUGS together. Passed via env (never the command line) so
# the payload value can't be re-parsed by the shell.
TUTORIAL_SLUGS: ${{ github.event.client_payload.slugs }}
# [#208/#275/#312] AI-quiz authoring on the QA channel. Always-on
# post-#312 (2026-06-27). The QA channel is gated by Tutorial.Author
# XSUAA scope so AI questions land somewhere safe to preview before
Expand Down Expand Up @@ -402,6 +408,13 @@ jobs:
# the slug and re-seeds the whole catalog (--force). EXCEPTION: an allow-revert
# run keeps the slug set so its `--force` stays scoped to the one tutorial.
PUBLISH_SLUG: ${{ ((! inputs.force-publish) || inputs.allow-revert) && (github.event.client_payload.slug || inputs.slug) || '' }}
# Multi-slug (N-tutorial push) companion. Scope the publish to exactly
# the changed slugs when the notify template sent a validated
# client_payload.slugs — publish-content.ts reads PUBLISH_SLUGS as the
# --slugs fallback (filterToSlugs). Empty on force-publish (full
# re-seed) and on single-slug/allow-revert runs. Payload-only (no
# inputs.slugs — the QA workflow_dispatch has no slugs input).
PUBLISH_SLUGS: ${{ (! inputs.force-publish) && github.event.client_payload.slugs || '' }}

# #1373 — synthetic watchdog. The publish step reporting success is not
# proof rows landed: the render-concepts 404 that hid for days (#1372)
Expand Down
88 changes: 84 additions & 4 deletions .github/workflows/rebuild-content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ jobs:
# tutorial slug (#1154 follow-up). Untrusted payload — validated below
# against a strict slug charset before use.
PAYLOAD_SLUG: ${{ github.event.client_payload.slug }}
# Multi-slug dispatch: a source-repo push that changed N>1 tutorials
# sends a comma-separated `slugs` payload so the rebuild stays
# slug-targeted (O(N)) instead of silently demoting to a ~10-min full
# rebuild. Untrusted payload — each entry validated below against the
# same strict charset before use.
PAYLOAD_SLUGS: ${{ github.event.client_payload.slugs }}
# #2349 — tutorials deleted upstream (comma-separated). Untrusted
# payload; validated below against the same strict charset before use.
PAYLOAD_DELETED_SLUGS: ${{ github.event.client_payload.deleted_slugs }}
Expand All @@ -261,11 +267,39 @@ jobs:
# junk, embedded newline) falls through to full — safe default.
if [ "$EVENT" = "repository_dispatch" ]; then
case "$PAYLOAD_SLUG" in
*[!a-z0-9-]* | -* | '' ) : ;; # invalid → no-op, stays full
*[!a-z0-9-]* | -* | '' ) : ;; # invalid → try slugs below
*) EFFECTIVE='slug-targeted'
REASON="auto-inferred (repository_dispatch slug=$PAYLOAD_SLUG)"
echo "dispatch_slug=$PAYLOAD_SLUG" >> "$GITHUB_OUTPUT" ;;
esac

# Multi-slug path: a push that changed N>1 tutorials sends a
# comma-separated `slugs` payload. Validate EVERY entry against the
# strict charset (reject newlines outright); if all are clean, stay
# slug-targeted and pass the list through as dispatch_slugs so the
# fetch + publish scope to exactly those slugs instead of demoting
# to a ~10-min full rebuild. Only consulted when the singular slug
# above didn't already resolve.
if [ "$EFFECTIVE" != 'slug-targeted' ] && [ -n "$PAYLOAD_SLUGS" ]; then
case "$PAYLOAD_SLUGS" in
*[!a-z0-9,-]* ) : ;; # any char outside [a-z0-9,-] (incl newline) → reject whole list
*,,* | ,* | *, ) : ;; # empty element / leading / trailing comma → reject
'' ) : ;;
*) EFFECTIVE='slug-targeted'
REASON="auto-inferred (repository_dispatch slugs=$PAYLOAD_SLUGS)"
echo "dispatch_slugs=$PAYLOAD_SLUGS" >> "$GITHUB_OUTPUT" ;;
esac
fi

# Loud fallback: a tutorial-updated dispatch that carried neither a
# usable slug nor usable slugs silently became a full-corpus rebuild
# (~10 min, render-concepts over ~10.8k concepts) for what is almost
# always a 1-few-slug change. Warn so the operator/dispatcher sees
# it — the fix is to populate client_payload.slug / .slugs upstream
# (notify-qa.yml.template + per-repo push workflows).
if [ "$EFFECTIVE" != 'slug-targeted' ]; then
echo "::warning title=Rebuild demoted to full::repository_dispatch '${{ github.event.action }}' arrived with no usable client_payload.slug/slugs (slug='$PAYLOAD_SLUG' slugs='$PAYLOAD_SLUGS') — running a full-corpus rebuild (~10 min) instead of a slug-targeted one (~2 min). The dispatcher should send the changed slug(s)."
fi
fi

# Issue #orphan-purge — reject purge-orphans=true with anything
Expand Down Expand Up @@ -468,13 +502,17 @@ jobs:
# (24h TTL) and pulls fresh from /build/catalog. Use after fixing data
# the catalog reads from (publish flag, slug format, etc.) so the
# rebuild reflects the change in the same run.
run: npm run fetch-tutorials -- ${{ inputs.force-cap-refetch == true && '--force-cap' || '' }}
# Tee to /tmp/fetch-tutorials.log so the EMPTY-QUIZ summary step below
# can grep it without re-running the build (#2800).
run: |
npm run fetch-tutorials -- ${{ inputs.force-cap-refetch == true && '--force-cap' || '' }} 2>&1 | tee /tmp/fetch-tutorials.log
exit "${PIPESTATUS[0]}"
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.TUTORIALS_GITHUB_TOKEN }}
CAP_BASE_URL: ${{ steps.srv.outputs.srv_url }}
CONTENT_API_KEY: ${{ secrets.CONTENT_API_KEY }}
TUTORIAL_SLUG: ${{ inputs.slug || steps.mode.outputs.dispatch_slug }}
TUTORIAL_SLUGS: ${{ inputs.slugs }}
TUTORIAL_SLUGS: ${{ inputs.slugs || steps.mode.outputs.dispatch_slugs }}
# [#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_*]
Expand Down Expand Up @@ -503,6 +541,37 @@ jobs:
# runs, but the sidecar is still written so the SAVE step warms it.
CONTENT_CACHE_FAST_PATH: ${{ steps.mode.outputs.content_cache_enabled }}

# [#2800] Surface any AUTOAUTHOR_VIDEO tutorials whose quiz was dropped
# because no transcript was available (source='none'). The marker
# [ai-author][EMPTY-QUIZ] is emitted by expand-ai-authored.ts via
# console.warn and captured in /tmp/fetch-tutorials.log by the tee above.
# Non-failing (warn-and-continue by design): a missing transcript is
# never a build-blocker, but the rebuild operator should know about it.
# Runs even when the Fetch step failed (if: always()) so the log is
# always drained; the grep is a no-op when the log is absent.
- name: Surface dropped video quizzes (EMPTY-QUIZ)
if: ${{ always() && steps.mode.outputs.effective_mode != 'catalog-only' }}
run: |
LOG=/tmp/fetch-tutorials.log
if [ ! -f "$LOG" ]; then
exit 0
fi
MATCHES=$(grep -F '[ai-author][EMPTY-QUIZ]' "$LOG" || true)
if [ -z "$MATCHES" ]; then
exit 0
fi
{
echo "### ⚠️ AUTOAUTHOR_VIDEO quizzes dropped (empty)"
echo ""
echo "The following steps had no transcript and their video quizzes were dropped:"
echo ""
echo '```'
echo "$MATCHES"
echo '```'
echo ""
echo "_To fix: add captions to the YouTube video, or ensure the description is ≥ 50 substantive words._"
} >> "$GITHUB_STEP_SUMMARY"

# Workstream C: SAVE the generated content tree + sidecar so the NEXT
# slug-targeted hotfix can restore and reuse it. Runs on full (to PRIME a
# clean freshly-regenerated tree) AND slug-targeted (to persist the tree
Expand Down Expand Up @@ -912,6 +981,13 @@ jobs:
# other mode → whole-catalog delta, unchanged. TUTORIAL_SLUG is the
# already-resolved slug (inputs.slug || dispatch_slug).
PUBLISH_SLUG: ${{ steps.mode.outputs.effective_mode == 'slug-targeted' && (inputs.slug || steps.mode.outputs.dispatch_slug) || '' }}
# Multi-slug variant of PUBLISH_SLUG. In slug-targeted mode, scope the
# publish to exactly the changed slugs (publish-content.ts reads
# PUBLISH_SLUGS as the --slugs fallback and keeps those N files via
# filterToSlugs) instead of a whole-catalog delta. Empty in every
# other mode. TUTORIAL_SLUGS is the already-resolved list
# (inputs.slugs || dispatch_slugs).
PUBLISH_SLUGS: ${{ steps.mode.outputs.effective_mode == 'slug-targeted' && (inputs.slugs || steps.mode.outputs.dispatch_slugs) || '' }}

# #1373 — synthetic watchdog. A green publish step is not proof rows
# landed (the #1372 render-concepts 404 aborted AFTER an earlier
Expand Down Expand Up @@ -976,6 +1052,9 @@ jobs:
# force-publish is set — that is exactly the "push my stubborn change
# through" case). POST /content/image dedups on image bytes; changed
# bytes are re-stored, unchanged report 'unchanged'.
# #2805: REFRESH_SLUG (inputs.slug / dispatch_slug) and REFRESH_SLUGS
# (inputs.slugs / dispatch_slugs) are both passed so a plural-slugs
# dispatch scopes the warm to those N tutorials, not the whole catalog.
#
# 2. full: re-warm the whole catalog (warm-images input, default true).
# No full-catalog warm existed before #2346; the prior comment claiming
Expand All @@ -995,7 +1074,8 @@ jobs:
CONTENT_API_KEY: ${{ steps.env.outputs.target == 'qa' && secrets.CONTENT_API_KEY_QA || secrets.CONTENT_API_KEY }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || secrets.TUTORIALS_GITHUB_TOKEN }}
REFRESH_SLUG: ${{ inputs.slug || steps.mode.outputs.dispatch_slug }}
run: npx tsx scripts/backfill-images.ts --slug "$REFRESH_SLUG"
REFRESH_SLUGS: ${{ inputs.slugs || steps.mode.outputs.dispatch_slugs }}
run: npx tsx scripts/backfill-images.ts --slug "$REFRESH_SLUG" --slugs "$REFRESH_SLUGS"

- name: Refresh tutorial images (full catalog)
if: ${{ steps.publish.outcome == 'success' && steps.mode.outputs.effective_mode == 'full' && inputs.warm-images != false }}
Expand Down
37 changes: 31 additions & 6 deletions app/admin-annotations.cds
Original file line number Diff line number Diff line change
Expand Up @@ -702,12 +702,14 @@ annotate AdminService.Tutorials with @UI: {
{ Value: redirectUrl, Label: 'Redirect URL (external)' }
]},
Identification: [
{
$Type : 'UI.DataFieldForAction',
Label : 'Rebuild this tutorial',
Action : 'AdminService.rebuildContent',
![@UI.Importance]: #High,
},
// "Rebuild this tutorial" is wired as a manifest custom header action
// (content.header.actions.RebuildTutorialAction → SourceMarkdownHandler.
// onRebuildTutorial) rather than a @UI.DataFieldForAction. FE V4 fires a
// DataFieldForAction through its generic action path, bypassing the
// controller-extension handler that owns the confirm dialog + success/
// error feedback + submit debounce — so the annotation form rendered a
// button that POSTed silently with no user feedback. The manifest custom
// action (AskJoule pattern) routes the press straight to the handler.
{ $Type: 'UI.DataFieldForAction', Label: 'Check freshness', Action: 'AdminService.checkFreshness', ![@UI.Importance]: #High }
]
};
Expand Down Expand Up @@ -744,6 +746,28 @@ annotate AdminService.TutorialContributors with @UI.LineItem: [
{ Value: role }
];

// TutorialVariables — read-only child list on the Tutorials Object Page (#2811).
// Surfaced as a plain ReferenceFacet so admins can inspect the declared
// variables without any edit capability (the entity is @readonly on AdminService).
annotate AdminService.TutorialVariables with {
varName @Common.Label: 'Name';
varLabel @Common.Label: 'Label';
varType @Common.Label: 'Type';
isRequired @Common.Label: 'Required';
};

annotate AdminService.TutorialVariables with @UI: {
LineItem: [
{ Value: varName, @UI.Importance: #High },
{ Value: varLabel },
{ Value: varType },
{ Value: isRequired }
],
PresentationVariant: {
SortOrder: [ { Property: sortOrder, Descending: false } ]
}
};

annotate AdminService.Steps with {
stepOrder @Common.Label: 'Step #';
title @Common.Label: 'Title';
Expand Down Expand Up @@ -993,6 +1017,7 @@ annotate AdminService.Tutorials with @UI: {
{ $Type: 'UI.ReferenceFacet', Label: 'Categories', ID: 'CategoriesFacet', Target: 'categories/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Steps', ID: 'StepsFacet', Target: 'steps/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Contributors', ID: 'ContributorsFacet', Target: 'contributors/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Variables', ID: 'VariablesFacet', Target: 'variables/@UI.LineItem' },
{ $Type: 'UI.ReferenceFacet', Label: 'Completion Stats', ID: 'CompletionStatsFacet',
Target: 'completionStats/@UI.FieldGroup#Stats' },
{ $Type: 'UI.ReferenceFacet', Label: 'AI-Graded Validation', ID: 'ValidationSpecsFacet',
Expand Down
Loading
Loading