Unit 14 is where the course turns into proof. Learners plan and build a capstone, then show it to people who might hire them or buy from them. Two things have to be in order first.
The workbench. Thirteen units each added tools, libraries and trial accounts. Some trials have expired by now. This setup re-runs every earlier check in one go and shows what still works, so each learner knows which pieces the capstone can rely on.
The shop window. A public portfolio repository on GitHub. It holds only the work a learner chooses to show, copied from their private course folder by a short allowlist, and checked for keys and large files before anyone else sees it.
Setup takes 45 to 75 minutes and costs nothing. No new SAP account is needed, though some learners will restart an expired trial.
A capstone that fails in the demo because a trial ran out the night before teaches the wrong lesson. In customer work, the same thing happens when a pilot depends on a sandbox nobody is watching. Knowing which accounts expire, and when, is part of delivery.
The public portfolio carries a different risk: publishing something that should stay private. A forward deployed engineer works close to customer systems. One API key, one real supplier name or one extract of customer data in a public repository is a security incident, and possibly a breach of contract.
Take the running example. A learner's blocked sales orders triage, built in Unit 1 and improved since, is good portfolio material. Their .env file with the model key, the service key for their SAP AI Core trial and their raw agent logs are not. The course folder holds both. The setup separates them on purpose, with a list of what may be published rather than a list of what may not.
For a hiring manager or a client, a clean portfolio says more than a certificate: working code, a clear README, an evaluation, and no secrets.
SAP's trials are how this course gives every learner a free SAP environment. As of October 2026, SAP describes them as follows. Check the pages again before a demo, since terms change.
SAP BTP trial. 90 days from registration, and the account needs at least one login within any 30-day period. SAP calls it a non-commercial, time-limited offering; when it ends, everything in it is deleted. A new trial can be started at any time.
Generative AI hub trial. A 30-day basic trial that includes SAP AI Core and models from several providers.
SAP HANA Cloud. SAP's tutorial says free tier instances are stopped every night, must be restarted each working day, and are deleted if not restarted within 30 days.
Beyond trials. For use without a time limit, SAP points to a commercial model: an Enterprise Agreement or Pay-As-You-Go.
GitHub isn't an SAP product. SAP's own change tooling, CTS and Git-enabled CTS, is covered in Git and GitHub for AI engineers. The portfolio here is a learner's personal record, not a customer delivery path.
Time: 45 to 75 minutes. Re-running thirteen checks takes a few minutes; fixing what they find can take longer.
Money for a learner: nothing. GitHub's free plan covers public repositories. A restarted SAP trial is free.
Money for a company: a team capstone or pilot needs accounts that don't expire, which means a commercial SAP BTP account and a company source-code platform.
"I'll just make my course repository public." Its whole history becomes public too, including anything ever committed and later deleted. A fresh repository avoids that.
"A .gitignore file is enough protection." It stops accidents with known files. It doesn't catch a key pasted into a script, which is why the setup scans every file it publishes.
"GitHub will stop any secret I push." GitHub's push protection blocks many known key formats in public repositories. It is a safety net, not a reason to skip your own check.
"No license means anyone can use my code." GitHub's documentation says the opposite: without a license, default copyright applies.
"Trials last as long as I need." Each has its own clock, and some delete everything when it runs out.
Pick one answer for each question. The explanation appears after you choose.
1Why does this setup re-run every earlier unit's check before the capstone?
Answer: B. Thirteen units added tools and trial accounts, and some trials end on their own. Knowing what still works lets a learner plan the capstone around it instead of finding out in the demo.
2A learner wants to make their private course repository public as their portfolio. What is the main risk?
Answer: C. A repository carries its full history. Anything ever committed, even if deleted later, becomes visible. A fresh public repository filled from an allowlist avoids that.
3Why does the setup publish by allowlist rather than by excluding known secret files?
Answer: B. An exclusion list only stops what you thought of. An allowlist publishes only what you deliberately picked, so a forgotten log or key file stays in the private folder.
4Your team's pilot will demo in five weeks on the 30-day generative AI hub trial a developer started last week. What should you ask?
Answer: B. SAP describes it as a 30-day trial, so it ends before a demo five weeks away. The plan needs a new trial at the right time or a commercial account.
5A candidate's public portfolio has no license file. What does that mean for your company?
Answer: C. GitHub's documentation says that without a license, default copyright laws apply. Viewing is fine; reusing the code needs the author's permission or a license.
6GitHub's push protection is on by default for public repositories. What is the soundest way to treat it?
Answer: B. Push protection blocks many known secret formats, but not every secret or every piece of customer data. The setup's own scan and the allowlist come first.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Your private course folder is a workshop. It is messy on purpose: half-finished scripts, agent logs, trial keys in .env, thirteen units of history. That is fine, because nobody else sees it.
Your portfolio is a shop window. It shows a few finished pieces, labelled and lit. Nothing walks from the workshop into the window by accident. You carry each piece over, look at it once more, and put it down.
Three rules follow:
Separate repositories. The window gets its own fresh history, so nothing from the workshop's past leaks.
An allowlist decides what moves. A file goes to the window only if you named it.
A check runs at the door. Every file is scanned for secrets and size before it is copied, and the window itself is scanned before it goes public.
flowchart LR
subgraph Laptop
C[orchestrate-course<br/>private workshop]
L[portfolio.txt<br/>allowlist]
P[publish_portfolio.py<br/>scan + copy]
W[sap-ai-portfolio<br/>shop window]
end
C --> P
L --> P
P --> W
W -->|git push| G[(GitHub<br/>public repository)]
K[check_unit14.py] -.checks.-> C
K -.checks.-> W
Each earlier setup topic left a check script in your course folder. Together they describe your environment. check_unit14.py runs them all and reports one line per unit.
Unit
Setup added
Check script
Account or clock
1
Python, VS Code, Git, .venv, .env, SAP sandbox key, model key
Your capstone won't use all thirteen units. A failing Unit 12 check doesn't matter if your capstone has no local model. So check_unit14.py treats two checks as always needed: check_setup.py (Python, keys and the SAP sandbox) and check_repo.py (no secrets in Git). The rest report LATER when they fail, unless you name the unit with --need. Then a failure becomes MISSING, which must be fixed.
Your computer can't ask SAP how long your trials have left. So you write the start dates into .env, and the check counts days. It uses SAP's stated lengths as of October 2026: 90 days for the BTP trial and 30 days for the generative AI hub trial. It warns when fewer than 14 days are left.
It can't see the other rules. The BTP trial also needs a login at least once in any 30-day period, and SAP's HANA Cloud tutorial says free tier instances stop every night and are deleted if not restarted within 30 days. Log in and start HANA Cloud on any day you work on the capstone.
GitHub's documentation describes two safety nets, as of October 2026:
Push protection for users is on by default. It blocks a push that contains a known secret format to a public repository.
Secret scanning runs automatically, for free, on public repositories. When it finds a secret from a partner provider, GitHub reports it to that provider so it can act, for example by revoking it.
Neither knows what counts as customer data, and neither sees what never matched a pattern. Your own allowlist and scan stay in front.
#Build it yourself: re-check the workbench and open the shop window
You will run one check across all earlier units, record your trial dates, create a separate portfolio repository, copy chosen work into it through an allowlist, and make it public once it passes. Everything is free.
Before you start: complete Set up your computer for this course and Git and GitHub for AI engineers. They install Python, VS Code, Git and the GitHub CLI (gh), create your orchestrate-course folder with its .venv, .env and check_repo.py, and sign gh in to your free GitHub account. This walkthrough doesn't repeat those steps. The other setup topics, Units 2 to 13, are needed only for the units your capstone uses.
These dates aren't secrets, but .env is where the course keeps settings, and check_unit14.py reads them from there. If you start a new trial later, change the date.
#Step 4: Create the portfolio repository, private for now
You create it private first, fill it, check it, and only then make it public in Step 7.
Go to your home folder. The same command works in PowerShell and in macOS/Linux terminals:
cd ~
See which license keywords gh knows:
gh repo license list
The commands below use mit, a short, permissive license that is common for portfolios. Pick another from the list if your employer or your own preference says so; choosealicense.com explains the options.
Create the repository on GitHub, with a README and a license, and clone it to your computer:
gh repo create sap-ai-portfolio --private --add-readme --license mit --description "AI for SAP portfolio: made-up, SAP-shaped sample data only" --clone
Go into the new folder and set the no-reply address from Step 2 for this repository only:
cd sap-ai-portfolio
git config user.email "PASTE-YOUR-NO-REPLY-ADDRESS"
Create a .gitignore for the portfolio, so a local .env or .venv there can never be committed:
What success looks like: a folder sap-ai-portfolio sits next to orchestrate-course, and holds README.md, LICENSE and .gitignore. The exact wording gh prints may differ between versions; it should say it created and cloned the repository.
The README is the first thing a visitor reads. Keep it short and honest.
In VS Code, open sap-ai-portfolio/README.md (File > Open File), replace its content with the text below, put in your name, and save.
# Your Name: AI for SAP portfolio
## About
Work from the Orchestrate course on enterprise AI for SAP: small, working AI services
around SAP processes, each with an evaluation and a business KPI.
## Projects
| Project | Business problem | KPI | Status |
| --- | --- | --- | --- |
| Blocked sales orders triage (`unit01/`) | Credit team reviews every blocked order by hand | Minutes per order reviewed | Working on sample data |
## How to run
1. Install Python 3.11 or newer.
2. Create a virtual environment and install the libraries:
`python -m venv .venv`, activate it, then `pip install -r requirements.txt`.
3. Each project folder says how to run it. Every project has a sample-data path that needs no account.
## Rules
- All data is made up and only shaped like SAP data. No customer or company data.
- No keys in this repository. Each project reads its keys from a local `.env` file that is never published.
- Independent learning work. Not affiliated with or endorsed by SAP.
check_unit14.py looks for the headings ## Projects and ## How to run, so keep them.
In the course folder, create portfolio.txt with the content below and save. Each line names one file or folder to publish; lines starting with # are notes.
# What goes into the public portfolio. One file or folder per line.
requirements.txt
unit01/triage.py
Create publish_portfolio.py in the course folder, paste the code below and save. It uses built-in Python only.
"""Copy the files you choose from your course folder into your public portfolio folder.
Run it from your course folder:
python publish_portfolio.py shows what it would copy (changes nothing)
python publish_portfolio.py --copy copies the files
It reads portfolio.txt: one file or folder per line, relative to the course folder.
Lines starting with # are notes. Only what is listed gets copied: an allowlist.
It never copies .env files, virtual environments, Git folders, caches, files over 50 MiB
or files with secret-looking text, and it never deletes anything. Built-in modules only.
"""
import argparse
import filecmp
import os
import re
import shutil
import sys
from pathlib import Path
LIST = Path("portfolio.txt")
MAX_SIZE = 50 * 1024 * 1024 # GitHub warns above 50 MiB and blocks above 100 MiB
SKIP_FOLDERS = {".git", ".venv", ".venv-garak", "venv", "__pycache__", "node_modules",
".ipynb_checkpoints", ".pytest_cache"}
SECRET_PATTERNS = [
re.compile(r"sk-ant-[A-Za-z0-9_-]{10,}"),
re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"),
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"\"clientsecret\"\s*:"),
re.compile(r"(?i)(api_?key|secret|password|token)\s*[=:]\s*[\"'][^\"'\s]{16,}[\"']"),
]
def read_env(path: str = ".env") -> dict:
"""Read KEY=value lines from .env without any library."""
values = {}
if os.path.exists(path):
with open(path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def why_not(path: Path) -> str:
"""Return the reason a file must not be published, or '' if it may be."""
if path.name.startswith(".env"):
return "a .env file holds keys"
if path.stat().st_size > MAX_SIZE:
return "over 50 MiB"
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
return "" # a binary file such as a PNG chart: no text to scan
for number, line in enumerate(text.splitlines(), start=1):
if any(p.search(line) for p in SECRET_PATTERNS):
return f"secret-looking text on line {number}"
return ""
def files_under(entry: Path):
"""Yield the file itself, or every file inside a folder, leaving out skipped folders."""
if entry.is_file():
yield entry
return
for folder, subfolders, names in os.walk(entry):
subfolders[:] = sorted(s for s in subfolders if s not in SKIP_FOLDERS)
for name in sorted(names):
yield Path(folder) / name
parser = argparse.ArgumentParser(description="Publish chosen course files to your portfolio.")
parser.add_argument("--copy", action="store_true", help="copy the files (default: only show)")
options = parser.parse_args()
course = Path.cwd().resolve()
target = Path(read_env().get("PORTFOLIO_DIR") or course.parent / "sap-ai-portfolio").expanduser()
if not (target / ".git").is_dir():
sys.exit(f"No portfolio repository at {target}. Create it first (Step 4).")
if not LIST.exists():
sys.exit("No portfolio.txt in this folder. Create it first (Step 6).")
copied = skipped = same = 0
for raw in LIST.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
entry = (course / line).resolve()
if course not in entry.parents and entry != course:
print(f" SKIP {line} (outside the course folder)")
skipped += 1
continue
if any(part in SKIP_FOLDERS for part in entry.relative_to(course).parts):
print(f" SKIP {line} (never published)")
skipped += 1
continue
if not entry.exists():
print(f" SKIP {line} (not found)")
skipped += 1
continue
for source in files_under(entry):
relative = source.relative_to(course)
folders = [part for part in relative.parts[:-1] if part in SKIP_FOLDERS]
reason = f"inside {folders[0]}" if folders else why_not(source)
if reason:
print(f" SKIP {relative.as_posix()} ({reason})")
skipped += 1
continue
destination = target / relative
if destination.exists() and filecmp.cmp(source, destination, shallow=False):
same += 1
continue
print(f" {'COPY' if options.copy else 'WOULD':<6} {relative.as_posix()}")
copied += 1
if options.copy:
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
print()
verb = "Copied" if options.copy else "Would copy"
print(f"{verb} {copied} file(s); {same} already up to date; {skipped} skipped. Target: {target}")
if not options.copy and copied:
print("Nothing changed yet. Run again with --copy to copy them.")
if skipped:
print("Read every SKIP line. Fix the file or remove it from portfolio.txt.")
Do a dry run first. It shows what it would copy and changes nothing:
python publish_portfolio.py
WOULD requirements.txt
WOULD unit01/triage.py
Would copy 2 file(s); 0 already up to date; 0 skipped. Target: C:\Users\you\sap-ai-portfolio
Nothing changed yet. Run again with --copy to copy them.
Your target path will differ. A SKIP line names a file the script refused and why, for example (secret-looking text on line 4). Fix that file or take it off the list. A run that copies 0 files and skips none means everything is already up to date.
Copy for real:
python publish_portfolio.py --copy
Commit and push in the portfolio folder:
cd ~/sap-ai-portfolio
git status
git add .
git commit -m "Add README, .gitignore and the blocked-orders triage"
git push
cd ~/orchestrate-course
In git status, you should see only README.md, .gitignore, requirements.txt and unit01/. Stop if you see anything else.
#Step 7: Run the Unit 14 check, then make the repository public
Unit 14 adds no lines to requirements.txt. Its check uses built-in Python only.
In the course folder, create check_unit14.py, paste the code below and save.
"""Check that your course folder is ready for the Unit 14 capstone.
Run it from your course folder: python check_unit14.py
Tell it which units your capstone needs, so their checks must pass:
python check_unit14.py --need 05 07 09
Skip re-running the earlier checks (faster): python check_unit14.py --skip-earlier
It uses built-in Python only. It runs the check scripts you saved in earlier units, reads the
trial start dates in .env, and inspects your public portfolio folder. It changes nothing.
The only thing it sends anywhere is what the earlier checks already send, plus one
'gh repo view' call to ask GitHub whether your portfolio repository is public.
"""
import argparse
import datetime
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# (unit, script, the topic that creates it)
EARLIER = [
("01", "check_setup.py", "Set up your computer for this course"),
("01", "check_repo.py", "Git and GitHub for AI engineers"),
("02", "check_unit02.py", "Set up for Unit 2"),
("03", "check_unit03.py", "Set up for Unit 3"),
("04", "check_unit04.py", "Set up for Unit 4"),
("05", "check_unit05.py", "Set up for Unit 5"),
("06", "check_unit06.py", "Set up for Unit 6"),
("07", "check_unit07.py", "Set up for Unit 7"),
("08", "check_unit08.py", "Set up for Unit 8"),
("09", "check_unit09.py", "Set up for Unit 9"),
("10", "check_unit10.py", "Set up for Unit 10"),
("11", "check_unit11.py", "Set up for Unit 11"),
("12", "check_unit12.py", "Set up for Unit 12"),
("13", "check_unit13.py", "Set up for Unit 13"),
]
# (line in .env, name, length in days, units that use it)
TRIALS = [
("BTP_TRIAL_START", "SAP BTP trial", 90, {"03", "06", "07", "10", "13"}),
("GENAI_HUB_TRIAL_START", "generative AI hub trial", 30, {"05"}),
]
WARN_DAYS = 14 # warn when a trial has fewer days left than this
WARN_SIZE = 50 * 1024 * 1024 # GitHub warns above 50 MiB
BLOCK_SIZE = 100 * 1024 * 1024 # GitHub blocks files above 100 MiB
TIMEOUT = 300 # seconds allowed for each earlier check
# Text that looks like a real secret. A match means "look at this line", not proof.
SECRET_PATTERNS = [
re.compile(r"sk-ant-[A-Za-z0-9_-]{10,}"), # Anthropic-style key
re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"), # GitHub token
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), # private key file
re.compile(r"\"clientsecret\"\s*:"), # SAP service key
re.compile(r"(?i)(api_?key|secret|password|token)\s*[=:]\s*[\"'][^\"'\s]{16,}[\"']"),
]
problems = 0
def report(ok: bool, label: str, fix: str = "", optional: bool = False) -> None:
"""Print one line: OK, MISSING (must fix) or LATER (optional for now)."""
global problems
if ok:
print(f" OK {label}")
elif optional:
print(f" LATER {label} -> {fix}")
else:
problems += 1
print(f" MISSING {label} -> {fix}")
def read_env(path: str = ".env") -> dict:
"""Read KEY=value lines from .env without any library. Ignores comments and blank lines."""
values = {}
if not os.path.exists(path):
return values
with open(path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def run_check(script: str) -> tuple:
"""Run one earlier check with this Python. Returns (exit code, MISSING count, LATER count)."""
try:
done = subprocess.run([sys.executable, script], capture_output=True, text=True,
timeout=TIMEOUT, encoding="utf-8", errors="replace")
except subprocess.TimeoutExpired:
return None, 0, 0
lines = done.stdout.splitlines()
missing = sum(1 for line in lines if line.lstrip().startswith("MISSING"))
later = sum(1 for line in lines if line.lstrip().startswith("LATER"))
return done.returncode, missing, later
def git(folder: Path, *args: str) -> subprocess.CompletedProcess:
"""Run one git command inside the given folder and capture its output as text."""
return subprocess.run(["git", "-C", str(folder), *args], capture_output=True, text=True)
parser = argparse.ArgumentParser(description="Check your capstone environment.")
parser.add_argument("--need", nargs="*", default=[], metavar="NN",
help="units your capstone needs, for example: --need 05 07 09")
parser.add_argument("--skip-earlier", action="store_true", help="don't re-run earlier checks")
options = parser.parse_args()
need = {n.zfill(2) for n in options.need}
env = read_env()
print("\n1. Python")
v = sys.version_info
report(v >= (3, 11), f"Python {v.major}.{v.minor}.{v.micro}",
"the course needs Python 3.11 or newer (see Set up for Unit 2, Step 1)")
report(sys.prefix != sys.base_prefix, "virtual environment is active", "activate .venv (Step 1)")
report(shutil.which("git") is not None, "git", "install Git (Set up your computer)")
report(shutil.which("gh") is not None, "gh (GitHub CLI)", "install it (Git and GitHub for AI engineers)")
print("\n2. Earlier units" + (f" (your capstone needs: {', '.join(sorted(need))})" if need else ""))
if options.skip_earlier:
print(" (skipped)")
else:
for unit, script, topic in EARLIER:
needed = script in ("check_setup.py", "check_repo.py") or unit in need
if not os.path.exists(script):
report(False, f"Unit {unit}: {script} not found", f"save it from '{topic}'",
optional=not needed)
continue
code, missing, later = run_check(script)
if code is None:
report(False, f"Unit {unit}: {script} didn't finish in {TIMEOUT} s",
f"run python {script} on its own to see where it stops", optional=not needed)
elif code == 0:
report(True, f"Unit {unit}: {script} passes" + (f" ({later} for later)" if later else ""))
else:
report(False, f"Unit {unit}: {script} has {missing} item(s) to fix",
f"run python {script} and fix its MISSING lines" if needed
else f"fix only if your capstone needs Unit {unit} (--need {unit})",
optional=not needed)
print("\n3. Trial clocks")
today = datetime.date.today()
for name, label, days, units in TRIALS:
needed = bool(units & need)
raw = env.get(name, "")
try:
started = datetime.date.fromisoformat(raw)
except ValueError:
report(False, f"{label}: start date", f"add {name}=YYYY-MM-DD to .env (Step 3)",
optional=not needed)
continue
left = days - (today - started).days
if left < 0:
report(False, f"{label}: ended {-left} day(s) ago",
"start a new trial (see its setup topic) and update the date in .env",
optional=not needed)
else:
report(left >= WARN_DAYS, f"{label}: about {left} day(s) left",
"plan the capstone demo before it ends, or start a new trial first", optional=True)
print("\n4. Portfolio repository")
folder = Path(env.get("PORTFOLIO_DIR") or Path.cwd().parent / "sap-ai-portfolio").expanduser()
report(folder.is_dir(), f"folder {folder}", "create it with gh repo create (Step 4)")
is_repo = folder.is_dir() and git(folder, "rev-parse", "--is-inside-work-tree").returncode == 0
report(is_repo, "it is a Git repository", "create it with gh repo create (Step 4)")
if is_repo:
readme = folder / "README.md"
text = readme.read_text(encoding="utf-8", errors="replace") if readme.exists() else ""
for heading in ("## Projects", "## How to run"):
report(heading in text, f"README.md has '{heading}'", "use the README from Step 5")
licences = [p.name for p in folder.iterdir() if p.name.upper().startswith(("LICENSE", "LICENCE"))]
report(bool(licences), f"license file: {licences[0]}" if licences else "license file",
"add one (Step 4 creates it with --license)")
tracked = git(folder, "ls-files").stdout.splitlines()
report(not any(Path(p).name.startswith(".env") for p in tracked), ".env is not tracked",
"git rm --cached .env, commit, and rotate every key that was pushed")
report(not any(p.startswith((".venv/", "venv/")) for p in tracked), ".venv is not tracked",
"add .venv/ to .gitignore, then git rm -r --cached .venv")
hits, big, huge = [], [], []
for path in tracked:
full = folder / path
if not full.is_file():
continue
size = full.stat().st_size
if size > BLOCK_SIZE:
huge.append(path)
elif size > WARN_SIZE:
big.append(path)
try:
with open(full, encoding="utf-8") as handle:
for number, line in enumerate(handle, start=1):
if any(p.search(line) for p in SECRET_PATTERNS):
hits.append(f"{path}:{number}")
except (UnicodeDecodeError, OSError):
continue # binary or unreadable files
report(not hits, "no secret-looking text in tracked files", "check and remove: " + ", ".join(hits[:5]))
report(not huge, "no file over 100 MiB", "GitHub blocks these; remove: " + ", ".join(huge[:5]))
report(not big, "no file over 50 MiB", "GitHub warns about: " + ", ".join(big[:5]), optional=True)
email = git(folder, "config", "user.email").stdout.strip()
report("noreply" in email, f"commit email: {email or 'not set'}",
"use your GitHub no-reply address (Step 2)", optional=True)
remote = git(folder, "remote", "get-url", "origin")
report(remote.returncode == 0, "remote 'origin': " + (remote.stdout.strip() or "not set"),
"create the GitHub repository (Step 4)", optional=True)
dirty = git(folder, "status", "--porcelain").stdout.strip()
report(not dirty, "no uncommitted changes", "review with git status, then commit", optional=True)
counts = git(folder, "rev-list", "--left-right", "--count", "@{upstream}...HEAD")
if counts.returncode == 0:
behind, ahead = counts.stdout.split()
report(ahead == "0", f"all commits pushed ({ahead} not on GitHub yet)", "git push", optional=True)
if remote.returncode == 0 and shutil.which("gh"):
seen = subprocess.run(["gh", "repo", "view", "--json", "visibility", "-q", ".visibility"],
cwd=folder, capture_output=True, text=True)
visibility = seen.stdout.strip() or "unknown"
report(visibility == "PUBLIC", f"visibility on GitHub: {visibility}",
"make it public when check and review pass (Step 7)", optional=True)
print("\n5. Course folder")
report(os.path.exists("portfolio.txt"), "portfolio.txt", "create it (Step 6)", optional=True)
report(os.path.exists("publish_portfolio.py"), "publish_portfolio.py", "create it (Step 6)",
optional=True)
print()
if problems:
print(f"{problems} item(s) to fix. Fix them in order, then run this again.")
sys.exit(1)
print("All set. Your capstone environment and portfolio repository are ready for Unit 14.")
Run it, naming the units your capstone will use. If you don't know yet, leave out --need:
python check_unit14.py --need 05 07
What success looks like (your versions, paths and numbers will differ):
1. Python
OK Python 3.13.16
OK virtual environment is active
OK git
OK gh (GitHub CLI)
2. Earlier units (your capstone needs: 05, 07)
OK Unit 01: check_setup.py passes (1 for later)
OK Unit 01: check_repo.py passes
OK Unit 02: check_unit02.py passes
...
OK Unit 05: check_unit05.py passes (4 for later)
OK Unit 07: check_unit07.py passes
LATER Unit 12: check_unit12.py has 2 item(s) to fix -> fix only if your capstone needs Unit 12 (--need 12)
...
3. Trial clocks
OK SAP BTP trial: about 63 day(s) left
LATER generative AI hub trial: about 9 day(s) left -> plan the capstone demo before it ends, or start a new trial first
4. Portfolio repository
OK folder C:\Users\you\sap-ai-portfolio
OK it is a Git repository
OK README.md has '## Projects'
OK README.md has '## How to run'
OK license file: LICENSE
OK .env is not tracked
OK .venv is not tracked
OK no secret-looking text in tracked files
OK no file over 100 MiB
OK no file over 50 MiB
OK commit email: <your no-reply address>
OK remote 'origin': https://github.com/your-name/sap-ai-portfolio.git
OK no uncommitted changes
OK all commits pushed (0 not on GitHub yet)
LATER visibility on GitHub: PRIVATE -> make it public when check and review pass (Step 7)
5. Course folder
OK portfolio.txt
OK publish_portfolio.py
All set. Your capstone environment and portfolio repository are ready for Unit 14.
LATER lines are fine. MISSING lines must be fixed; each names the step or the earlier check that fixes it. The first run takes a few minutes, because it runs every earlier check. Add --skip-earlier to re-check only the trials and the portfolio.
Look at the repository on GitHub before anyone else does:
cd ~/sap-ai-portfolio
gh repo view --web
Read every file in the list. Ask: would I be happy for a client to read this?
Make it public. gh asks you to accept the consequences of a visibility change by adding a flag:
gh repo edit --visibility public --accept-visibility-change-consequences
cd ~/orchestrate-course
Run python check_unit14.py --skip-earlier again. The last portfolio line now reads OK visibility on GitHub: PUBLIC.
You should see check_unit14.py, publish_portfolio.py and portfolio.txt. You must not see .env.
Save:
git add check_unit14.py publish_portfolio.py portfolio.txt
git commit -m "Set up Unit 14: capstone check and portfolio publishing"
From now on, adding work to the portfolio is three moves: add a line to portfolio.txt, run python publish_portfolio.py --copy, then commit and push in sap-ai-portfolio.
Trials versus real accounts. Every SAP environment in this course is a trial with a clock. SAP's trial page says lasting use needs an Enterprise Agreement or Pay-As-You-Go. A customer pilot starts there, in the customer's own account, not in a personal trial.
The customer owns the code. In customer work, code usually lives in the customer's repositories and moves through their change process, for ABAP through CTS or Git-enabled CTS, as Git and GitHub for AI engineers describes. A personal portfolio holds only course work and your own made-up data.
Two kinds of evidence. A public portfolio shows how you work. SAP's own certifications, covered in Preparing for SAP's AI certifications, show what you know. They answer different questions.
Need
Learn with (this setup)
On a customer project
SAP environment
Personal trials with clocks
The customer's licensed accounts, with owners and budgets
Source code
Private course folder, public portfolio
The customer's repositories and change process
Secrets
.env on your laptop, push protection
The platform's secret store and the customer's rotation rules
Secrets. Keep keys out of anything that is published. GitHub's push protection blocks known secret formats in public repositories, and secret scanning runs on them for free; treat both as a last line, behind your own scan.
Client confidentiality. Never name a client, a client's system or an internal process detail in a public repository without written permission. Use the course's made-up, SAP-shaped data.
Personal data. Sample data must not contain real people's names, emails or addresses, including your own colleagues.
History. A public repository shows every commit. Starting fresh, as this setup does, keeps old mistakes private. If something leaks anyway, rotate first, then clean up.
Size. Don't publish datasets, model files or container images in Git. GitHub blocks files over 100 MiB and recommends keeping repositories ideally under 1 GB.
Licensing. Without a license, default copyright applies. Choose one deliberately, and check with your employer if you wrote the code on company time.
Trials. Plan demos around trial clocks, log in to the BTP trial regularly, and start HANA Cloud on the day you need it.
Prove that a stranger could run your portfolio. A fresh-clone test catches files you forgot to publish and libraries you forgot to list. The capstone's README will claim it runs, so check that claim now.
Go to your home folder and clone your public portfolio under a different name:
cd ~
git clone https://github.com/YOUR-NAME/sap-ai-portfolio.git portfolio-fresh-test
cd portfolio-fresh-test
Create a new virtual environment there and turn it on:
Windows (PowerShell):
python -m venv .venv
.venv\Scripts\Activate.ps1
macOS / Linux:
python3 -m venv .venv
source .venv/bin/activate
Install what the README says:
pip install -r requirements.txt
This can take a while, because the course's requirements.txt lists everything you installed in thirteen units. If it is slow, that is a finding: trim the portfolio's requirements.txt to what your published projects need.
Run the blocked-orders triage. It uses made-up data and needs no key:
python unit01/triage.py
It prints the four sample orders with ESCALATE or queue, as it did in Unit 1.
If anything fails, fix it in the course folder, add the missing file to portfolio.txt, publish with python publish_portfolio.py --copy, and push from sap-ai-portfolio. Then repeat from step 1 with a new clone.
In orchestrate-course/unit14/, create fresh_clone_test.md with three lines: the date, the commands you ran, and what you had to fix.
Delete the test folder when you're done. It is a copy; your work is safe on GitHub and in sap-ai-portfolio.
Commit the note in the course folder:
cd ~/orchestrate-course
git add unit14/fresh_clone_test.md
git commit -m "Unit 14: fresh-clone test of the portfolio"
Done when a fresh clone of your public portfolio installs and runs the triage on sample data with no extra steps, fresh_clone_test.md records the run, and python check_unit14.py --skip-earlier ends with All set.
Pick one answer for each question. The explanation appears after you choose.
1In the workshop and shop window model, why does the portfolio get its own repository?
Answer: D. A repository carries its whole history. Starting the portfolio fresh means nothing from thirteen units of course commits, including anything later deleted, becomes public.
2What does check_unit14.py --need 05 07 change compared with running it without options?
Answer: B. Every earlier check still runs. --need marks which units the capstone depends on, so their failures must be fixed; the setup and repository checks are always needed.
3Why does publish_portfolio.py refuse a file whose text matches "clientsecret"?
Answer: D. SAP service keys carry a client secret. The pattern catches a key file or a pasted key before it is copied into the public folder.
4Your BTP trial started 80 days ago and the capstone demo is in two weeks. What does the check report, and what would you do?
Answer: B. The check uses SAP's stated 90 days for the BTP trial and warns below 14 days left. The demo falls after the end date, so start a new trial and rebuild what the demo needs in time.
5A push to your public portfolio is rejected because GitHub found a secret. What is the right response?
Answer: D. Push protection means a known secret format is in your commit. Remove it, and treat the key as exposed by rotating it. Renaming or hiding it leaves the secret in place.
6Why does Step 4 create the repository private and only make it public in Step 7?
Answer: A. The safe order is fill, check, review, then publish. Making it public last means a mistake found by the check or your own review is never visible to others.
7The fresh-clone test fails with ModuleNotFoundError for a library. What does that tell you?
Answer: C. A fresh clone has only what you published. If a library is missing, the published requirements.txt doesn't list it; add it in the course folder and publish again.
8In which case should a file never go into the portfolio, even on the allowlist?
Answer: B. Real client data breaks confidentiality and may contain personal data, whatever the file type. The other three are exactly what the portfolio is for.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Start your free trial of SAP Business Technology Platform (sap.com)— 90 days from registration; at least one login within a 30-day period; non-commercial, time-limited; all data deleted at the end; a new trial can be started at any time; Enterprise Agreement or Pay-As-You-Go for ongoing use