Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

Set up for Unit 14: capstone environment and a public portfolio

Re-check every earlier unit's setup in one run, track your SAP trial clocks, and publish a clean public portfolio repository on GitHub.

Updated Oct 10, 2026Foundational 8 minDeep 40 min
Foundational layer · 8 min read

The 60-second version

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.

Why it matters to the business

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.

How SAP does it

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.

What this unit adds

Item What it is Cost Used in
check_unit14.py Runs every earlier check, reads your trial dates, inspects the portfolio Free Before every Unit 14 topic and the capstone demo
Trial dates in .env Start dates of your BTP and generative AI hub trials Free Planning the capstone around expiry dates
Public portfolio repository A separate GitHub repository with fresh history Free The capstone and the portfolio projects
portfolio.txt and publish_portfolio.py An allowlist and a script that copies only listed, clean files Free Every time you add work to the portfolio

Unit 14 adds no Python libraries and no new accounts beyond the GitHub account from Unit 1.

Time and money

  • 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.

Questions to ask

Ask your employer or IT before you publish:

  • May I publish code I wrote during training, and under which license?
  • Must the portfolio live on a personal account, or on a company account?
  • Are there client names, system names or data I must never mention publicly?

Ask a partner or candidate who shows you their portfolio:

  • Which parts did you write, and which came from tutorials or an AI assistant?
  • What data does it use? Is any of it real?
  • How did you evaluate it, and what were the numbers?
  • Could I clone it and run it today on sample data?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • Capstone: the end-of-unit project that combines what the course taught into one working solution.
  • Portfolio repository: a public GitHub repository holding the work you choose to show.
  • Allowlist: a list of what is allowed; everything not on it is left out.
  • Push protection: a GitHub feature that blocks a push when it finds a known secret format.
  • License: the terms under which others may use your code.
  • Trial clock: the time left before a trial account or service is suspended or deleted.

Check yourself

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.
Deep layer · 40 min read

Mental model: a workshop and a shop window

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.

How it works

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

The capstone environment, unit by unit

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 check_setup.py Model key: small per-request charge
1 Repository safety check, GitHub CLI, private course repository check_repo.py GitHub free plan
2 numpy, pandas, scikit-learn, matplotlib, notebooks check_unit02.py None
3 PyTorch (CPU), Sentence Transformers, SAP BTP trial check_unit03.py BTP trial: 90 days
4 PyTorch confirmed, optional free GPU notebook check_unit04.py None
5 Generative AI hub access, SAP Cloud SDK for AI check_unit05.py Generative AI hub trial: 30 days
6 Node.js, CAP development kit, Cloud Foundry CLI, FastAPI check_unit06.py Uses the BTP trial
7 Chroma, SAP HANA client, HANA Cloud check_unit07.py HANA Cloud stops nightly
8 Inspect, ranx, the golden set check_unit08.py Model key for judging
9 MCP Python SDK, MCP Inspector check_unit09.py Model key for agents
10 Container engine, Phoenix, OpenTelemetry check_unit10.py Uses the BTP trial
11 garak, Presidio, spaCy model, pip-audit check_unit11.py None
12 Ollama and small local models check_unit12.py None
13 ABAP trial, ADT in Eclipse and VS Code, Copilot Free check_unit13.py ABAP trial in the BTP trial

Needed and not needed

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.

Trial clocks

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.

What GitHub adds for public repositories

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.

flowchart LR
  S1[Step 1<br/>open folder] --> S2[Step 2<br/>private email]
  S2 --> S3[Step 3<br/>trial dates]
  S3 --> S4[Step 4<br/>portfolio repo]
  S4 --> S5[Step 5<br/>README]
  S5 --> S6[Step 6<br/>allowlist + copy]
  S6 --> S7[Step 7<br/>check, go public]
  S7 --> S8[Step 8<br/>save in Git]

What you need

  • Your course folder from Unit 1, with gh signed in to GitHub (gh auth status says you are logged in).
  • The dates you started your SAP BTP trial and your generative AI hub trial, if you have them. The welcome emails show them.
  • 45 to 75 minutes.
  • Cost: free.

Step 1: Open your course folder and turn on the virtual environment

  1. Open VS Code, choose File > Open Folder, and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. If the prompt doesn't start with (.venv), turn it on:

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate
  4. Create the folder for this unit:

    mkdir unit14

Step 2: Keep your email address private on public commits

Every Git commit records an email address. In a public repository, anyone can read it. GitHub gives you a no-reply address to use instead.

  1. On github.com, click your profile picture in the upper-right corner, then Settings.
  2. In the Access section of the left sidebar, click Emails.
  3. Tick Keep my email addresses private.
  4. On the same page, find the no-reply address GitHub shows you and copy it. You will use it in Step 4.

What success looks like: the Emails page shows Keep my email addresses private ticked, and you have the no-reply address copied.

Step 3: Write your trial dates into .env

  1. Open .env in the course folder.

  2. Add these lines at the end, with your own dates in YYYY-MM-DD form. Leave out a line for a trial you never started.

    BTP_TRIAL_START=2026-07-20
    GENAI_HUB_TRIAL_START=2026-09-01
  3. Save the file.

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.

  1. Go to your home folder. The same command works in PowerShell and in macOS/Linux terminals:

    cd ~
  2. 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.

  3. 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
  4. 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"
  5. Create a .gitignore for the portfolio, so a local .env or .venv there can never be committed:

    • Windows (PowerShell):

      Set-Content .gitignore ".env`n.env.*`n.venv/`n__pycache__/"
    • macOS / Linux:

      printf '.env\n.env.*\n.venv/\n__pycache__/\n' > .gitignore
  6. Go back to your course folder:

    cd ~/orchestrate-course

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.

Step 5: Write the portfolio README

The README is the first thing a visitor reads. Keep it short and honest.

  1. 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.

Step 6: Choose what to publish and copy it

  1. 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
  2. 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.")
  1. 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.

  1. Copy for real:

    python publish_portfolio.py --copy
  2. 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.

  1. 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.")
  1. 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.

  1. 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?

  2. 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
  3. Run python check_unit14.py --skip-earlier again. The last portfolio line now reads OK visibility on GitHub: PUBLIC.

Step 8: Save your work in Git

  1. In the course folder, check what Git sees:

    git status

    You should see check_unit14.py, publish_portfolio.py and portfolio.txt. You must not see .env.

  2. 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.

How the code works

Part What it does
EARLIER The thirteen units' check scripts, in course order, with the topic that creates each
run_check() Runs one earlier check with the same Python and counts its MISSING and LATER lines
--need Names the units your capstone uses; their failures become MISSING
TRIALS Each trial's .env line, its length in days, and the units that use it
SECRET_PATTERNS Text that looks like a key, a token, a private key or an SAP service key ("clientsecret")
git(folder, ...) Runs Git inside the portfolio folder without changing the current folder
gh repo view --json visibility Asks GitHub whether the portfolio is public; the only network call the new code makes
portfolio.txt The allowlist: only listed files and folders are considered
why_not() Refuses .env files, files over 50 MiB and files with secret-looking text
SKIP_FOLDERS Folders never published, even inside a listed folder: .git, .venv, caches, node_modules
--copy Without it, the script only shows what it would do; it never deletes anything

The 50 MiB and 100 MiB limits come from GitHub's documentation: Git warns above 50 MiB, and GitHub blocks files larger than 100 MiB.

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed, or the terminal can't find it Windows: repeat Unit 1, Step 1, then open a new terminal. macOS/Linux: use python3 until .venv is active
MISSING virtual environment is active .venv isn't turned on, so libraries from earlier units look missing Run Step 1, item 3, then run the check again
MISSING gh (GitHub CLI) gh isn't installed, or the terminal was opened before it was Install it as in Git and GitHub for AI engineers, then open a new terminal
gh repo create says you aren't logged in gh has no valid sign-in Run gh auth login and follow the browser prompts
gh repo create says the name already exists You already have a repository called sap-ai-portfolio Use it if it's yours and clean, or pick another name and set PORTFOLIO_DIR in .env
No portfolio repository at ... The script can't find the folder Check Step 4 created it next to orchestrate-course, or set PORTFOLIO_DIR
SKIP ... (secret-looking text on line N) A file in the allowlist looks like it holds a key Open the file at that line. Move the key to .env and read it with load_dotenv(). If it was ever committed, rotate the key
git push is rejected with a message about a secret GitHub's push protection found a known secret format Don't bypass it. Remove the secret from the commit, rotate the key, and push again
MISSING Unit 05: check_unit05.py has N item(s) to fix Your capstone needs Unit 5, and its setup has gaps Run python check_unit05.py and fix its MISSING lines, using that unit's setup topic
A trial line says ended N day(s) ago The trial is over and its data is gone Start a new trial as in its setup topic, rebuild what the capstone needs, update the date in .env
An earlier check didn't finish in 300 s That check is waiting on the network or a stopped service Run it on its own to see where it stops; a proxy or a stopped HANA Cloud instance are common causes
Network or proxy errors in earlier checks or gh A company network blocks GitHub or SAP sites Try another network, or ask IT to allow the sites named in the error

Where this shows up in SAP

  • 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
What you may show Anything you wrote on made-up data Only what your contract and the client allow

Production concerns

  • 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.

Pitfalls

  • Copying the whole course folder. It brings logs, notebooks with output, and every half-finished file. Publish by allowlist.
  • Bypassing push protection. If GitHub blocks a push, a secret is in it. Remove and rotate it; don't click past the warning.
  • Running the check without .venv. Every earlier check reports missing libraries. Turn on .venv first.
  • Ignoring LATER lines for units you need. Use --need so the check treats them as must-fix.
  • Forgetting the trial dates. A 30-day trial started early in Unit 5 is long gone by Unit 14. Write the dates down and check before you plan.
  • Leaving the README as a placeholder. A visitor decides in seconds whether to read on. Name the problem and the KPI.

Exercise

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.

  1. 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
  2. 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
  3. 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.

  4. 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.

  5. 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.

  6. In orchestrate-course/unit14/, create fresh_clone_test.md with three lines: the date, the commands you ran, and what you had to fix.

  7. Delete the test folder when you're done. It is a copy; your work is safe on GitHub and in sap-ai-portfolio.

  8. 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.

Check yourself

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.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in