sap-discovery-tutorial-sync
Use when a team maintains a tutorial in SAP Discovery Center format (e.g. SAP-samples/btp-developer-guide-cap/documentation/tutorials) and wants to copy or update the sap-tutorials platform version of
SKILL.md.claude/skills/sap-discovery-tutorial-sync/SKILL.md
GitHubSync a Discovery Center tutorial into the sap-tutorials platform format
This skill converts one tutorial from SAP Discovery Center source format into the sap-tutorials v2-parser format, validates it, and opens a PR. It handles both new imports (first time the tutorial is added to our platform) and updates (syncing upstream changes while preserving our enrichments).
Supported source: default SAP-samples/btp-developer-guide-cap at
documentation/tutorials/<slug>/<slug>.md. The source repo and path are skill
parameters — other teams' repos work too.
Target: sap-tutorials/btp-dev-guidance at tutorials/<slug>.md.
Global Constraints
These invariants apply at every step and override any shortcut:
- Never direct-merge, never push to main. Every change goes through a PR.
git fetch originbefore branching from the target repo.- Branch from fresh
origin/main, not a stale local copy. TUTORIAL_SLUG=<slug>is the env-var for single-tutorial fetch (no--slugflag exists).validate-tutorialsis fail-open (exit 0 even on quarantine) — always inspect.tutorial-cache/quarantine/errors.jsonand the presence ofhugo/content/tutorials/<slug>.md, never the exit code.- OPTION marker syntax:
[OPTION BEGIN [LABEL]]…[OPTION END] - OS picker auto-wires only when the body contains ≥2 distinct canonical OS
labels:
Windows,macOS,Linux,BAS. Preserve source labels exactly — do not invent OS labels that the source did not use. rules.vrcontent goes to the<repo>-Contributionprivate companion repo (consistent with the quiz-file location convention and thetutorial-qa-to-prodskill).- All
npm run fetch-tutorials/validate-tutorialscommands wrap inscripts/quiet-run.shper the repo's token-efficiency rule. - Author frontmatter keys you write:
parser,primary_tag,tags,time,author_name,author_profile; optional:video,auto_validation.
Phase A — Draft locally (always run first)
Step 1: Resolve the source
Set parameters (user may override):
SOURCE_REPO=SAP-samples/btp-developer-guide-cap
SOURCE_PATH=documentation/tutorials
SLUG=<slug>
TARGET_REPO=sap-tutorials/btp-dev-guidance
TARGET_PATH=tutorials
Fetch the source markdown:
SOURCE_URL="https://raw.githubusercontent.com/${SOURCE_REPO}/main/${SOURCE_PATH}/${SLUG}/${SLUG}.md"
curl -fsSL "$SOURCE_URL" -o /tmp/discovery-sync-${SLUG}-source.md
Record the source commit SHA for PR attribution:
SOURCE_SHA=$(gh api "repos/${SOURCE_REPO}/commits?path=${SOURCE_PATH}/${SLUG}/${SLUG}.md&per_page=1" \
--jq '.[0].sha')
echo "Source SHA: $SOURCE_SHA"
Step 2: Detect mode — new import or update
Check whether the tutorial already exists in the target repo. If you have a local
checkout of sap-tutorials/btp-dev-guidance, check the file on disk. Otherwise:
gh api "repos/${TARGET_REPO}/contents/${TARGET_PATH}/${SLUG}.md" \
--jq '.sha' 2>/dev/null && echo "UPDATE" || echo "NEW"
- NEW: no merge step; straight conversion.
- UPDATE: 3-way merge required (Step 7).
Step 3: Run the converter
Run convert-body.mjs against the fetched source and parse the JSON output.
convert-body.mjs is a first-party repo script — use plain node (no -I flag;
-I is not recognised on Node 26 on Windows and is not needed for first-party scripts):
node <tutorials-ims-checkout>/.claude/skills/sap-discovery-tutorial-sync/scripts/convert-body.mjs \
/tmp/discovery-sync-${SLUG}-source.md "${SLUG}" \
> /tmp/discovery-sync-${SLUG}-result.json
Parse {body, flags, images} from the JSON result:
body: the converted markdown body (no frontmatter yet).flags[]: items needing human judgment (nested<details>, dropped H2 wrappers, fences missing language tags, unbalanced markup). Every flag must appear in the final report and in the PR body.images[]: relative filenames of every image referenced in the converted body.
Step 4: Review OPTION label normalization
Inspect the OPTION blocks in body. The converter preserves source labels verbatim.
After conversion, apply Claude judgment:
- Labels
Node.js,Java→ generic technology OPTION tabs (no OS picker). - Labels matching
Windows/Win,macOS/Mac,Linux/Ubuntu,BAS→ these activate the global OS picker only if ≥2 distinct canonical OS names are present. If only one OS label exists, treat as a generic tab. - Combined labels like
Mac and Linux→ preserve verbatim; the parser treats them as a single non-OS tab. Flag this for the author to consider splitting. - Never invent OS labels that did not appear in the source.
Step 5: Fetch images
Create the draft image folder and download every file in images[]:
mkdir -p .sync-draft/${SLUG}/${SLUG}/
for img in <each file from images[]>; do
IMG_URL="https://raw.githubusercontent.com/${SOURCE_REPO}/main/${SOURCE_PATH}/${SLUG}/${img}"
curl -fsSL "$IMG_URL" -o ".sync-draft/${SLUG}/${SLUG}/${img}"
done
Verify every image file was saved successfully. If any image 404s, flag it — a missing image is the most common build failure.
Step 6: Collect frontmatter interactively
Prompt the author for each required field. On update, use the existing values from the target repo as defaults (do not re-ask from scratch).
| Field | Rule |
|---|---|
parser: v2 |
Always set to v2. Never ask. |
primary_tag |
Hard gate. Fetch live taxonomy: GET ${CAP_BASE_URL:-http://localhost:4004}/build/tags → {tags:string[]} (also cached at hugo/data/tags.json). Present valid options. Reject any value not in the taxonomy. See references/taxonomy-check.md for the full resolution procedure. |
tags |
Array. Element 0 must be a level tag (tutorial>beginner, tutorial>intermediate, or tutorial>advanced). Validate all other elements against the same taxonomy source. |
time |
Integer minutes. Suggest an estimate from step count and prose length; author confirms. |
author_name / author_profile |
Prompt. On update, carry forward from the existing file (do not re-ask). |
video |
If the source contains an intro YouTube/Vimeo/openSAP iframe or link, offer to lift it into video: frontmatter. Only hosts on the platform allowlist are valid. |
auto_validation |
Set true only after Step 8 confirms a quiz will be added. Do not ask directly. |
Taxonomy-unavailable fallback: if neither hugo/data/tags.json nor the CAP
service is reachable, warn the author, allow free-text input, and append
# UNVERIFIED TAG to each unverified value in the review report. Never silently trust
unverified taxonomy values.
Step 7: rules.vr prompt
Ask the author two questions:
- Should this tutorial have a quiz (
rules.vr)? - If yes, which type?
[AUTOAUTHOR_ALL]— platform auto-generates questions from step text at build time.- Hand-authored
[VALIDATE_N]…[VALIDATE_END_N]stubs. - Carry-forward existing
rules.vr(update case only — show the current content).
Marker syntax reference:
- Validation block:
[VALIDATE_N]…[VALIDATE_END_N](N = 1-based step number) - Auto-author:
[AUTOAUTHOR_ALL]at the top of the rules.vr file
Write rules.vr to the -Contribution repo working path
(.sync-draft/${SLUG}-Contribution/tutorials/${SLUG}/rules.vr), not the public
target repo. Set auto_validation: true in frontmatter only when a quiz exists.
Step 8: 3-way merge (UPDATE path only)
Skip this step on a new import. On update:
Inputs:
- base = converted source body at the last-synced SHA. Look up
.sync-draft/${SLUG}/.sync-manifest.jsonforlastSyncedSHA; if absent (first update after a manual import), treat as no-base → apply upstream changes heuristically. - theirs = the
bodyfrom Step 3 (current source converted). - ours = current
tutorials/${SLUG}.mdfrom the target repo (body without frontmatter).
Rules:
- Take upstream prose and step changes from theirs.
- Preserve our
frontmatter,rules.vrreferences, OPTION/OS enrichments,<!-- description -->text, and any platform-specific callouts we added.
Render a unified diff of ours vs the merged result and present it for review. STOP here. Do not write any file until the author approves the merge result.
After approval, write the merged body and update .sync-manifest.json:
{ "slug": "<slug>", "lastSyncedSHA": "<SOURCE_SHA>", "syncedAt": "<ISO timestamp>" }
Step 9: Write draft and validate
Assemble the full tutorial markdown with frontmatter:
---
parser: v2
primary_tag: <value>
tags:
- <level tag>
- <additional tags...>
time: <minutes>
author_name: <value>
author_profile: <value>
# optional:
# video: <url>
# auto_validation: true
---
# <title>
<!-- description -->
<one-line description — fill this in>
---
<converted body>
Write to .sync-draft/${SLUG}/${SLUG}.md.
Then run validation — see the Validation section below (Task 6 wires the full details of both the built-in precheck and the real-parser path).
Step 10: Print the Phase A report
The report must cover:
Files written:
.sync-draft/<slug>/<slug>.md
.sync-draft/<slug>/<slug>/<image files...>
.sync-draft/<slug>-Contribution/tutorials/<slug>/rules.vr (if quiz)
Validation verdict: PASS | FAIL | PARSER-UNVERIFIED
(reason if fail; quarantine reason from errors.json if applicable)
Flags from converter (each must be resolved before PR):
[list every entry from flags[]]
Unverified frontmatter fields:
[list any UNVERIFIED TAG fields]
Review checklist:
[ ] <!-- description --> line is filled in (do not leave the placeholder)
[ ] Every converter flag addressed
[ ] OPTION labels correct — no invented OS labels
[ ] Image files all present on disk
[ ] Taxonomy tags verified against /build/tags
[ ] author_name / author_profile correct
[ ] rules.vr decision confirmed
[ ] 3-way merge diff approved (update path only)
── STOP: Human reviews the draft ──
╔══════════════════════════════════════════════════════════╗
║ Phase B runs ONLY after you explicitly confirm. ║
║ Nothing has been pushed. The draft is local only. ║
║ ║
║ Review .sync-draft/<slug>/ and confirm: ║
║ • All checklist items above are resolved ║
║ • Validation passed (or PARSER-UNVERIFIED is accepted) ║
║ • Merge result approved (update path) ║
║ ║
║ Type "confirm" or "yes, open the PR" to proceed. ║
╚══════════════════════════════════════════════════════════╝
Do not proceed to Phase B until the user explicitly says yes. If they request changes, return to the relevant Phase A step, update the draft, re-run validation, and present the report again.
Phase B — Open PR (only on explicit confirmation)
Step 11: Branch from fresh origin/main of the target repo
In a local checkout of sap-tutorials/btp-dev-guidance:
git fetch origin
git checkout -b discovery-sync/${SLUG} origin/main
Never branch from a stale local main. Never reuse an existing branch that may
contain unrelated changes.
Step 12: Copy draft into place
cp .sync-draft/${SLUG}/${SLUG}.md tutorials/${SLUG}.md
cp -r .sync-draft/${SLUG}/${SLUG}/ tutorials/${SLUG}/
# Update the sync manifest if present:
cp .sync-draft/${SLUG}/.sync-manifest.json tutorials/${SLUG}/.sync-manifest.json 2>/dev/null || true
Stage and commit:
git add tutorials/${SLUG}.md tutorials/${SLUG}/
git commit -m "feat(${SLUG}): sync from Discovery Center source
Source: https://github.com/${SOURCE_REPO}/tree/${SOURCE_SHA}
Generated by sap-discovery-tutorial-sync skill."
Step 13: Open the PR
gh pr create \
--title "feat(${SLUG}): sync from Discovery Center (${SOURCE_REPO})" \
--body "$(cat <<'PREOF'
## Summary
Converts \`${SLUG}\` from SAP Discovery Center source into sap-tutorials v2-parser format.
**Source commit:** ${SOURCE_REPO}@${SOURCE_SHA}
**Mode:** NEW IMPORT | UPDATE (pick one)
## Inferred / unverified fields (please verify)
<!-- List every UNVERIFIED TAG and every converter flag[] entry here -->
- `primary_tag`:
- `tags`:
- Converter flags: (list from Phase A report)
## Review checklist
- [ ] `<!-- description -->` line is filled in
- [ ] OPTION labels correct
- [ ] Images all present
- [ ] Taxonomy tags verified
- [ ] rules.vr decision implemented (see companion PR if applicable)
- [ ] 3-way merge result reviewed (update path)
_Generated by the `sap-discovery-tutorial-sync` skill._
PREOF
)" \
--base main \
--head discovery-sync/${SLUG}
Step 14: rules.vr companion PR (if quiz)
In a local checkout of the <repo>-Contribution companion repo:
git fetch origin
git checkout -b discovery-sync/${SLUG} origin/main
cp .sync-draft/${SLUG}-Contribution/tutorials/${SLUG}/rules.vr tutorials/${SLUG}/rules.vr
git add tutorials/${SLUG}/rules.vr
git commit -m "feat(${SLUG}): add rules.vr from Discovery Center sync"
gh pr create \
--title "feat(${SLUG}): rules.vr from Discovery Center sync" \
--body "Companion rules.vr for sap-tutorials/btp-dev-guidance PR <link main PR URL here>.
Generated by the \`sap-discovery-tutorial-sync\` skill." \
--base main \
--head discovery-sync/${SLUG}
Cross-link both PRs: edit the main repo PR to add the companion PR URL, and the companion PR to add the main repo PR URL.
Step 15: Final report
Return to the user:
PR opened: <main PR URL>
Companion PR (rules.vr): <companion PR URL> (or "none — no quiz")
Source: ${SOURCE_REPO}@${SOURCE_SHA}
Draft preserved at: .sync-draft/${SLUG}/
Next: after PR review, the content publish workflow will pick up the merged file.
Never direct-merge. Never push to main.
Validation
Overview (for Phase A Step 9)
Two-stage validation in order. Always run Stage 1 first; Stage 2 requires a local
tutorials-ims checkout.
Stage 1 — Built-in precheck (always, no checkout needed)
convert-body.mjs is a self-contained skill script (.claude/skills/sap-discovery-tutorial-sync/scripts/
in the tutorials-ims checkout). Run it with plain node — no -I flag (the -I
isolation flag is not recognised on Node 26 on Windows and is not needed for
first-party scripts operating on already-fetched local files):
node <tutorials-ims-checkout>/.claude/skills/sap-discovery-tutorial-sync/scripts/convert-body.mjs \
--precheck .sync-draft/${SLUG}/${SLUG}.md \
"${SLUG}" \
.sync-draft/${SLUG}/${SLUG}/
- The third argument is the images directory —
readdirSyncscans it for filenames to check against everyreference in the markdown. - Output: a JSON array of problem strings on stdout. Empty array (
[]) = clean. - Checks performed: balanced
[OPTION BEGIN]/[OPTION END]pairs; every referenced image exists on disk; exactly one#H1; slug is lowercase;parser: v2present in frontmatter.
If the result array is non-empty → STOP. Report every problem string to the
author. Do not proceed to Stage 2 or Phase B until all problems are resolved and
--precheck returns [].
Stage 2 — Real parser (requires tutorials-ims checkout)
Place the draft file and its images into the fetch layout the fetcher expects, then
run the full pipeline against the local checkout (both commands wrapped in
scripts/quiet-run.sh per the repo token-efficiency rule):
# Copy draft into the source layout the fetcher reads from:
mkdir -p scripts/fetch-sources/${SLUG}/
cp .sync-draft/${SLUG}/${SLUG}.md scripts/fetch-sources/${SLUG}/${SLUG}.md
cp -r .sync-draft/${SLUG}/${SLUG}/ scripts/fetch-sources/${SLUG}/${SLUG}/
# Run the fetcher (single-tutorial mode) then the validator:
TUTORIAL_SLUG=${SLUG} CAP_BASE_URL=${CAP_BASE_URL:-http://localhost:4004} \
scripts/quiet-run.sh npm run fetch-tutorials
scripts/quiet-run.sh npm run validate-tutorials
validate-tutorials is fail-open — it exits 0 even when tutorials are
quarantined. Never trust the exit code. After both commands complete, inspect:
hugo/content/tutorials/${SLUG}.md— must exist. Quarantined files are moved OUT of this directory; absence means the tutorial was rejected..tutorial-cache/quarantine/errors.json— if this file exists, check whether it contains an entry for${SLUG}. If it does, the tutorial failed validation; report thereasonfield to the author.
If either check fails → STOP. Report the quarantine reason and do not proceed to Phase B.
Stage 3 — No checkout available
When no tutorials-ims checkout is accessible, run Stage 1 (built-in precheck)
only. Mark the draft "parser-unverified" in the Phase A report and in the PR
body. Never claim the tutorial builds without evidence from Stage 2.