Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
152 commits
Select commit Hold shift + click to select a range
950a9a3
fix(app-space): show correct event for anonymous visitors, no default…
jung-thomas Sep 15, 2026
d32633d
Merge pull request #2315 from sap-tutorials/worktree-fix-2314-appspac…
jung-thomas Sep 15, 2026
56e6624
fix(display-app): pin websocket-only transport (#2306)
jung-thomas Sep 15, 2026
5ff07e5
Merge pull request #2316 from sap-tutorials/worktree-fix-2306-display…
jung-thomas Sep 15, 2026
22f0327
fix(provenance): read signing key credstore-first via resolveSecret (…
jung-thomas Sep 15, 2026
98b1c23
fix(ord): serve ORD discovery endpoints at runtime (#2307)
jung-thomas Sep 15, 2026
04456d2
Merge pull request #2317 from sap-tutorials/fix/provenance-signing-ke…
jung-thomas Sep 15, 2026
e26dd87
Merge pull request #2318 from sap-tutorials/fix/ord-discovery-runtime…
jung-thomas Sep 15, 2026
fe97bd6
feat(skills): add tutorial-qa-to-prod author skill (#2310)
jung-thomas Sep 15, 2026
aade771
Merge pull request #2319 from sap-tutorials/feat/qa-to-prod-author-sk…
jung-thomas Sep 15, 2026
241876a
feat(kg): Devtoberfest sessions as first-class KG + semantic-search n…
jung-thomas Sep 15, 2026
fd0c088
feat(kg): surface Devtoberfest sessions in the explore-graph viz (#2311)
jung-thomas Sep 15, 2026
6d21c88
Merge pull request #2321 from sap-tutorials/worktree-devtoberfest-kg-…
jung-thomas Sep 15, 2026
9e1da7b
feat(teched): RainFocus fetcher + data model + seed + /build/teched f…
jung-thomas Sep 15, 2026
16cfb36
Merge pull request #2322 from sap-tutorials/feat/teched-foundation-2312
jung-thomas Sep 15, 2026
e77fb37
docs(teched): document TechEd sessions subsystem (#2312, Unit 11)
jung-thomas Sep 15, 2026
b99e6b5
feat(homepage): surface TechEd 2026 sessions in the events band (#2312)
jung-thomas Sep 15, 2026
b01ad08
feat(kg): project SAP TechEd sessions into the RDF/SPARQL graph (#231…
jung-thomas Sep 15, 2026
04f1807
feat(teched): bidirectional TechEd <-> Devtoberfest session cross-lin…
jung-thomas Sep 15, 2026
e5b7aef
feat(search): add search_teched MCP tool on SearchService (#2312)
jung-thomas Sep 15, 2026
987265b
feat(teched): teched-sessions-grid Vue island (#2312)
jung-thomas Sep 15, 2026
fa4c0c7
feat(kg): add TechEd sessions to HANA property graph (#2312)
jung-thomas Sep 15, 2026
c6ce822
feat(teched): add /teched/ top-level page (Unit 7, #2312)
jung-thomas Sep 15, 2026
816d255
feat(teched): TechEd sessions in semantic search + embedding backfill…
jung-thomas Sep 15, 2026
45de5ee
feat(teched): correct RainFocus fetcher to real /api/sessions contrac…
jung-thomas Sep 15, 2026
00d1bc2
Merge pull request #2332 from sap-tutorials/feat/teched-cron-jobs-2312
jung-thomas Sep 15, 2026
044a58a
Merge pull request #2331 from sap-tutorials/feat/teched-embeddings-se…
jung-thomas Sep 15, 2026
7f437b2
Merge pull request #2330 from sap-tutorials/feat/teched-page-2312
jung-thomas Sep 15, 2026
56e8886
Merge pull request #2329 from sap-tutorials/feat/teched-property-grap…
jung-thomas Sep 15, 2026
4b3bf19
Merge pull request #2328 from sap-tutorials/feat/teched-island-2312
jung-thomas Sep 15, 2026
91b8b35
Merge pull request #2327 from sap-tutorials/feat/teched-mcp-search-2312
jung-thomas Sep 15, 2026
878e6c8
Merge pull request #2324 from sap-tutorials/feat/teched-homepage-2312
jung-thomas Sep 15, 2026
4b29647
Merge pull request #2323 from sap-tutorials/docs/teched-2312
jung-thomas Sep 15, 2026
4163b34
Merge origin/DEV into feat/teched-devtoberfest-crosslink-2312
jung-thomas Sep 15, 2026
d22ed1f
docs(teched): register sidebar entry + reconcile RainFocus contract (…
jung-thomas Sep 15, 2026
3507734
Merge origin/DEV into feat/teched-kg-projection-2312
jung-thomas Sep 15, 2026
89cb0d9
Merge pull request #2325 from sap-tutorials/feat/teched-kg-projection…
jung-thomas Sep 15, 2026
9f73a82
Merge pull request #2326 from sap-tutorials/feat/teched-devtoberfest-…
jung-thomas Sep 15, 2026
6020da0
Merge pull request #2334 from sap-tutorials/docs/teched-reconcile-2312
jung-thomas Sep 15, 2026
0968980
fix(teched): don't prune session speaker links on incomplete seed inp…
jung-thomas Sep 15, 2026
d294af7
fix(teched): bound public /build/teched feed (#2312)
jung-thomas Sep 15, 2026
5559992
fix(kg): reconcile community-event/devtoberfest/teched extractors to …
jung-thomas Sep 15, 2026
2c3b0d1
Merge pull request #2335 from sap-tutorials/fix/seed-core-prune-guard…
jung-thomas Sep 15, 2026
1b5bb77
Merge pull request #2336 from sap-tutorials/fix/build-teched-bounded-…
jung-thomas Sep 15, 2026
508582a
Merge pull request #2337 from sap-tutorials/fix/concept-extractor-cal…
jung-thomas Sep 15, 2026
bc01472
fix(teched): correctness fixes (pagination partial-page skip, MCP ven…
jung-thomas Sep 15, 2026
43c358d
Merge pull request #2338 from sap-tutorials/fix/teched-fetcher-search…
jung-thomas Sep 15, 2026
95d7c96
fix(freshness): fault-isolate DevtoberfestSessions embedding arm
jung-thomas Sep 15, 2026
0e4079f
fix(teched): escalate TOTAL RainFocus fetch failure loudly
jung-thomas Sep 15, 2026
1441fdd
perf: parallelize independent ingestion SELECTs and batch teched abst…
jung-thomas Sep 15, 2026
4ac97c8
Merge pull request #2339 from sap-tutorials/fix/freshness-devtoberfes…
jung-thomas Sep 15, 2026
d0394cf
Merge pull request #2340 from sap-tutorials/fix/teched-fetch-escalate…
jung-thomas Sep 15, 2026
7904424
Merge remote-tracking branch 'origin/DEV' into tmp-merge-2341
jung-thomas Sep 15, 2026
c05d494
Merge pull request #2341 from sap-tutorials/perf/parallelize-ingestio…
jung-thomas Sep 16, 2026
ca38d49
fix(teched): repair /teched page 404 (blob id/shape + approuter allow…
jung-thomas Sep 16, 2026
3faeb56
Merge pull request #2342 from sap-tutorials/fix/teched-404-blob-and-a…
jung-thomas Sep 16, 2026
fa532e4
feat(teched): add SAP TechEd to Community nav after Devtoberfest
jung-thomas Sep 16, 2026
a72b8d1
Merge pull request #2343 from sap-tutorials/feat/teched-nav-2312
jung-thomas Sep 16, 2026
86e1918
fix(smoke): decouple catalog-freshness SEO checks from the deploy gate
jung-thomas Sep 16, 2026
b786228
Merge pull request #2347 from sap-tutorials/fix/smoke-decouple-conten…
jung-thomas Sep 16, 2026
9aeaaa1
fix(images): process in-place image edits on tutorial rebuilds (#2346)
jung-thomas Sep 16, 2026
a44c6ea
feat(content): unpublish tutorials deleted upstream (#2349)
jung-thomas Sep 16, 2026
de9fe69
Merge pull request #2350 from sap-tutorials/worktree-fix-image-update…
jung-thomas Sep 16, 2026
ac7a031
Merge pull request #2351 from sap-tutorials/worktree-tutorial-deletio…
jung-thomas Sep 16, 2026
5e73dba
fix(quiz): relax AI free-text grader + let partial unlock progression…
jung-thomas Sep 16, 2026
508d8a8
chore: remove empty file
ogulcan-k Sep 16, 2026
9eda255
Merge pull request #2352 from sap-tutorials/worktree-fix-2348-quiz-st…
jung-thomas Sep 16, 2026
f1a1a50
Merge pull request #2353 from ogulcan-k/DEV
jung-thomas Sep 16, 2026
c5626bf
fix(feedback-email): working dashboard link + per-item tutorial/admin…
jung-thomas Sep 16, 2026
29eafd8
refactor(teched): factor loadTechEdFeed out of /build/teched; add ses…
jung-thomas Sep 16, 2026
8f8ff22
Merge pull request #2357 from sap-tutorials/worktree-feedback-email-l…
jung-thomas Sep 16, 2026
51823ab
feat(teched): collapse Berlin/Virtual sections into one unified grid …
jung-thomas Sep 16, 2026
5277eab
feat(advocates): attach speaker sessions to /api/advocates/:slug
jung-thomas Sep 16, 2026
83d910e
feat(devtoberfest): render related TechEd sessions in session detail …
jung-thomas Sep 16, 2026
9558861
feat(teched): add calendar view island — Week+Day only (#2355 unit 7)
jung-thomas Sep 16, 2026
9d5f79d
feat(authors): attach speaker sessions to author pages at build time
jung-thomas Sep 16, 2026
61028d0
feat(devtoberfest): add breadcrumb back to main page on sub-pages
jung-thomas Sep 16, 2026
d67c00b
Merge pull request #2363 from sap-tutorials/worktree-devtoberfest-bre…
jung-thomas Sep 16, 2026
4f1cff8
feat(ui): session cards on advocate + author pages; convert advocate …
jung-thomas Sep 16, 2026
7bcc8d4
fix(teched-calendar): null-guard loadFeed, remove emoji, add null-gua…
jung-thomas Sep 16, 2026
9811a27
docs(teched,advocates): document session cards + speaker matcher (#2354)
jung-thomas Sep 16, 2026
cc373aa
Merge pull request #2368 from sap-tutorials/worktree-session-cards-2354
jung-thomas Sep 16, 2026
543b69b
Merge pull request #2359 from sap-tutorials/feat/teched-2355-unit1-re…
jung-thomas Sep 16, 2026
d8104eb
Merge pull request #2360 from sap-tutorials/feat/teched-2355-unit5-dt…
jung-thomas Sep 16, 2026
6c54599
Merge pull request #2361 from sap-tutorials/feat/teched-2355-unit7-ca…
jung-thomas Sep 16, 2026
ec4bdf3
feat(teched): color-code track badges + legend (#2355 Unit 3)
jung-thomas Sep 16, 2026
a441907
feat(teched): add schedule table view island + Hugo page (issue #2355…
jung-thomas Sep 16, 2026
e90c656
chore(css): rebuild compiled CSS after rebase onto DEV (Unit 7 teched…
jung-thomas Sep 16, 2026
c258a3f
Merge pull request #2364 from sap-tutorials/feat/teched-2355-unit3-tr…
jung-thomas Sep 16, 2026
f394edf
Merge pull request #2365 from sap-tutorials/feat/teched-2355-unit6-sc…
jung-thomas Sep 16, 2026
e5df28b
feat(teched): render related Devtoberfest sessions on TechEd cards (#…
jung-thomas Sep 16, 2026
ede876d
Merge pull request #2366 from sap-tutorials/feat/teched-2355-unit4-re…
jung-thomas Sep 16, 2026
59e9595
feat(teched): clickable cards open detail side panel (#2355 unit 2)
jung-thomas Sep 16, 2026
2687f3d
Merge pull request #2367 from sap-tutorials/feat/teched-2355-unit2-de…
jung-thomas Sep 16, 2026
5cb6682
feat(teched,dtf): link speakers to author pages (#2355 unit 8)
jung-thomas Sep 16, 2026
f79aad3
Merge pull request #2369 from sap-tutorials/feat/teched-2355-unit8-sp…
jung-thomas Sep 16, 2026
8ab1f26
docs(research): MCP Apps + json-render spike (#2362) + json-render POC
jung-thomas Sep 16, 2026
2ec39c1
Merge pull request #2371 from sap-tutorials/worktree-generative-ui-re…
jung-thomas Sep 16, 2026
a53333f
feat(ai-quiz): generate + grade quizzes from a YouTube video transcri…
jung-thomas Sep 16, 2026
8f500a7
feat(events): add location, eventUrl, attendanceMode to Events (#2370)
jung-thomas Sep 16, 2026
74d641d
docs(ai-quiz): document [AUTOAUTHOR_VIDEO_*] for authors + engineers …
jung-thomas Sep 16, 2026
32f4ef4
Merge pull request #2372 from sap-tutorials/worktree-ai-quiz-video-2345
jung-thomas Sep 16, 2026
96b8345
chore(cds): stage regenerated csn.json for Events fields (#2370)
jung-thomas Sep 16, 2026
7f7154e
Merge pull request #2373 from sap-tutorials/feat/event-entity-extensi…
jung-thomas Sep 16, 2026
d0d21b7
docs: register generative-ui-research page in sidebar (fixes Deploy D…
jung-thomas Sep 16, 2026
8277f7e
Merge pull request #2376 from sap-tutorials/fix/docs-sidebar-generati…
jung-thomas Sep 16, 2026
9d55d12
fix(db): add migration=5 for Events location/eventUrl/attendanceMode …
jung-thomas Sep 16, 2026
24d221b
Merge pull request #2377 from sap-tutorials/fix/events-migration-2370-v2
jung-thomas Sep 16, 2026
33813d6
fix(teched): guard loadFeed against Hugo jsonify-of-nil 'null' blob (…
jung-thomas Sep 16, 2026
a2af782
fix(ci): run fetch-teched before Hugo build in rebuild-content (#2312)
jung-thomas Sep 16, 2026
abedcb8
Merge pull request #2378 from sap-tutorials/fix/teched-loadfeed-null-…
jung-thomas Sep 16, 2026
95bc50a
Merge pull request #2379 from sap-tutorials/fix/teched-rebuild-fetch-…
jung-thomas Sep 16, 2026
f00a4e7
feat(teched): Add-to-calendar (.ics + Google/Outlook) for TechEd sess…
jung-thomas Sep 16, 2026
c7ed6a3
Merge pull request #2380 from sap-tutorials/feat/teched-calendar-ics
jung-thomas Sep 16, 2026
0c10137
fix(qa-rebuild): skip missing/misnamed source tutorials instead of fa…
jung-thomas Sep 17, 2026
cd16629
Merge pull request #2388 from sap-tutorials/worktree-harden-qa-rebuil…
jung-thomas Sep 17, 2026
6664a1e
fix(qa-rebuild): owner-qualify source repo + guard author login (#238…
jung-thomas Sep 17, 2026
8043195
Merge pull request #2391 from sap-tutorials/worktree-fix-qa-source-er…
jung-thomas Sep 17, 2026
d65421f
feat(teched): extract subnav partial and fix calendar/schedule consis…
jung-thomas Sep 17, 2026
84ca71d
feat(teched): consolidate track color legend into filter chips
jung-thomas Sep 17, 2026
9a81267
feat(teched): speaker author-links + bio tooltip in detail pane (#239…
jung-thomas Sep 17, 2026
8ba671b
fix(teched): construct sap.com session URL from sourceId+venue (#2392)
jung-thomas Sep 17, 2026
a159f81
Merge pull request #2394 from sap-tutorials/feat/teched-subnav-2392
jung-thomas Sep 17, 2026
a569a33
Merge pull request #2395 from sap-tutorials/feat/teched-track-chips-2392
jung-thomas Sep 17, 2026
62633b7
Merge pull request #2396 from sap-tutorials/feat/teched-speaker-detai…
jung-thomas Sep 17, 2026
fc79ffd
Merge pull request #2398 from sap-tutorials/feat/teched-session-url-2392
jung-thomas Sep 17, 2026
58bb80a
feat(teched): sort speaker dropdown by last name, display "Last, Firs…
jung-thomas Sep 17, 2026
dee90c1
feat(teched): list all-day activities (issue #2392 item 6)
jung-thomas Sep 17, 2026
2a55526
Merge pull request #2397 from sap-tutorials/feat/teched-speaker-sort-…
jung-thomas Sep 17, 2026
0c43c3c
Merge pull request #2400 from sap-tutorials/feat/teched-allday-activi…
jung-thomas Sep 17, 2026
4d7604c
feat(teched): add Community Clubhouse session filter (#2392)
jung-thomas Sep 17, 2026
ac2b91b
Merge pull request #2399 from sap-tutorials/feat/teched-clubhouse-fil…
jung-thomas Sep 17, 2026
8f32b1b
fix(teched): make /build/teched feed query HANA-compatible
jung-thomas Sep 17, 2026
5c07d42
Merge pull request #2402 from sap-tutorials/worktree-teched-feed-hana…
jung-thomas Sep 17, 2026
35e05b1
fix(teched): dedup cross-venue speakers + serve schedule/calendar fro…
jung-thomas Sep 17, 2026
88aa4fd
Merge pull request #2403 from sap-tutorials/worktree-teched-schedule-…
jung-thomas Sep 17, 2026
d968c62
fix(teched): surface all 19 all-day activities + speaker photos in sc…
jung-thomas Sep 17, 2026
326cedd
Merge pull request #2404 from sap-tutorials/fix/teched-allday-speaker…
jung-thomas Sep 18, 2026
84d6deb
fix(teched): correct approuter target for /teched/schedule + /teched/…
jung-thomas Sep 18, 2026
52c490d
Merge pull request #2406 from sap-tutorials/fix/teched-subpage-approu…
jung-thomas Sep 18, 2026
cd545eb
fix(teched): allow static.rainfocus.com in CSP img-src for speaker ph…
jung-thomas Sep 18, 2026
77c8aca
fix(teched): link speakers to their developer-advocate page (#2392)
jung-thomas Sep 18, 2026
387da00
Merge pull request #2407 from sap-tutorials/fix/csp-rainfocus-img-2392
jung-thomas Sep 18, 2026
0bb3560
Merge pull request #2408 from sap-tutorials/fix/teched-advocate-links…
jung-thomas Sep 18, 2026
f90b549
fix(devtoberfest): carry current page as returnTo on PointsBanner Sig…
jung-thomas Sep 18, 2026
70e8e97
Merge pull request #2410 from sap-tutorials/worktree-fix-2409-devtobe…
jung-thomas Sep 18, 2026
f582604
fix(advocates): rename adv-* CSS namespace so ad blockers stop hiding…
jung-thomas Sep 18, 2026
5384350
Merge pull request #2412 from sap-tutorials/worktree-fix-2405-adv-adb…
jung-thomas Sep 18, 2026
be07075
docs(authors): list restored AI-authoring meta-tutorials (#2374)
jung-thomas Sep 18, 2026
9a5bd79
chore(release): 1.27.0 — What's New digest + version bump
jung-thomas Sep 18, 2026
015b692
Merge pull request #2413 from sap-tutorials/docs/meta-tutorials-ai-fe…
jung-thomas Sep 18, 2026
a6ac306
Merge pull request #2414 from sap-tutorials/worktree-release-1.27.0
jung-thomas Sep 18, 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
175 changes: 175 additions & 0 deletions .claude/skills/tutorial-qa-to-prod/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
---
name: tutorial-qa-to-prod
description: Use when a tutorial author has previewed a tutorial in the QA channel (/tutorials-qa/) and wants to promote it to PROD — copies the tutorial markdown from the private sap-tutorials/<product>-Contribution repo (QA source) to the canonical public sap-tutorials/Tutorials repo (PROD source) and opens a PR in each, cross-linked. Triggers on "promote this tutorial to prod", "copy from QA to PROD", "publish my QA tutorial", "move <slug> from Contribution to Tutorials".
---

# Promote a tutorial from QA (Contribution) to PROD (canonical Tutorials)

An author edits and previews a tutorial in its product-specific
**`sap-tutorials/<product>-Contribution`** repo, served on the platform's QA
channel at `/tutorials-qa/<slug>`. Promoting it to PROD means landing the same
markdown in the **canonical public repo `sap-tutorials/Tutorials`** at the same
`tutorials/<slug>/<slug>.md` path. Merging the `Tutorials` PR is what the PROD
content rebuild consumes — the platform auto-discovers `Tutorials` content and
republishes to the PROD HANA BLOBs.

The two repos share **no git fork network**, so one cross-repo PR is impossible.
This skill copies the file once and opens **one PR in each repo**, cross-linked.

## Non-negotiable facts (verified 2026-09-15)

- **Host is `github.com`, NOT `github.tools.sap`.** Always `export GH_HOST=github.com`.
- **Default branches DIFFER per repo — never hardcode.**
- Canonical `sap-tutorials/Tutorials` (PROD) → **`master`**.
- `sap-tutorials/<product>-Contribution` (QA) → **`main`**.
- Resolve each repo's default with the API (Step 0); do not assume.
- **`*-Contribution` repos are private** → `raw.githubusercontent.com` 404s.
Verify pushed content via the authenticated contents API, not raw curl.
- **Path parity.** Both repos use `tutorials/<slug>/<slug>.md`. Same slug → same path.
- **First promotion is a CREATE, not an update.** A QA-only tutorial does NOT
exist in `Tutorials` yet (contents API returns 404). Handle create vs update.
- **No `tutorials-ims` code change is needed.** The platform auto-discovers any
org repo with a `tutorials/` folder; merging the `Tutorials` PR triggers PROD.

## Workflow

Work through the GitHub contents API (`gh api`) — do NOT clone `Tutorials`; it is
huge. Use `$CLAUDE_JOB_DIR/tmp` or a scratch dir for the file. Set
`export GH_HOST=github.com` first.

### Step 0 — Resolve inputs and per-repo default branches

Identify the QA repo (`sap-tutorials/<product>-Contribution`), the slug, and the
path `tutorials/<slug>/<slug>.md`. Then resolve BOTH default branches — do not
assume:

```bash
export GH_HOST=github.com
CONTRIB=sap-tutorials/<product>-Contribution
MAIN=sap-tutorials/Tutorials
PATHREL="tutorials/<slug>/<slug>.md"

CONTRIB_DEF=$(gh api "repos/$CONTRIB" --jq '.default_branch') # expect: main
MAIN_DEF=$(gh api "repos/$MAIN" --jq '.default_branch') # expect: master
```

### Step 1 — Fetch the QA source (source of truth for the copy)

The QA file is what the author previewed and approved — it is authoritative.

```bash
CONTRIB_SHA=$(gh api "repos/$CONTRIB/contents/$PATHREL?ref=$CONTRIB_DEF" --jq '.sha')
gh api "repos/$CONTRIB/contents/$PATHREL?ref=$CONTRIB_DEF" --jq '.content' | base64 -d > file.md
```

If this 404s, the slug/path is wrong — re-confirm with the author. Do NOT invent content.

### Step 2 — Determine PROD state: CREATE or UPDATE (+ drift guard)

```bash
if MAIN_SHA=$(gh api "repos/$MAIN/contents/$PATHREL?ref=$MAIN_DEF" --jq '.sha' 2>/dev/null); then
MODE=update
else
MODE=create # QA-only tutorial, first promotion — no sha, no drift check
fi
```

- **MODE=create** → nothing in PROD to clobber. Proceed; the contents PUT omits `sha`.
- **MODE=update** → PROD already has this tutorial. **DRIFT GUARD:** diff the
current PROD file against the QA file you fetched. If PROD has content the QA
copy would silently discard (someone edited PROD directly, or the repos have
drifted), **STOP and show the author the diff** before overwriting:

```bash
gh api "repos/$MAIN/contents/$PATHREL?ref=$MAIN_DEF" --jq '.content' | base64 -d > prod-current.md
diff prod-current.md file.md && echo "IN SYNC (no-op)" || echo "DRIFT — review below before promoting"
```

If `diff` is empty, the promotion is a no-op — tell the author PROD already
matches QA; don't open an empty PR. If it differs, that's the expected
promotion delta — but confirm the diff is only the author's intended change,
not an unrelated PROD edit being reverted.

### Step 3 — For EACH repo, branch + commit the copied file

Same branch name in both (e.g. `promote/<slug>-to-prod`). QA repo commits the
same content back to itself on a branch (so the promotion is auditable there
too); PROD repo receives the copy. Use each repo's OWN default branch as base
and each repo's OWN current file sha.

```bash
promote_to() { # $1=repo $2=default-branch $3=current-file-sha-or-empty
local REPO="$1" DEF="$2" CUR_SHA="$3" BR="promote/<slug>-to-prod"
local BASE_SHA; BASE_SHA=$(gh api "repos/$REPO/git/ref/heads/$DEF" --jq '.object.sha')
gh api "repos/$REPO/git/refs" -f ref="refs/heads/$BR" -f sha="$BASE_SHA" >/dev/null

local B64; B64=$(base64 -w0 file.md)
if [ -n "$CUR_SHA" ]; then
printf '{"message":"docs(%s): promote to PROD","branch":"%s","sha":"%s","content":"%s"}' \
"<slug>" "$BR" "$CUR_SHA" "$B64" > payload.json
else
printf '{"message":"docs(%s): promote to PROD","branch":"%s","content":"%s"}' \
"<slug>" "$BR" "$B64" > payload.json
fi
gh api -X PUT "repos/$REPO/contents/$PATHREL" --input payload.json --jq '.commit.sha'

# verify byte-for-byte (works on private repos)
gh api "repos/$REPO/contents/$PATHREL?ref=$BR" --jq '.content' | base64 -d > pushed.md
diff file.md pushed.md && echo "MATCH ($REPO)" || { echo "MISMATCH ($REPO) — STOP"; return 1; }
}

promote_to "$CONTRIB" "$CONTRIB_DEF" "$CONTRIB_SHA" # QA branch (audit)
promote_to "$MAIN" "$MAIN_DEF" "${MAIN_SHA:-}" # PROD copy (empty sha => create)
```

Do not open a PR for a repo until its `promote_to` printed `MATCH`.

### Step 4 — Open a PR in each repo, cross-linked

```bash
# PROD first (the one that matters), base = its OWN default branch
PROD_PR=$(gh pr create --repo "$MAIN" --base "$MAIN_DEF" --head promote/<slug>-to-prod \
--title "Promote <slug> to PROD" \
--body "Promotes QA-previewed tutorial \`<slug>\` to PROD. Paired QA PR: <fill after>. Merging this triggers the PROD content rebuild." )

QA_PR=$(gh pr create --repo "$CONTRIB" --base "$CONTRIB_DEF" --head promote/<slug>-to-prod \
--title "Promote <slug> to PROD (QA-side record)" \
--body "QA-side record of promoting \`<slug>\`. Paired PROD PR: $PROD_PR" )

gh pr edit "$PROD_PR" --body "Promotes QA-previewed tutorial \`<slug>\` to PROD. Paired QA PR: $QA_PR. Merging this triggers the PROD content rebuild."
```

### Step 5 — Report

Report **both PR URLs**, whether it was a CREATE or UPDATE, and that content was
verified identical in both. Remind the author: **merging the `Tutorials` (PROD)
PR is what publishes to PROD** — the QA-side PR is an audit record.

## Guardrails

- **Never hardcode a base branch.** Resolve each repo's default (Step 0);
`Tutorials`=`master`, `-Contribution`=`main` today, but always resolve live.
- **Never open one PR expecting it to span both repos** — always two.
- **UPDATE mode: run the drift guard before overwriting PROD.** An empty diff is
a no-op (don't open a PR); a diff must be confirmed as only the intended change.
- **Before adding a follow-up commit to an existing PR branch, check PR state**
(`gh pr view <n> --repo <r> --json state,mergedAt`). If MERGED/CLOSED, branch
fresh and open a NEW PR — a PUT to a merged PR's branch SUCCEEDS but never
reaches the default branch. Confirm changes landed by reading the file at the
default-branch ref.
- **Never `git push`/force-push; never target a non-default base branch.**
- **QA is the source of truth for the copy** — the author previewed it there.
Do not hand-edit content during promotion; if the content is wrong, fix it in
the Contribution repo first (see `sap-tutorial-dual-pr` for editing flows),
then promote.
- This skill promotes ONE tutorial (one slug) per run. For a batch, loop the
slug and open one PR pair per slug, or ask the author to confirm a multi-slug
scope explicitly.

## Relationship to `sap-tutorial-dual-pr`

`sap-tutorial-dual-pr` **edits** a tutorial and mirrors the edit to both repos in
lockstep (keeping QA and PROD identical after an edit). This skill **promotes** a
QA-previewed tutorial to PROD (the QA→PROD direction, create-or-update, with a
drift guard for the first-promotion 404 case). Use dual-pr to change content;
use this to ship a QA-approved tutorial to PROD.
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.26.0
version: 1.27.0

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
94 changes: 76 additions & 18 deletions .github/actions/report-rebuild-failure/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ inputs:
channel:
description: 'Rebuild channel/env used to dedup issues (e.g. "qa", "prod", "dev").'
required: true
repo:
description: >-
Repository to file/find/close the issue in (owner/name). Defaults to the
repo running the workflow. Set to a source `-Contribution` repo to file a
per-tutorial author issue there (requires the token to have issues:write
on that repo — the tutorials GitHub App with Issues:RW, not the default
job GITHUB_TOKEN, which is scoped to the running repo only).
required: false
default: ${{ github.repository }}
slug:
description: >-
Optional tutorial slug. When set, the issue is per-tutorial (its own
dedup marker + title) rather than the channel-level aggregate. Used for
author content errors (missing/misnamed source file) so each tutorial
gets its own issue in its source repo, assigned to / @-mentioning the
author. Empty (default) keeps the channel-aggregate behavior (#1373).
required: false
default: ''
run-url:
description: 'Link to the failing (or succeeding) Actions run.'
required: true
Expand Down Expand Up @@ -48,9 +66,10 @@ runs:
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
REPO: ${{ github.repository }}
REPO: ${{ inputs.repo }}
OUTCOME: ${{ inputs.outcome }}
CHANNEL: ${{ inputs.channel }}
SLUG: ${{ inputs.slug }}
RUN_URL: ${{ inputs.run-url }}
TRIGGER: ${{ inputs.trigger }}
DETAILS: ${{ inputs.details }}
Expand All @@ -60,14 +79,23 @@ runs:
run: |
set -uo pipefail

# Stable per-channel marker so we find the same issue across runs
# without depending on the exact title text.
MARKER="<!-- rebuild-failure-marker channel=${CHANNEL} -->"
# Stable dedup marker so we find the same issue across runs without
# depending on the exact title text. Channel-level by default; when a
# SLUG is given it becomes per-tutorial (a slug-scoped marker + title)
# so each author content error gets its own deduplicated issue in its
# source repo instead of collapsing onto the channel aggregate.
if [ -n "$SLUG" ]; then
MARKER="<!-- rebuild-failure-marker channel=${CHANNEL} slug=${SLUG} -->"
ISSUE_TITLE="🚨 tutorial source error: ${SLUG} (${CHANNEL})"
else
MARKER="<!-- rebuild-failure-marker channel=${CHANNEL} -->"
ISSUE_TITLE="🚨 ${CHANNEL} content rebuild failing"
fi
ESCALATED_LABEL="rebuild-failure-escalated"

# Locate an existing OPEN failure issue for this channel. Search by the
# dedup label, then match the hidden marker in the body so per-channel
# issues never collide (qa vs prod). --search keeps the marker literal.
# Locate an existing OPEN failure issue for this channel (and slug, when
# set). Search by the dedup label, then match the hidden marker in the
# body so per-channel / per-tutorial issues never collide.
find_open_issue() {
gh issue list --repo "$REPO" --state open --label "$LABEL" \
--json number,body --limit 50 \
Expand Down Expand Up @@ -110,22 +138,52 @@ runs:
[ -n "$DETAILS" ] && DETAIL_LINE="- **Detail:** ${DETAILS}"

if [ -z "$ISSUE_NUM" ]; then
# First failure of a new streak — create the issue. Assignment is what
# triggers GitHub's native email to the on-call owner. Build the body
# with a single printf (no heredoc — an unindented heredoc breaks the
# YAML block scalar; and per-line $(printf) would have its trailing
# newlines stripped by command substitution, collapsing the layout).
# First failure of a new streak — create the issue, then assign in a
# SEPARATE step. Assignment is what triggers GitHub's native email,
# but a per-tutorial author is often a fork contributor who is NOT
# assignable on the source repo — passing them to `gh issue create`
# would fail the WHOLE create (no issue at all). So we create first
# (never blocked by a bad assignee) and best-effort assign after; the
# @-mention in the body emails the author regardless of assignability.
# Body is built with a single printf (no heredoc — an unindented
# heredoc breaks the YAML block scalar; and per-line $(printf) would
# have its trailing newlines stripped by command substitution,
# collapsing the layout).
DETAIL_BLOCK=""
[ -n "$DETAIL_LINE" ] && DETAIL_BLOCK="${DETAIL_LINE}"$'\n'
BODY="$(printf '%s\n\n**Automated alert (#1373).** A **%s** content rebuild failed.\n\n- **Channel:** %s\n- **Trigger:** %s\n- **First failing run:** %s\n%s\nThis issue is deduplicated per channel: each additional consecutive failure adds a comment below (so the comment count is the consecutive-failure count). A subsequent successful rebuild auto-closes this issue and resets the streak.\n\nConsecutive failures: **1**\n' \
"$MARKER" "$CHANNEL" "$CHANNEL" "$TRIGGER" "$RUN_URL" "$DETAIL_BLOCK")"

# @-mention every assignee in the body so the author is emailed even
# when they can't be assigned (fork contributors on a source repo).
MENTION_LINE=""
for a in "${_A[@]}"; do
a="$(echo "$a" | tr -d '[:space:]')"
[ -n "$a" ] && MENTION_LINE="${MENTION_LINE} @${a}"
done

if [ -n "$SLUG" ]; then
# Per-tutorial author content error (missing/misnamed source file).
MENTION_BLOCK=""
[ -n "$MENTION_LINE" ] && MENTION_BLOCK="- **Author:**${MENTION_LINE}"$'\n'
BODY="$(printf '%s\n\n**Automated alert (#1373).** A **%s**-channel content rebuild could not fetch the source markdown for tutorial `%s`.\n\n- **Tutorial slug:** %s\n- **Channel:** %s\n- **Trigger:** %s\n- **Run:** %s\n%s%s\n**Most likely cause:** the primary tutorial markdown is missing or misnamed. It MUST be at `tutorials/%s/%s.md` (folder name and file name both equal the slug). If the file is named something else (e.g. `architecture.md`), rename it to `%s.md`.\n\nThe rebuild **skipped this tutorial and continued** — the rest of the channel published normally. Fix the source file and re-trigger the QA rebuild for this slug; a successful rebuild auto-closes this issue.\n' \
"$MARKER" "$CHANNEL" "$SLUG" "$SLUG" "$CHANNEL" "$TRIGGER" "$RUN_URL" "$DETAIL_BLOCK" "$MENTION_BLOCK" "$SLUG" "$SLUG" "$SLUG")"
else
BODY="$(printf '%s\n\n**Automated alert (#1373).** A **%s** content rebuild failed.\n\n- **Channel:** %s\n- **Trigger:** %s\n- **First failing run:** %s\n%s\nThis issue is deduplicated per channel: each additional consecutive failure adds a comment below (so the comment count is the consecutive-failure count). A subsequent successful rebuild auto-closes this issue and resets the streak.\n\nConsecutive failures: **1**\n' \
"$MARKER" "$CHANNEL" "$CHANNEL" "$TRIGGER" "$RUN_URL" "$DETAIL_BLOCK")"
fi

NEW_URL="$(gh issue create --repo "$REPO" \
--title "🚨 ${CHANNEL} content rebuild failing" \
--title "$ISSUE_TITLE" \
--label "$LABEL" \
"${ASSIGNEE_FLAGS[@]}" \
--body "$BODY")" \
&& echo "Filed new ${CHANNEL} rebuild-failure issue: ${NEW_URL}" \
|| echo "::warning::Failed to create ${CHANNEL} rebuild-failure issue"
&& echo "Filed new issue: ${NEW_URL}" \
|| { echo "::warning::Failed to create issue in ${REPO} (title: ${ISSUE_TITLE})"; exit 0; }

# Best-effort assign — tolerate an unassignable (fork-contributor)
# author; the @-mention already emailed them. Never fail the step.
if [ "${#ASSIGNEE_FLAGS[@]}" -gt 0 ]; then
gh issue edit "$NEW_URL" --repo "$REPO" "${ASSIGNEE_FLAGS[@]}" >/dev/null 2>&1 \
|| echo "::notice::Could not assign ${ASSIGNEES} on ${NEW_URL} (likely not a collaborator on ${REPO}); @-mention still notifies them."
fi
exit 0
fi

Expand Down
Loading
Loading