From 987db96c153c7b759e19b0677141199bc455feeb Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Sun, 5 Jul 2026 09:50:47 +0200 Subject: [PATCH 01/28] bk: fail fast on invalid BK_STREAM, persist env for new terminals - abort with an example when BK_STREAM is unset instead of continuing - abort with the list of available streams when content/$BK_STREAM does not exist - persist BK_DIR, vars.sh/vars.local.sh sourcing, PROJECT_NUMBER and BK_INITIALIZED in ~/.bashrc so that new terminals no longer require re-running '. bk'; BK_INITIALIZED/PROJECT_NUMBER are only persisted once a valid PROJECT_ID is configured because the stream bootstrap scripts guard on them --- .scripts/bk | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.scripts/bk b/.scripts/bk index 94c016f..5553981 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -58,6 +58,8 @@ fi if [ -z $BK_STREAM ]; then err 'Variable BK_STREAM is not set. Please set it to the stream you want to start' + err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' + return 1 fi export BK_GITHUB_USERNAME=$(echo $BK_REPO | cut -d/ -f1) # first part of GoogleCloudPlatform/bootkon @@ -84,6 +86,12 @@ fi cd $BK_GITHUB_REPOSITORY +if [ ! -d "content/${BK_STREAM}" ]; then + err "Stream '${BK_STREAM}' does not exist (no content/${BK_STREAM} directory)." + err "Available streams: $(ls content | grep -vx common | tr '\n' ' ')" + return 1 +fi + NEW_PATH=~/${BK_GITHUB_REPOSITORY}/.scripts PATH_EXPORT_LINE="export PATH=\${HOME}/${BK_GITHUB_REPOSITORY}/.scripts:\$PATH" @@ -175,6 +183,51 @@ else echo "$line" >> ~/.bashrc fi +## Persist the environment in ~/.bashrc so that new terminals +## work without re-running `. bk`. +line="export BK_DIR=\${HOME}/${BK_GITHUB_REPOSITORY}" +if grep -q '^export BK_DIR=' ~/.bashrc; then + if ! grep -Fxq "$line" ~/.bashrc; then + sed -i "s|^export BK_DIR=.*|$line|" ~/.bashrc + echo "Updated the existing BK_DIR line in ~/.bashrc." + fi +else + echo "Adding BK_DIR to .bashrc" + echo "$line" >> ~/.bashrc +fi + +for line in \ + 'if [ -f ${BK_DIR}/vars.sh ]; then source ${BK_DIR}/vars.sh; fi' \ + 'if [ -f ${BK_DIR}/vars.local.sh ]; then source ${BK_DIR}/vars.local.sh; fi' +do + if ! grep -Fxq "$line" ~/.bashrc; then + echo "Adding to .bashrc: $line" + echo "$line" >> ~/.bashrc + fi +done + +## Only mark the environment as initialized in ~/.bashrc once a valid +## PROJECT_ID is configured: the stream bootstrap scripts guard on +## BK_INITIALIZED and rely on PROJECT_NUMBER. +if [ ! -z "$PROJECT_ID" ]; then + line="export PROJECT_NUMBER=${PROJECT_NUMBER}" + if grep -q '^export PROJECT_NUMBER=' ~/.bashrc; then + if ! grep -Fxq "$line" ~/.bashrc; then + sed -i "s|^export PROJECT_NUMBER=.*|$line|" ~/.bashrc + echo "Updated the existing PROJECT_NUMBER line in ~/.bashrc." + fi + else + echo "Adding PROJECT_NUMBER to .bashrc" + echo "$line" >> ~/.bashrc + fi + + line='export BK_INITIALIZED=1' + if ! grep -Fxq "$line" ~/.bashrc; then + echo "Adding BK_INITIALIZED to .bashrc" + echo "$line" >> ~/.bashrc + fi +fi + unset line echo From 68f7e8fb55fa7f6de09513b04f24911cfdc7edd0 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Wed, 8 Jul 2026 21:45:05 +0200 Subject: [PATCH 02/28] bk: consolidate per-participant env into vars.local.sh Move all per-participant state into a single git-ignored vars.local.sh instead of scattering per-variable exports across ~/.bashrc, and reduce ~/.bashrc to one guarded block that sources it. Autodetect PROJECT_ID and GCP_USERNAME from the Cloud Shell environment (filling only empty values), so most participants no longer edit any config by hand. `. bk` is dual-mode: full bootstrap on first run, fast reload afterwards. - add bk-set-var: idempotent export upserter, atomic (same-dir temp + mv) so an interrupted write can't truncate the file's generated secrets - add bk-remove-block: robust ~/.bashrc block remover -- CRLF-safe, keeps an unterminated block instead of deleting to EOF, strips trailing blank lines - fixed clone path (~/bootkon); derive the repo identity from the checkout's git origin so rendered image links always match the files on disk - persist seed vars and install the ~/.bashrc block before validating the project, and mark the shell initialized unconditionally, so a failed or empty project still leaves new terminals working and still opens the tutorial - surface expired-auth as a likely cause of a failed project check - move agenticdata stream config (secrets + runtime env) into vars.local.sh - chmod 600 vars.local.sh (it holds generated database passwords) - update tutorials and contributor docs for the new flow --- .scripts/bk | 346 +++++++++--------- .scripts/bk-deactivate | 28 +- .scripts/bk-info | 14 +- .scripts/bk-remove-block | 61 +++ .scripts/bk-set-var | 105 ++++++ content/agenticdata/TUTORIAL.md | 18 +- content/agenticdata/bk-bootstrap | 48 +-- .../agenticdata/labs/1_environment_setup.md | 2 +- .../src/adk/cymbal_analyst/a2a_server.py | 2 +- .../src/adk/cymbal_analyst/agent.py | 4 +- .../src/adk/cymbal_concierge/agent.py | 2 +- content/agents/TUTORIAL.md | 18 +- content/contributing/TUTORIAL.md | 19 +- content/data/TUTORIAL.md | 18 +- content/devex/TUTORIAL.md | 18 +- vars.sh | 18 +- 16 files changed, 462 insertions(+), 259 deletions(-) create mode 100755 .scripts/bk-remove-block create mode 100755 .scripts/bk-set-var diff --git a/.scripts/bk b/.scripts/bk index 5553981..f655af0 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -1,24 +1,35 @@ #!/bin/bash -# This script initializes the Data & AI Bootkon environment in Cloud Shell. -# It clones the Bootkon repository (or uses an existing one), sets up necessary -# environment variables, installs required packages, renders the tutorial markdown file, -# and finally opens the tutorial in Cloud Shell. It also adds helpful aliases and -# configurations to the .bashrc file for easier future access and customization. +# This script initializes the Bootkon environment in Cloud Shell. +# It clones the Bootkon repository (or uses an existing one), sets up the +# environment variables, installs required packages, renders the tutorial +# markdown file, and finally opens the tutorial in Cloud Shell. +# +# All per-participant state lives in a single git-ignored file, +# $BK_DIR/vars.local.sh (created from vars.sh on first run). A minimal guarded +# block in ~/.bashrc sources that file so every new terminal picks up the +# environment automatically -- no need to re-run this script per terminal. +# +# `. bk` has two modes: +# * First run (not yet initialized): clone, create vars.local.sh, autodetect +# PROJECT_ID/GCP_USERNAME, render and open the tutorial. +# * Re-run (already initialized): a fast reload -- re-source vars.local.sh, +# fill still-empty config from the environment, refresh PROJECT_NUMBER and +# the active gcloud project. Handy after editing vars.local.sh. # # Author: Fabian Hirschmann # # Usage: # From GitHub (replace ): -# BK_REPO=; . <(wget -qO- https://raw.githubusercontent.com//main/.scripts/bk) +# BK_STREAM= BK_REPO=; . <(wget -qO- https://raw.githubusercontent.com//main/.scripts/bk) # Locally: -# BK_REPO= . bk +# BK_STREAM= BK_REPO= . bk # Defaults, repo as argument: -# . bk -# -# Skip workspace opening (add to ~/.bashrc above bk): +# BK_STREAM= . bk # # Environment variables: -# BK_REPO: GitHub repository (). +# BK_REPO: GitHub repository (). +# BK_STREAM: stream to start (a directory under content/). +# BK_DIR: checkout location (defaults to ~/bootkon). RED='\033[0;31m' GREEN='\033[0;32m' @@ -34,57 +45,62 @@ err() { echo -e "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" -if [ -z $CLOUD_SHELL ]; then +if [ -z "$CLOUD_SHELL" ]; then err 'Please run this script in Cloud Shell.' return 1 fi if [ "${BASH_SOURCE[0]}" == "$0" ]; then err 'Script is not sourced. Please source it.' - err 'Example: BK_REPO=GoogleCloudPlatform/bootkon . bk' + err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' exit 1 fi -if [ -z "$1" ]; then - if [ -z $BK_REPO ]; then - err 'Variable BK_REPO is not set. Please set it to a GitHub username and repository' - err 'Example: BK_REPO=GoogleCloudPlatform/bootkon . bk' - return 1 - fi -else - export BK_REPO=$1 +if [ -n "$1" ]; then + export BK_REPO="$1" echo -e "Setting BK_REPO to $BK_REPO based on first argument to this script." fi -if [ -z $BK_STREAM ]; then +if [ -z "$BK_REPO" ]; then + err 'Variable BK_REPO is not set. Please set it to a GitHub username and repository' + err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' + return 1 +fi + +if [ -z "$BK_STREAM" ]; then err 'Variable BK_STREAM is not set. Please set it to the stream you want to start' err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' return 1 fi -export BK_GITHUB_USERNAME=$(echo $BK_REPO | cut -d/ -f1) # first part of GoogleCloudPlatform/bootkon -export BK_GITHUB_REPOSITORY=$(echo $BK_REPO | cut -d/ -f2) # second part of GoogleCloudPlatform/bootkon -export BK_REPO_URL="https://github.com/${BK_REPO}.git" -export BK_STREAM="${BK_STREAM}" -export BK_DIR=~/${BK_GITHUB_REPOSITORY} -export BK_INITIALIZED=1 +# Remember whether this is a first run or a reload (before we set the flag), and +# the explicitly requested repo/stream (env or argument) so they can win over a +# stale persisted value further below. +BK_WAS_INITIALIZED="${BK_INITIALIZED:-}" +BK_REQ_REPO="$BK_REPO" +BK_REQ_STREAM="$BK_STREAM" -cd ~/ +export BK_REPO="$BK_REPO" +export BK_STREAM="$BK_STREAM" +export BK_REPO_URL="https://github.com/${BK_REPO}.git" +# Always clone to a fixed location so the ~/.bashrc block is static. Override +# BK_DIR (e.g. contributors juggling several checkouts) is honored. +export BK_DIR="${BK_DIR:-$HOME/bootkon}" if ! command -v git &> /dev/null; then sudo apt update sudo apt install -y git fi -if [ -d $BK_GITHUB_REPOSITORY ]; then - echo -e "${GREEN}Not cloning $BK_REPO_URL because folder ~/$BK_GITHUB_REPOSITORY already exists.${NC}" +if [ -d "$BK_DIR/.git" ]; then + echo -e "${GREEN}Not cloning $BK_REPO_URL because $BK_DIR already exists.${NC}" else BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten - echo -e "Cloning branch $BK_BRANCH from $BK_REPO_URL into $BK_GITHUB_REPOSITORY..." - git clone --branch "$BK_BRANCH" https://github.com/${BK_REPO}.git + echo -e "Cloning branch $BK_BRANCH from $BK_REPO_URL into $BK_DIR..." + git clone --branch "$BK_BRANCH" "$BK_REPO_URL" "$BK_DIR" fi -cd $BK_GITHUB_REPOSITORY +cd "$BK_DIR" if [ ! -d "content/${BK_STREAM}" ]; then err "Stream '${BK_STREAM}' does not exist (no content/${BK_STREAM} directory)." @@ -92,157 +108,159 @@ if [ ! -d "content/${BK_STREAM}" ]; then return 1 fi -NEW_PATH=~/${BK_GITHUB_REPOSITORY}/.scripts -PATH_EXPORT_LINE="export PATH=\${HOME}/${BK_GITHUB_REPOSITORY}/.scripts:\$PATH" - -# 1. Add to current session's PATH if missing -if [[ ":$PATH:" != *":$NEW_PATH:"* ]]; then - echo -e "${MAGENTA}Adding $NEW_PATH to your current session's PATH${NC}" - export PATH=${NEW_PATH}:$PATH -else - echo -e "${GREEN}Your current session's PATH already contains $NEW_PATH. Not adding it again.${NC}" +# Add the scripts directory to the current session's PATH (idempotent). The +# permanent export lives in the ~/.bashrc block written further below. +BK_SCRIPTS="$BK_DIR/.scripts" +case ":$PATH:" in + *":$BK_SCRIPTS:"*) ;; + *) echo -e "${MAGENTA}Adding $BK_SCRIPTS to your current session's PATH${NC}" + export PATH="$BK_SCRIPTS:$PATH" ;; +esac + +LOCAL="$BK_DIR/vars.local.sh" +BK_SET_VAR="$BK_SCRIPTS/bk-set-var" +BK_REMOVE_BLOCK="$BK_SCRIPTS/bk-remove-block" + +# vars.local.sh is the single per-participant config + state file. Seed it from +# the checked-in template on first run; it is git-ignored, never pushed, and +# holds generated secrets, so keep it private. +if [ ! -f "$LOCAL" ]; then + echo -e "Creating $(readlink -f "$LOCAL") from vars.sh" + cp "$BK_DIR/vars.sh" "$LOCAL" + chmod 600 "$LOCAL" fi -# 2. Persist the PATH setting in ~/.bashrc if not already there -if ! grep -qF "$PATH_EXPORT_LINE" ~/.bashrc ; then - echo -e "${MAGENTA}Adding $NEW_PATH to ~/.bashrc for future sessions.${NC}" - # Use '>>' to append the line to the file - echo "$PATH_EXPORT_LINE" >> ~/.bashrc +echo -e "Sourcing $(readlink -f "$BK_DIR/vars.sh")" +source "$BK_DIR/vars.sh" +echo -e "Sourcing $(readlink -f "$LOCAL")" +source "$LOCAL" + +# Autodetect PROJECT_ID / GCP_USERNAME from the Cloud Shell environment, but +# only fill values that are still empty -- the just-sourced value is the source +# of truth, so a manual edit in vars.local.sh always wins. +detected_project="${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}" +detected_account="$(gcloud config get-value account 2>/dev/null || true)" +[ "$detected_project" = "(unset)" ] && detected_project="" +[ "$detected_account" = "(unset)" ] && detected_account="" +if [ -z "$PROJECT_ID" ] && [ -n "$detected_project" ]; then + "$BK_SET_VAR" "$LOCAL" PROJECT_ID "$detected_project" +fi +if [ -z "$GCP_USERNAME" ] && [ -n "$detected_account" ]; then + "$BK_SET_VAR" "$LOCAL" GCP_USERNAME "$detected_account" +fi +# Reload so this shell sees any freshly detected values. +source "$LOCAL" + +# Derive the repo identity AFTER the last source (so the persisted values can't +# clobber it) and from the checkout's own origin remote, so BK_REPO and the +# rendered image links always match the files actually on disk -- even if +# vars.local.sh is stale or a fork was requested against an existing checkout. +_origin="$(git -C "$BK_DIR" remote get-url origin 2>/dev/null || true)" +_actual="$(printf '%s' "$_origin" | sed -E 's#^git@github\.com:##; s#^https?://github\.com/##; s#\.git$##')" +if [ -n "$_actual" ]; then + if [ "$_actual" != "$BK_REQ_REPO" ]; then + echo -e "${YELLOW}Note: $BK_DIR is a checkout of $_actual, not $BK_REQ_REPO.${NC}" + echo -e "${YELLOW}Using $_actual; set BK_DIR to another path (or remove $BK_DIR) to use $BK_REQ_REPO.${NC}" + fi + BK_REPO="$_actual" else - echo -e "${GREEN}The permanent PATH export for $NEW_PATH is already in ~/.bashrc. Skipping.${NC}" + BK_REPO="$BK_REQ_REPO" fi - -unset NEW_PATH -unset PATH_EXPORT_LINE - -echo -e "Sourcing $(readlink -f $BK_DIR/vars.sh)" -source $BK_DIR/vars.sh - -if [ -f $BK_DIR/vars.local.sh ]; then - echo -e "Sourcing $(readlink -f $BK_DIR/vars.local.sh)" - source $BK_DIR/vars.local.sh +export BK_REPO +export BK_GITHUB_USERNAME="$(echo "$BK_REPO" | cut -d/ -f1)" +export BK_GITHUB_REPOSITORY="$(echo "$BK_REPO" | cut -d/ -f2)" +export BK_REPO_URL="https://github.com/${BK_REPO}.git" +# The explicitly requested stream wins over any persisted value (stream switch). +export BK_STREAM="$BK_REQ_STREAM" + +# BK_BRANCH is derived from the checkout. Fall back cleanly for a detached HEAD +# ('HEAD') or a non-git directory (empty), which are not valid clone targets. +_branch="$(git -C "$BK_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" +if [ -z "$_branch" ] || [ "$_branch" = "HEAD" ]; then + _branch="${BK_BRANCH:-main}" fi +export BK_BRANCH="$_branch" + +echo -e "Config: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP_USERNAME${NC} REGION=${YELLOW}$REGION${NC}" + +# Persist the managed variables and install the ~/.bashrc block BEFORE the +# project is validated below: even if validation fails (expired auth, wrong +# project) a freshly opened terminal must still have PATH and the seed vars. +"$BK_SET_VAR" "$LOCAL" BK_STREAM "$BK_STREAM" +"$BK_SET_VAR" "$LOCAL" BK_REPO "$BK_REPO" +"$BK_SET_VAR" "$LOCAL" BK_BRANCH "$BK_BRANCH" +"$BK_SET_VAR" "$LOCAL" BK_GITHUB_USERNAME "$BK_GITHUB_USERNAME" +"$BK_SET_VAR" "$LOCAL" BK_GITHUB_REPOSITORY "$BK_GITHUB_REPOSITORY" +"$BK_SET_VAR" "$LOCAL" BK_REPO_URL "$BK_REPO_URL" + +# Install/refresh a single guarded block in ~/.bashrc so every new terminal +# loads the environment by sourcing vars.local.sh. bk-remove-block strips any +# existing block robustly; then we append a fresh one. +BASHRC=~/.bashrc +"$BK_REMOVE_BLOCK" "$BASHRC" 2>/dev/null || true +cat >> "$BASHRC" <>> bootkon >>> +# Managed by bk (.scripts/bk). Loads the bootkon environment in every new +# terminal. Edit values in \$BK_DIR/vars.local.sh, not here. +export BK_DIR="$BK_DIR" +case ":\$PATH:" in *":\$BK_DIR/.scripts:"*) ;; *) export PATH="\$BK_DIR/.scripts:\$PATH" ;; esac +[ -f "\$BK_DIR/vars.local.sh" ] && source "\$BK_DIR/vars.local.sh" +# <<< bootkon <<< +EOF + +# Mark the environment initialized in this shell so bk-start opens the tutorial +# even before a project is set (the tutorial is what guides the user to set it). +export BK_INITIALIZED=1 -echo -e "Variables from $BK_DIR/vars.sh: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP_USERNAME${NC} REGION=${YELLOW}$REGION${NC}" - - -if [ -z $PROJECT_ID ]; then +if [ -z "$PROJECT_ID" ]; then echo -e "The variable PROJECT_ID is empty. Not setting the default Google Cloud project." else - gcloud projects get-iam-policy $PROJECT_ID --quiet 2>>/dev/null 1>>/dev/null - if [ $? -ne 0 ]; then - echo -en "${RED}ERROR: Unable to get iam policy for project $PROJECT_ID. " - echo -e "Are you sure you set your PROJECT_ID in vars.sh correctly?${NC}" + if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then + echo -e "${RED}ERROR: Unable to access project $PROJECT_ID.${NC}" + echo -e "${RED}Two common causes:${NC}" + echo -e "${RED} 1. Your session needs re-authentication (typical after a break) —${NC}" + echo -e "${RED} re-authorize Cloud Shell when prompted, or run 'gcloud auth login', then '. bk' again.${NC}" + echo -e "${RED} 2. PROJECT_ID in vars.local.sh is wrong.${NC}" return 1 fi - gcloud config set project $PROJECT_ID --quiet 2>>/dev/null + gcloud config set project "$PROJECT_ID" --quiet 2>/dev/null - export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)") + export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" echo -e "Project for gcloud CLI has been set: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." -fi -## Set or update $BK_REPO in ~/.bashrc -line="export BK_REPO=${BK_REPO}" -if grep -q '^export BK_REPO=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_REPO=.*|$line|" ~/.bashrc - echo "Updated the existing BK_REPO line in ~/.bashrc." + # Persist project-derived state only once it is valid: the stream bootstrap + # scripts guard on BK_INITIALIZED and rely on PROJECT_NUMBER. + if [ -n "$PROJECT_NUMBER" ]; then + "$BK_SET_VAR" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" + "$BK_SET_VAR" "$LOCAL" BK_INITIALIZED 1 fi -else - echo "Adding BK_REPO to .bashrc" - echo "$line" >> ~/.bashrc fi -## Set or update $BK_STREAM in ~/.bashrc -line="export BK_STREAM=${BK_STREAM}" -if grep -q '^export BK_STREAM=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_STREAM=.*|$line|" ~/.bashrc - echo "Updated the existing BK_STREAM line in ~/.bashrc." - fi +unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS +unset detected_project detected_account _origin _actual _branch +unset BK_REQ_REPO BK_REQ_STREAM + +if [ -z "$BK_WAS_INITIALIZED" ]; then + echo + echo -e " __ ---------------------------------------" + echo -e " _(\ |${RED}@@${NC}| | |" + echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" + echo -e " \___|----| | __ | |" + echo -e " \ }{ /\ )_ / _\ ---------------------------------------" + echo -e " /\__/\ \__O (__" + echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." + echo -e " _)( )(_ This will open the tutorial and IDE workspace." + echo -e ' `---''---`' + echo else - echo "Adding BK_STREAM=$BK_STREAM to .bashrc" - echo "$line" >> ~/.bashrc + echo -e "${GREEN}Reloaded bootkon environment${NC} (BK_STREAM=${YELLOW}$BK_STREAM${NC} BK_BRANCH=${YELLOW}$BK_BRANCH${NC})." fi - -## Set or update $BK_BRANCH in ~/.bashrc -export BK_BRANCH=$(git rev-parse --abbrev-ref HEAD) -line="export BK_BRANCH=${BK_BRANCH}" -if grep -q '^export BK_BRANCH=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_BRANCH=.*|$line|" ~/.bashrc - echo "Updated the existing BK_BRANCH line in ~/.bashrc." - fi -else - echo "Adding BK_BRANCH=$BK_BRANCH to .bashrc" - echo "$line" >> ~/.bashrc -fi - -## Persist the environment in ~/.bashrc so that new terminals -## work without re-running `. bk`. -line="export BK_DIR=\${HOME}/${BK_GITHUB_REPOSITORY}" -if grep -q '^export BK_DIR=' ~/.bashrc; then - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_DIR=.*|$line|" ~/.bashrc - echo "Updated the existing BK_DIR line in ~/.bashrc." - fi -else - echo "Adding BK_DIR to .bashrc" - echo "$line" >> ~/.bashrc -fi - -for line in \ - 'if [ -f ${BK_DIR}/vars.sh ]; then source ${BK_DIR}/vars.sh; fi' \ - 'if [ -f ${BK_DIR}/vars.local.sh ]; then source ${BK_DIR}/vars.local.sh; fi' -do - if ! grep -Fxq "$line" ~/.bashrc; then - echo "Adding to .bashrc: $line" - echo "$line" >> ~/.bashrc - fi -done - -## Only mark the environment as initialized in ~/.bashrc once a valid -## PROJECT_ID is configured: the stream bootstrap scripts guard on -## BK_INITIALIZED and rely on PROJECT_NUMBER. -if [ ! -z "$PROJECT_ID" ]; then - line="export PROJECT_NUMBER=${PROJECT_NUMBER}" - if grep -q '^export PROJECT_NUMBER=' ~/.bashrc; then - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export PROJECT_NUMBER=.*|$line|" ~/.bashrc - echo "Updated the existing PROJECT_NUMBER line in ~/.bashrc." - fi - else - echo "Adding PROJECT_NUMBER to .bashrc" - echo "$line" >> ~/.bashrc - fi - - line='export BK_INITIALIZED=1' - if ! grep -Fxq "$line" ~/.bashrc; then - echo "Adding BK_INITIALIZED to .bashrc" - echo "$line" >> ~/.bashrc - fi -fi - -unset line - -echo -echo -e " __ ---------------------------------------" -echo -e " _(\ |${RED}@@${NC}| | |" -echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" -echo -e " \___|----| | __ | |" -echo -e " \ }{ /\ )_ / _\ ---------------------------------------" -echo -e " /\__/\ \__O (__" -echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." -echo -e " _)( )(_ This will open the tutorial and IDE workspace." -echo -e ' `---''---`' -echo +unset BK_WAS_INITIALIZED if [ "$(basename ${BASH_SOURCE[0]})" != "bk" ]; then # This script is run the first time from GitHub bk-start -fi +fi diff --git a/.scripts/bk-deactivate b/.scripts/bk-deactivate index 91cb7d6..c4de44e 100755 --- a/.scripts/bk-deactivate +++ b/.scripts/bk-deactivate @@ -1,13 +1,25 @@ #!/bin/bash +# Removes the bootkon block from ~/.bashrc so new terminals no longer load the +# environment automatically. vars.local.sh and the checkout are left untouched; +# re-run `. bk` to reactivate. -line='if [ -f ${BK_INIT_SCRIPT} ]; then source ${BK_INIT_SCRIPT}; fi' +BASHRC=~/.bashrc +START='# >>> bootkon >>>' +REMOVE="${BK_DIR:-$HOME/bootkon}/.scripts/bk-remove-block" -# Check if the line exists in the file -if grep -qF "$line" ~/.bashrc; then - sed -i "/$(echo "$line" | sed 's/[]\\/.*^$[]/\\&/g')/d" ~/.bashrc - echo "SUCCESS: The following line has been removed from ~/.bashrc:" -else - echo "ERROR: The following line has not been found in ~/.bashrc:" +if [ ! -f "$BASHRC" ] || ! grep -qF "$START" "$BASHRC"; then + echo "No bootkon block found in ~/.bashrc; nothing to do." + exit 0 fi -echo $line \ No newline at end of file +"$REMOVE" "$BASHRC" + +if grep -qF "$START" "$BASHRC"; then + echo "ERROR: could not cleanly remove the bootkon block from ~/.bashrc" >&2 + echo "(the block may be missing its end marker). Please check the file by hand." >&2 + exit 1 +fi + +echo "Removed the bootkon block from ~/.bashrc." +echo "Open a new terminal for a clean environment (this one keeps the variables" +echo "until you 'unset BK_INITIALIZED' or start a new shell)." diff --git a/.scripts/bk-info b/.scripts/bk-info index f6e9d65..d1387e1 100755 --- a/.scripts/bk-info +++ b/.scripts/bk-info @@ -11,19 +11,23 @@ echo -e "${CYAN}Variables:${NC}" echo -e "BK_GITHUB_USERNAME: ${GREEN}$BK_GITHUB_USERNAME${NC}" echo -e "BK_GITHUB_REPOSITORY: ${GREEN}$BK_GITHUB_REPOSITORY${NC}" echo -e "BK_REPO_URL: ${GREEN}$BK_REPO_URL${NC}" -echo -e "BK_TUTORIAL: ${GREEN}$BK_TUTORIAL${NC}" echo -e "BK_DIR: ${GREEN}$BK_DIR${NC}" -echo -e "BK_INIT_SCRIPT: ${GREEN}$BK_INIT_SCRIPT${NC}" +echo -e "BK_STREAM: ${GREEN}$BK_STREAM${NC}" +echo -e "BK_BRANCH: ${GREEN}$BK_BRANCH${NC}" echo -e "BK_INITIALIZED: ${GREEN}$BK_INITIALIZED${NC}" +echo -e "config file: ${GREEN}$BK_DIR/vars.local.sh${NC}" +echo -e "PROJECT_ID: ${GREEN}$PROJECT_ID${NC}" +echo -e "PROJECT_NUMBER: ${GREEN}$PROJECT_NUMBER${NC}" +echo -e "REGION: ${GREEN}$REGION${NC}" echo echo -e "${CYAN}Info:${NC}" -if [ ! -d "data" ]; then +if [ ! -d "${BK_DIR}/data" ]; then echo -e "data: ${RED}does not exist${NC}" else echo -e "data: ${GREEN}exists${NC}" - du -sh data + du -sh "${BK_DIR}/data" fi -echo -e "git origin: ${GREEN}$(git config --get remote.origin.url)${NC}" \ No newline at end of file +echo -e "git origin: ${GREEN}$(git -C "${BK_DIR}" config --get remote.origin.url 2>/dev/null)${NC}" \ No newline at end of file diff --git a/.scripts/bk-remove-block b/.scripts/bk-remove-block new file mode 100755 index 0000000..0e569d7 --- /dev/null +++ b/.scripts/bk-remove-block @@ -0,0 +1,61 @@ +#!/bin/bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# bk-remove-block FILE +# +# Remove the bootkon-managed block(s) from FILE. Shared by `bk` (which strips +# then re-appends a fresh block) and `bk-deactivate`. Robust by design: +# * markers are matched ignoring trailing whitespace / CR (CRLF-safe); +# * a start marker with no matching end (truncated/corrupted file) is KEPT, +# not deleted to EOF -- no data loss; +# * trailing blank lines are stripped so repeated `. bk` reloads do not +# accumulate whitespace; +# * the rewrite is atomic (temp file in the SAME directory, then mv) and +# preserves the original file's permissions. + +set -eu + +file="${1:?bk-remove-block: FILE required}" +[ -f "$file" ] || exit 0 + +start='# >>> bootkon >>>' +end='# <<< bootkon <<<' + +dir="$(dirname "$file")" +tmp="$(mktemp "$dir/.bootkon.XXXXXX")" +chmod --reference="$file" "$tmp" 2>/dev/null || true + +awk -v s="$start" -v e="$end" ' + function trim(x) { sub(/[[:space:]]+$/, "", x); return x } + { + if (!inblk) { + if (trim($0) == s) { inblk = 1; buf = $0; next } + out[++n] = $0; next + } + buf = buf "\n" $0 + if (trim($0) == e) { inblk = 0; buf = ""; next } + next + } + END { + if (inblk) { # unterminated block: keep it verbatim + m = split(buf, a, "\n") + for (i = 1; i <= m; i++) out[++n] = a[i] + } + while (n > 0 && trim(out[n]) == "") n-- # strip trailing blank lines + for (i = 1; i <= n; i++) print out[i] + } +' "$file" > "$tmp" + +mv "$tmp" "$file" diff --git a/.scripts/bk-set-var b/.scripts/bk-set-var new file mode 100755 index 0000000..1ac3076 --- /dev/null +++ b/.scripts/bk-set-var @@ -0,0 +1,105 @@ +#!/bin/bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# bk-set-var — idempotently upsert an `export NAME='VALUE'` line into a file. +# +# Usage: +# bk-set-var [--if-empty] FILE NAME VALUE +# +# --if-empty Only write if NAME is currently unset or empty in FILE. +# Existing non-empty values are left untouched. Used for +# autodetected config (respect manual edits) and for +# once-generated secrets (never regenerate). +# +# The file is created if missing. An existing `export NAME=...` line is +# replaced in place (order preserved, duplicates collapsed); otherwise the +# line is appended. This one helper replaces the repetitive grep/sed/echo +# blocks that used to litter `bk` and the stream bootstraps. + +set -eu + +if_empty=0 +if [ "${1:-}" = "--if-empty" ]; then + if_empty=1 + shift +fi + +file="${1:?bk-set-var: FILE required}" +name="${2:?bk-set-var: NAME required}" +value="${3-}" + +# Guard: NAME must be a plain shell identifier. +case "$name" in + [!A-Za-z_]* | *[!A-Za-z0-9_]*) + echo "bk-set-var: invalid variable name '$name'" >&2 + exit 1 + ;; +esac + +# A newline would be split across physical lines and corrupt the file on the +# next upsert; no caller needs it, so reject it outright. +case "$value" in + *" +"*) + echo "bk-set-var: value for '$name' may not contain a newline" >&2 + exit 1 + ;; +esac + +[ -f "$file" ] || touch "$file" + +match="^[[:space:]]*export[[:space:]]+${name}=" + +if [ "$if_empty" -eq 1 ]; then + # Skip if a non-empty value is already present. Parse robustly: strip the + # `export NAME=` prefix, a trailing ` # comment`, surrounding whitespace and + # quotes. (Values in this project never contain a literal '#'.) + current_line="$(grep -E "$match" "$file" | tail -n1 || true)" + if [ -n "$current_line" ]; then + current_value="$(printf '%s' "$current_line" | sed -E \ + 's/^[^=]*=//; s/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/')" + [ -n "$current_value" ] && exit 0 + fi + # Nothing to gain from writing an empty value. + [ -z "$value" ] && exit 0 +fi + +# Single-quote the value, escaping any embedded single quotes. +escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" +new_line="export ${name}='${escaped}'" + +# No-op if the exact line is already present -- avoids rewriting the file (and +# its mtime) on every `. bk` reload for the many deterministic values. +grep -qxF "$new_line" "$file" && exit 0 + +# Write atomically: a temp file in the SAME directory (so the final mv is a +# same-filesystem rename, not a cross-device copy -- $HOME and /tmp are +# different filesystems in Cloud Shell), preserving the target's permissions. +# This protects the once-generated secrets in vars.local.sh from a truncated +# write if the process is interrupted mid-update. +dir="$(dirname "$file")" +tmp="$(mktemp "$dir/.bkvar.XXXXXX")" +chmod --reference="$file" "$tmp" 2>/dev/null || true +if grep -qE "$match" "$file"; then + # Replace the first matching line, drop any later duplicates. + NEW_LINE="$new_line" MATCH="$match" awk ' + $0 ~ ENVIRON["MATCH"] { if (!seen) { print ENVIRON["NEW_LINE"]; seen=1 } next } + { print } + ' "$file" > "$tmp" +else + cat "$file" > "$tmp" + printf '%s\n' "$new_line" >> "$tmp" +fi +mv "$tmp" "$file" diff --git a/content/agenticdata/TUTORIAL.md b/content/agenticdata/TUTORIAL.md index b4ffbfd..40f3855 100644 --- a/content/agenticdata/TUTORIAL.md +++ b/content/agenticdata/TUTORIAL.md @@ -31,27 +31,24 @@ echo "Let's go, Cymbal!" Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Set up your environment -❗ Please do not include any whitespaces when setting these variables. - -Please initialize bootkon. The next command will set environment variables in your current terminal. +Initialize bootkon. This sets your environment variables and auto-detects your +Google Cloud project and account from Cloud Shell: ```bash . bk ``` -Reload the tutorial window on the right-hand side of your screen. +Then reload the tutorial window on the right-hand side of your screen (run this +again any time you accidentally close the tutorial or the editor): ```bash bk-start ``` -In case you accidentally close the tutorial or the editor, you can execute `bk-start` to start it again. Please make sure that you execute `. bk` in every terminal -you open so that the environment variables are set. +New terminals load the environment automatically — you only re-run `. bk` after +changing your configuration. Now, your @@ -59,6 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. If neither is `None`, press the `START` button below to get started! diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index d45c7dd..4bbca47 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -95,29 +95,28 @@ for role in "${dataquality_roles[@]}"; do --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" >>/dev/null done -# Per-participant configuration, appended to ~/.bashrc so every new terminal -# picks it up automatically (same pattern .scripts/bk uses for BK_* vars). -if ! grep -q '# agenticdata-bootkon' ~/.bashrc; then - echo "Generating database passwords and adding stream exports to ~/.bashrc ..." - cat >> ~/.bashrc <= 0.3/1.0 -- diff --git a/content/agenticdata/src/adk/cymbal_analyst/agent.py b/content/agenticdata/src/adk/cymbal_analyst/agent.py index 27a3d95..2db9b33 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/agent.py +++ b/content/agenticdata/src/adk/cymbal_analyst/agent.py @@ -5,8 +5,8 @@ protocol by a2a_server.py. All configuration (GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_VERTEXAI, -BK_CYMBAL_MODEL, BK_DATA_AGENT_ID) comes from environment variables exported to -~/.bashrc by bk-bootstrap in Lab 1. +BK_CYMBAL_MODEL, BK_DATA_AGENT_ID) comes from environment variables written to +vars.local.sh by bk-bootstrap in Lab 1. """ import os diff --git a/content/agenticdata/src/adk/cymbal_concierge/agent.py b/content/agenticdata/src/adk/cymbal_concierge/agent.py index 48cd0ea..e23fe7b 100644 --- a/content/agenticdata/src/adk/cymbal_concierge/agent.py +++ b/content/agenticdata/src/adk/cymbal_concierge/agent.py @@ -7,7 +7,7 @@ Postgres through the IAP tunnel (localhost:5432). All configuration (model, database password, hosts) comes from environment -variables exported to ~/.bashrc by bk-bootstrap in Lab 1. +variables written to vars.local.sh by bk-bootstrap in Lab 1. """ import os diff --git a/content/agents/TUTORIAL.md b/content/agents/TUTORIAL.md index 3d040ca..1d50632 100644 --- a/content/agents/TUTORIAL.md +++ b/content/agents/TUTORIAL.md @@ -30,32 +30,32 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Set up your environment -❗ Please do not include any whitespaces when setting these variablers. - -Please reload bootkon and make sure there are no errors printed: +Initialize bootkon. This sets your environment variables and auto-detects your +Google Cloud project and account from Cloud Shell: ```bash . bk ``` - -And restart the tutorial using the next command. You can also use the next command to continue bootkon in case you accidentally close the tutorial or the editor: +Then reload the tutorial window on the right-hand side of your screen (run this +again any time you accidentally close the tutorial or the editor): ```bash bk-start ``` +New terminals load the environment automatically — you only re-run `. bk` after +changing your configuration. + Now, your * `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. If neither is `None`, press the `START` button below to get started! diff --git a/content/contributing/TUTORIAL.md b/content/contributing/TUTORIAL.md index 1c0fcf3..9404585 100644 --- a/content/contributing/TUTORIAL.md +++ b/content/contributing/TUTORIAL.md @@ -52,21 +52,18 @@ git status ## Set up your development environment -During the first lab, participants are asked to edit `vars.sh`. It is suggested to make a copy of this file and not touch the original in order not to accidently commit it to git. +`. bk` creates `vars.local.sh` from `vars.sh` on first run and auto-detects your +`PROJECT_ID`/`GCP_USERNAME` from Cloud Shell. `vars.local.sh` is git-ignored, so +you never risk committing your project — never edit `vars.sh` itself. -First, make a copy: +To override a detected value (or set `MY_NAME`), edit `vars.local.sh`, then reload: ```bash -cp vars.sh vars.local.sh +. bk ``` -And edit it. It also runs on Argolis (for Google employees). - -Next, source it: -```bash -. vars.local.sh -``` - -Note that the init script (`bk`) automatically loads `vars.local.sh` the next time and `vars.local.sh` takes presendence over `vars.sh`. +It also runs on Argolis (for Google employees). `vars.local.sh` takes precedence +over `vars.sh`, and every new terminal picks it up automatically via the block +`bk` adds to `~/.bashrc`. ## Reloading the tutorial diff --git a/content/data/TUTORIAL.md b/content/data/TUTORIAL.md index f320715..a76f99b 100644 --- a/content/data/TUTORIAL.md +++ b/content/data/TUTORIAL.md @@ -31,27 +31,24 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Set up your environment -❗ Please do not include any whitespaces when setting these variablers. - -Please initialize bootkon. The next command will set environment variables in your current terminal. +Initialize bootkon. This sets your environment variables and auto-detects your +Google Cloud project and account from Cloud Shell: ```bash . bk ``` -Reload the tutorial window on the right-hand side of your screen. +Then reload the tutorial window on the right-hand side of your screen (run this +again any time you accidentally close the tutorial or the editor): ```bash bk-start ``` -In case you accidently close the tutorial or the editor, you can execute `bk-start` to start it again. Please make sure that you execute `. bk` in every terminal -you open so that the environment variables are set. +New terminals load the environment automatically — you only re-run `. bk` after +changing your configuration. Now, your @@ -59,6 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. If neither is `None`, press the `START` button below to get started! diff --git a/content/devex/TUTORIAL.md b/content/devex/TUTORIAL.md index 5172d47..ea15059 100644 --- a/content/devex/TUTORIAL.md +++ b/content/devex/TUTORIAL.md @@ -30,32 +30,32 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Set up your environment -❗ Please do not include any whitespaces when setting these variablers. - -Please reload bootkon and make sure there are no errors printed: +Initialize bootkon. This sets your environment variables and auto-detects your +Google Cloud project and account from Cloud Shell: ```bash . bk ``` - -And restart the tutorial using the next command. You can also use the next command to continue bootkon in case you accidentally close the tutorial or the editor: +Then reload the tutorial window on the right-hand side of your screen (run this +again any time you accidentally close the tutorial or the editor): ```bash bk-start ``` +New terminals load the environment automatically — you only re-run `. bk` after +changing your configuration. + Now, your * `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. If neither is `None`, press the `START` button below to get started! diff --git a/vars.sh b/vars.sh index c19ef33..f7a46f6 100644 --- a/vars.sh +++ b/vars.sh @@ -12,7 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -export MY_NAME="" # example: Ada -export PROJECT_ID="" # example: bootkon-data-3472 -export GCP_USERNAME="" # example: devstar3110@gcplab.me -export REGION="us-central1" # do not change this value +# This file holds the defaults. `. bk` copies it to vars.local.sh (git-ignored) +# on first run and works from there -- edit vars.local.sh, not this file. + +# Your (first) name, shown in the tutorial greeting. Optional. Example: Ada +export MY_NAME="" + +# Google Cloud project and account. Leave empty to auto-detect from Cloud Shell; +# set a value to override what `. bk` detects. +# Examples: bootkon-data-3472 / devstar3110@gcplab.me +export PROJECT_ID="" +export GCP_USERNAME="" + +# Deployment region. Do not change this value. +export REGION="us-central1" From b6e1a028f5885ff01866789d1fc0a269531f401b Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Wed, 8 Jul 2026 22:12:19 +0200 Subject: [PATCH 03/28] docs: use `. bk` reload in agenticdata, document branch testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agenticdata lab 1 / bk-bootstrap / simulate.py now reload with `. bk` instead of `source ~/.bashrc`, consistent with the tutorial intro and the dual-mode bk reload - contributing: add "Test a branch end-to-end" — the branch must go in the wget URL (the stream READMEs hard-code main), and ~/bootkon is reused if it already exists; BK_BRANCH and image links follow the checkout --- content/agenticdata/bk-bootstrap | 2 +- content/agenticdata/labs/1_environment_setup.md | 4 ++-- content/agenticdata/src/datagen/simulate.py | 2 +- content/contributing/TUTORIAL.md | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index 4bbca47..37ebb65 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -164,5 +164,5 @@ fi echo echo "Bootstrap complete. The stream configuration is in vars.local.sh." echo "Load it into this terminal with:" -echo " source ~/.bashrc" +echo " . bk" echo "(New terminals pick it up automatically.)" diff --git a/content/agenticdata/labs/1_environment_setup.md b/content/agenticdata/labs/1_environment_setup.md index 6276bfb..fd37118 100644 --- a/content/agenticdata/labs/1_environment_setup.md +++ b/content/agenticdata/labs/1_environment_setup.md @@ -43,10 +43,10 @@ Execute the following script. It installs Python dependencies, grants IAM roles, content/agenticdata/bk-bootstrap ``` -The script wrote your generated database passwords and agent settings to `vars.local.sh`, so **every new terminal picks them up automatically**. Load them into this already-open one: +The script wrote your generated database passwords and agent settings to `vars.local.sh`, so **every new terminal picks them up automatically**. Reload this already-open one: ```bash -source ~/.bashrc +. bk ``` Then reload the tutorial once — this renders your freshly generated passwords directly into the commands of the next labs: diff --git a/content/agenticdata/src/datagen/simulate.py b/content/agenticdata/src/datagen/simulate.py index b240817..c46ad5e 100755 --- a/content/agenticdata/src/datagen/simulate.py +++ b/content/agenticdata/src/datagen/simulate.py @@ -39,7 +39,7 @@ def log(action, detail): def main(): password = os.environ.get("BK_DB_PASSWORD") if not password: - sys.exit("BK_DB_PASSWORD is not set. Run: source ~/.bashrc (set by bk-bootstrap)") + sys.exit("BK_DB_PASSWORD is not set. Run '. bk' to reload it (written to vars.local.sh by bk-bootstrap).") host = os.environ.get("BK_CYMBAL_DB_HOST", "localhost") port = int(os.environ.get("BK_CYMBAL_DB_PORT", "5432")) diff --git a/content/contributing/TUTORIAL.md b/content/contributing/TUTORIAL.md index 9404585..6c5f9c0 100644 --- a/content/contributing/TUTORIAL.md +++ b/content/contributing/TUTORIAL.md @@ -65,6 +65,23 @@ It also runs on Argolis (for Google employees). `vars.local.sh` takes precedence over `vars.sh`, and every new terminal picks it up automatically via the block `bk` adds to `~/.bashrc`. +## Test a branch end-to-end + +To try your branch the way a participant would — a clean clone and the full +`bk` bootstrap — run the one-liner with your branch in **both** `BK_BRANCH` and +the URL. The stream READMEs hard-code `main`, so you must swap it out: + +```bash +BK_BRANCH= BK_STREAM= BK_REPO=/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/.scripts/bk) +``` + +`bk` clones the branch into `~/bootkon`. If `~/bootkon` already exists (your dev +checkout), `bk` reuses it instead of cloning — so for a truly clean test, remove +it first (`rm -rf ~/bootkon`). In your own dev checkout you don't need the +one-liner at all: `git checkout ` and run `. bk`. `BK_BRANCH` — and +therefore the rendered image links (`.../blob//...`) — follow whatever +the checkout is on, so push image changes before expecting them to render. + ## Reloading the tutorial You can reload a lab on-the-fly by typing `bk-tutorial` followed by the lab markdown file into the terminal and pressing return. Let's reload From bfc815b53a3ab3970c97fc1ecb1254b418e0f7fa Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Wed, 8 Jul 2026 23:45:27 +0200 Subject: [PATCH 04/28] bk: describe every var in vars.local.sh; set MY_NAME at launch; two-line bootstrap - bk-set-var gains --comment, written as a `# text` line above the export on first append, so a fresh vars.local.sh is self-documenting; not duplicated on reload, and config comments from vars.sh are preserved on in-place replace - bk / bk-bootstrap pass a one-line description for every managed and stream var - bk fills MY_NAME from the launch command (captured before sourcing; fill-if- empty so a later manual edit wins), so no vars.local.sh edit is needed - READMEs: two-line bootstrap (MY_NAME on line 1, fixed wget on line 2); this also fixes the agents one-liner (was fhirschmann fork + empty ${BK_BRANCH}) - TUTORIAL intros: note MY_NAME comes from the launch command --- .scripts/bk | 24 +++++++++++++++--------- .scripts/bk-set-var | 19 ++++++++++++++----- content/agenticdata/README.md | 1 + content/agenticdata/TUTORIAL.md | 2 +- content/agenticdata/bk-bootstrap | 18 +++++++++--------- content/agents/README.md | 3 ++- content/agents/TUTORIAL.md | 2 +- content/data/README.md | 1 + content/data/TUTORIAL.md | 2 +- content/devex/README.md | 1 + content/devex/TUTORIAL.md | 2 +- content/example/README.md | 1 + 12 files changed, 48 insertions(+), 28 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index f655af0..f68da36 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -79,6 +79,7 @@ fi BK_WAS_INITIALIZED="${BK_INITIALIZED:-}" BK_REQ_REPO="$BK_REPO" BK_REQ_STREAM="$BK_STREAM" +BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it export BK_REPO="$BK_REPO" export BK_STREAM="$BK_STREAM" @@ -148,6 +149,11 @@ fi if [ -z "$GCP_USERNAME" ] && [ -n "$detected_account" ]; then "$BK_SET_VAR" "$LOCAL" GCP_USERNAME "$detected_account" fi +# Fill MY_NAME from the launch command if it was provided and not already set +# (respects a later manual edit; the comment from vars.sh stays on replace). +if [ -z "$MY_NAME" ] && [ -n "$BK_REQ_MY_NAME" ]; then + "$BK_SET_VAR" "$LOCAL" MY_NAME "$BK_REQ_MY_NAME" +fi # Reload so this shell sees any freshly detected values. source "$LOCAL" @@ -186,12 +192,12 @@ echo -e "Config: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP # Persist the managed variables and install the ~/.bashrc block BEFORE the # project is validated below: even if validation fails (expired auth, wrong # project) a freshly opened terminal must still have PATH and the seed vars. -"$BK_SET_VAR" "$LOCAL" BK_STREAM "$BK_STREAM" -"$BK_SET_VAR" "$LOCAL" BK_REPO "$BK_REPO" -"$BK_SET_VAR" "$LOCAL" BK_BRANCH "$BK_BRANCH" -"$BK_SET_VAR" "$LOCAL" BK_GITHUB_USERNAME "$BK_GITHUB_USERNAME" -"$BK_SET_VAR" "$LOCAL" BK_GITHUB_REPOSITORY "$BK_GITHUB_REPOSITORY" -"$BK_SET_VAR" "$LOCAL" BK_REPO_URL "$BK_REPO_URL" +"$BK_SET_VAR" --comment "Selected stream; picks content//" "$LOCAL" BK_STREAM "$BK_STREAM" +"$BK_SET_VAR" --comment "GitHub repo the labs are served from (user/repo)" "$LOCAL" BK_REPO "$BK_REPO" +"$BK_SET_VAR" --comment "Checkout branch; used for rendered image links" "$LOCAL" BK_BRANCH "$BK_BRANCH" +"$BK_SET_VAR" --comment "GitHub org/user (first half of BK_REPO)" "$LOCAL" BK_GITHUB_USERNAME "$BK_GITHUB_USERNAME" +"$BK_SET_VAR" --comment "GitHub repo name (second half of BK_REPO)" "$LOCAL" BK_GITHUB_REPOSITORY "$BK_GITHUB_REPOSITORY" +"$BK_SET_VAR" --comment "Clone URL derived from BK_REPO" "$LOCAL" BK_REPO_URL "$BK_REPO_URL" # Install/refresh a single guarded block in ~/.bashrc so every new terminal # loads the environment by sourcing vars.local.sh. bk-remove-block strips any @@ -233,14 +239,14 @@ else # Persist project-derived state only once it is valid: the stream bootstrap # scripts guard on BK_INITIALIZED and rely on PROJECT_NUMBER. if [ -n "$PROJECT_NUMBER" ]; then - "$BK_SET_VAR" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" - "$BK_SET_VAR" "$LOCAL" BK_INITIALIZED 1 + "$BK_SET_VAR" --comment "Numeric GCP project number (derived from PROJECT_ID)" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" + "$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 fi fi unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS unset detected_project detected_account _origin _actual _branch -unset BK_REQ_REPO BK_REQ_STREAM +unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME if [ -z "$BK_WAS_INITIALIZED" ]; then echo diff --git a/.scripts/bk-set-var b/.scripts/bk-set-var index 1ac3076..5b9871a 100755 --- a/.scripts/bk-set-var +++ b/.scripts/bk-set-var @@ -16,12 +16,16 @@ # bk-set-var — idempotently upsert an `export NAME='VALUE'` line into a file. # # Usage: -# bk-set-var [--if-empty] FILE NAME VALUE +# bk-set-var [--if-empty] [--comment "text"] FILE NAME VALUE # # --if-empty Only write if NAME is currently unset or empty in FILE. # Existing non-empty values are left untouched. Used for # autodetected config (respect manual edits) and for # once-generated secrets (never regenerate). +# --comment One-line description, written as a `# text` comment directly +# above the export when the line is first appended (so a fresh +# vars.local.sh is self-documenting). Not re-added on later +# in-place replacements, and never duplicated. # # The file is created if missing. An existing `export NAME=...` line is # replaced in place (order preserved, duplicates collapsed); otherwise the @@ -31,10 +35,14 @@ set -eu if_empty=0 -if [ "${1:-}" = "--if-empty" ]; then - if_empty=1 - shift -fi +comment="" +while [ $# -gt 0 ]; do + case "$1" in + --if-empty) if_empty=1; shift ;; + --comment) comment="$2"; shift 2 ;; # one-line description, written above the export + *) break ;; + esac +done file="${1:?bk-set-var: FILE required}" name="${2:?bk-set-var: NAME required}" @@ -100,6 +108,7 @@ if grep -qE "$match" "$file"; then ' "$file" > "$tmp" else cat "$file" > "$tmp" + [ -n "$comment" ] && printf '# %s\n' "$comment" >> "$tmp" printf '%s\n' "$new_line" >> "$tmp" fi mv "$tmp" "$file" diff --git a/content/agenticdata/README.md b/content/agenticdata/README.md index 6b4c442..91abde7 100644 --- a/content/agenticdata/README.md +++ b/content/agenticdata/README.md @@ -171,6 +171,7 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash +MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) ``` diff --git a/content/agenticdata/TUTORIAL.md b/content/agenticdata/TUTORIAL.md index 40f3855..f6f26a9 100644 --- a/content/agenticdata/TUTORIAL.md +++ b/content/agenticdata/TUTORIAL.md @@ -56,7 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) If neither is `None`, press the `START` button below to get started! diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index 37ebb65..7c45bbf 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -108,15 +108,15 @@ LOCAL="$BK_DIR/vars.local.sh" # and, since there is no `set -e` here, silently write nothing. SET_VAR="$BK_DIR/.scripts/bk-set-var" echo "Writing stream configuration to vars.local.sh (generating passwords if needed) ..." -"$SET_VAR" --if-empty "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" -"$SET_VAR" --if-empty "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" -"$SET_VAR" "$LOCAL" GOOGLE_CLOUD_PROJECT "$PROJECT_ID" -"$SET_VAR" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE -"$SET_VAR" "$LOCAL" GOOGLE_CLOUD_LOCATION global -"$SET_VAR" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash -"$SET_VAR" "$LOCAL" BK_DATA_AGENT_ID cymbal-data-agent -"$SET_VAR" "$LOCAL" BK_CYMBAL_DB_HOST localhost -"$SET_VAR" "$LOCAL" BK_CYMBAL_DB_PORT 5432 +"$SET_VAR" --if-empty --comment "Cloud SQL (Postgres) root password -- generated once" "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" +"$SET_VAR" --if-empty --comment "Datastream connection password -- generated once" "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" +"$SET_VAR" --comment "Project for the ADK / Antigravity (agy) runtime" "$LOCAL" GOOGLE_CLOUD_PROJECT "$PROJECT_ID" +"$SET_VAR" --comment "Route google-genai through Vertex AI" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE +"$SET_VAR" --comment "Location for the GenAI/Vertex client" "$LOCAL" GOOGLE_CLOUD_LOCATION global +"$SET_VAR" --comment "Gemini model id used by the Cymbal agents" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash +"$SET_VAR" --comment "Conversational Analytics data agent id" "$LOCAL" BK_DATA_AGENT_ID cymbal-data-agent +"$SET_VAR" --comment "Cymbal DB host (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_HOST localhost +"$SET_VAR" --comment "Cymbal DB port (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_PORT 5432 # Pre-configure Antigravity CLI (agy). It is already installed in Cloud Shell # and authenticates with the ambient Cloud Shell credentials (ADC); it reads diff --git a/content/agents/README.md b/content/agents/README.md index ad7cb94..92b9138 100644 --- a/content/agents/README.md +++ b/content/agents/README.md @@ -79,7 +79,8 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -BK_STREAM=agents BK_REPO=fhirschmann/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/.scripts/bk) +MY_NAME="" # your (first) name, shown in the greeting (optional) +BK_STREAM=agents BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/agents/TUTORIAL.md b/content/agents/TUTORIAL.md index 1d50632..4570f51 100644 --- a/content/agents/TUTORIAL.md +++ b/content/agents/TUTORIAL.md @@ -55,7 +55,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) If neither is `None`, press the `START` button below to get started! diff --git a/content/data/README.md b/content/data/README.md index a059038..ad5906d 100644 --- a/content/data/README.md +++ b/content/data/README.md @@ -133,6 +133,7 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash +MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) ``` diff --git a/content/data/TUTORIAL.md b/content/data/TUTORIAL.md index a76f99b..b89a982 100644 --- a/content/data/TUTORIAL.md +++ b/content/data/TUTORIAL.md @@ -56,7 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) If neither is `None`, press the `START` button below to get started! diff --git a/content/devex/README.md b/content/devex/README.md index eda3236..bf9701c 100644 --- a/content/devex/README.md +++ b/content/devex/README.md @@ -79,6 +79,7 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash +MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) ``` diff --git a/content/devex/TUTORIAL.md b/content/devex/TUTORIAL.md index ea15059..93f03f7 100644 --- a/content/devex/TUTORIAL.md +++ b/content/devex/TUTORIAL.md @@ -55,7 +55,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. You can also set your (first) name in `MY_NAME`. +If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) If neither is `None`, press the `START` button below to get started! diff --git a/content/example/README.md b/content/example/README.md index 805f5d4..90f8caa 100644 --- a/content/example/README.md +++ b/content/example/README.md @@ -1,3 +1,4 @@ ```bash +MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) ``` \ No newline at end of file From 902d8ed6f76ca4eb80ada3d4cead52dac6c06963 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 00:05:50 +0200 Subject: [PATCH 05/28] agenticdata: use PROJECT_ID as the single project var; stop pinning GOOGLE_CLOUD_PROJECT Cloud Shell already exports GOOGLE_CLOUD_PROJECT (= the active project = PROJECT_ID, which bk autodetects from it), so persisting a duplicate in vars.local.sh was redundant. bootkon code now uses PROJECT_ID; the Vertex SDK and agy keep reading GOOGLE_CLOUD_PROJECT straight from Cloud Shell. - bk-bootstrap: drop the GOOGLE_CLOUD_PROJECT write (LOCATION + USE_VERTEXAI stay) - ca_tool.py: read PROJECT_ID instead of GOOGLE_CLOUD_PROJECT - docs (Lab 6, ADK AGENTS.md, agent docstring): PROJECT_ID is the canonical var --- content/agenticdata/bk-bootstrap | 9 ++++++--- content/agenticdata/labs/6_a2a.md | 2 +- content/agenticdata/src/adk/AGENTS.md | 6 ++++-- content/agenticdata/src/adk/cymbal_analyst/agent.py | 7 ++++--- content/agenticdata/src/adk/cymbal_analyst/ca_tool.py | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index 7c45bbf..9e65cc1 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -110,7 +110,9 @@ SET_VAR="$BK_DIR/.scripts/bk-set-var" echo "Writing stream configuration to vars.local.sh (generating passwords if needed) ..." "$SET_VAR" --if-empty --comment "Cloud SQL (Postgres) root password -- generated once" "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" "$SET_VAR" --if-empty --comment "Datastream connection password -- generated once" "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" -"$SET_VAR" --comment "Project for the ADK / Antigravity (agy) runtime" "$LOCAL" GOOGLE_CLOUD_PROJECT "$PROJECT_ID" +# GOOGLE_CLOUD_PROJECT is intentionally NOT set here: Cloud Shell already exports +# it (= the active project = PROJECT_ID), so the google-genai/Vertex SDK and agy +# read it from there. bootkon's own code uses PROJECT_ID. "$SET_VAR" --comment "Route google-genai through Vertex AI" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE "$SET_VAR" --comment "Location for the GenAI/Vertex client" "$LOCAL" GOOGLE_CLOUD_LOCATION global "$SET_VAR" --comment "Gemini model id used by the Cymbal agents" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash @@ -120,8 +122,9 @@ echo "Writing stream configuration to vars.local.sh (generating passwords if nee # Pre-configure Antigravity CLI (agy). It is already installed in Cloud Shell # and authenticates with the ambient Cloud Shell credentials (ADC); it reads -# the project and location from GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION -# (exported above). Seeding settings.json suppresses the first-run wizard's +# the project from GOOGLE_CLOUD_PROJECT (Cloud Shell sets it to the active +# project) and the location from GOOGLE_CLOUD_LOCATION (written above). Seeding +# settings.json suppresses the first-run wizard's # auth, theme and workspace-trust prompts so the labs start clean. AGY_DIR=~/.gemini/antigravity-cli if [ ! -f "$AGY_DIR/settings.json" ]; then diff --git a/content/agenticdata/labs/6_a2a.md b/content/agenticdata/labs/6_a2a.md index 6fda64c..5da7866 100644 --- a/content/agenticdata/labs/6_a2a.md +++ b/content/agenticdata/labs/6_a2a.md @@ -25,7 +25,7 @@ First, verify the foundation: open [BigQuery](https://console.cloud.google.com/b ### Set up the project -The agent project ships with the repository — you work directly in `content/agenticdata/src/adk`. There is no configuration file to create: everything the agents need (`GOOGLE_CLOUD_PROJECT`, the pinned Gemini model, the data agent ID, the database password) was exported to your `~/.bashrc` by `bk-bootstrap` in Lab 1, and the agents read it from the environment. They authenticate to Gemini via Vertex AI with your logged-in credentials — no API keys anywhere. +The agent project ships with the repository — you work directly in `content/agenticdata/src/adk`. There is no configuration file to create: everything the agents need (your `PROJECT_ID`, the pinned Gemini model, the data agent ID, the database password) lives in `vars.local.sh` from Lab 1, and the agents read it from the environment. They authenticate to Gemini via Vertex AI with your logged-in credentials — Vertex picks up your project from Cloud Shell — no API keys anywhere. All Python dependencies were installed by the bootstrap too, so just confirm the ADK CLI is there and check your configuration: diff --git a/content/agenticdata/src/adk/AGENTS.md b/content/agenticdata/src/adk/AGENTS.md index e3834fc..ae3e0b4 100644 --- a/content/agenticdata/src/adk/AGENTS.md +++ b/content/agenticdata/src/adk/AGENTS.md @@ -46,9 +46,11 @@ tunnel on localhost:5432. `from google.adk.a2a.utils.agent_to_a2a import to_a2a`, `from google.cloud import geminidataanalytics`. - All configuration comes from environment variables: - `GOOGLE_CLOUD_PROJECT`, `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_LOCATION`, + `PROJECT_ID` (bootkon's canonical project var — use this, not + `GOOGLE_CLOUD_PROJECT`), `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_LOCATION`, `BK_CYMBAL_MODEL` (default `gemini-2.5-flash`), `BK_DATA_AGENT_ID`, - `BK_CYMBAL_DB_HOST`/`BK_CYMBAL_DB_PORT`, `BK_DB_PASSWORD`. + `BK_CYMBAL_DB_HOST`/`BK_CYMBAL_DB_PORT`, `BK_DB_PASSWORD`. (The Vertex SDK and + agy read `GOOGLE_CLOUD_PROJECT`, which Cloud Shell provides = `PROJECT_ID`.) Never create config files and never hardcode credentials. - Read the model as `os.environ.get("BK_CYMBAL_MODEL", "gemini-2.5-flash")`. - Keep tools small and readable; docstrings are the tool documentation the diff --git a/content/agenticdata/src/adk/cymbal_analyst/agent.py b/content/agenticdata/src/adk/cymbal_analyst/agent.py index 2db9b33..3c96f15 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/agent.py +++ b/content/agenticdata/src/adk/cymbal_analyst/agent.py @@ -4,9 +4,10 @@ agent (Conversational Analytics) and is exposed to other agents over the A2A protocol by a2a_server.py. -All configuration (GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_VERTEXAI, -BK_CYMBAL_MODEL, BK_DATA_AGENT_ID) comes from environment variables written to -vars.local.sh by bk-bootstrap in Lab 1. +All configuration (PROJECT_ID, GOOGLE_GENAI_USE_VERTEXAI, BK_CYMBAL_MODEL, +BK_DATA_AGENT_ID) comes from environment variables in vars.local.sh (written by +bk / bk-bootstrap in Lab 1). Vertex AI reads GOOGLE_CLOUD_PROJECT, which Cloud +Shell provides (= PROJECT_ID). """ import os diff --git a/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py b/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py index 6f32b18..21e1cc9 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py +++ b/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py @@ -24,7 +24,7 @@ def ask_cymbal_data_agent(question: str) -> str: The data agent's final textual answer (it runs SQL on BigQuery behind the scenes). """ - project = os.environ["GOOGLE_CLOUD_PROJECT"] + project = os.environ["PROJECT_ID"] agent_id = os.environ.get("BK_DATA_AGENT_ID", "cymbal-data-agent") client = geminidataanalytics.DataChatServiceClient() From d17b072c49c66ba67a3dd018e8ce1b35f1115b75 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 21:50:46 +0200 Subject: [PATCH 06/28] bk: derive PROJECT_ID live from Cloud Shell; cache PROJECT_NUMBER PROJECT_ID is no longer autodetected-and-persisted. bk derives it live from ${DEVSHELL_PROJECT_ID:-$GOOGLE_CLOUD_PROJECT} (Cloud Shell refreshes it every prompt and keeps gcloud pointed at it), so it cannot go stale and there is nothing to edit -- to use another project, switch it in the Cloud Shell picker. The ~/.bashrc block exports it live in every new terminal. PROJECT_NUMBER is the only project value not in the environment, so it is cached in vars.local.sh keyed by project (BK_PROJECT_NUMBER_OF): computed once, reused on same-project reloads with zero network calls, auto-refreshed on a project switch. An empty project number is now a hard error, and the redundant `gcloud config set project` is dropped. - vars.sh: drop PROJECT_ID (now live) - TUTORIAL intros (data, agenticdata, devex): a wrong PROJECT_ID is fixed via the Cloud Shell project picker, not by editing vars.local.sh --- .scripts/bk | 55 ++++++++++++++++++++------------- content/agenticdata/TUTORIAL.md | 2 +- content/data/TUTORIAL.md | 2 +- content/devex/TUTORIAL.md | 2 +- vars.sh | 8 ++--- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index f68da36..b2775f2 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -136,16 +136,9 @@ source "$BK_DIR/vars.sh" echo -e "Sourcing $(readlink -f "$LOCAL")" source "$LOCAL" -# Autodetect PROJECT_ID / GCP_USERNAME from the Cloud Shell environment, but -# only fill values that are still empty -- the just-sourced value is the source -# of truth, so a manual edit in vars.local.sh always wins. -detected_project="${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}" +# GCP_USERNAME: autodetect once, filling only if still empty (a manual edit wins). detected_account="$(gcloud config get-value account 2>/dev/null || true)" -[ "$detected_project" = "(unset)" ] && detected_project="" [ "$detected_account" = "(unset)" ] && detected_account="" -if [ -z "$PROJECT_ID" ] && [ -n "$detected_project" ]; then - "$BK_SET_VAR" "$LOCAL" PROJECT_ID "$detected_project" -fi if [ -z "$GCP_USERNAME" ] && [ -n "$detected_account" ]; then "$BK_SET_VAR" "$LOCAL" GCP_USERNAME "$detected_account" fi @@ -157,6 +150,15 @@ fi # Reload so this shell sees any freshly detected values. source "$LOCAL" +# PROJECT_ID is LIVE from Cloud Shell -- it always mirrors the project selected +# in the Cloud Shell project picker (DEVSHELL_PROJECT_ID, which Cloud Shell +# refreshes on every prompt). Derived AFTER the source so a stale persisted value +# can never override it, and never written to vars.local.sh: nothing to edit, +# nothing to go stale. To use a different project, switch it in the picker. +PROJECT_ID="${DEVSHELL_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}}" +[ "$PROJECT_ID" = "(unset)" ] && PROJECT_ID="" +export PROJECT_ID + # Derive the repo identity AFTER the last source (so the persisted values can't # clobber it) and from the checkout's own origin remote, so BK_REPO and the # rendered image links always match the files actually on disk -- even if @@ -212,6 +214,7 @@ cat >> "$BASHRC" </dev/null 2>&1; then echo -e "${RED}ERROR: Unable to access project $PROJECT_ID.${NC}" echo -e "${RED}Two common causes:${NC}" echo -e "${RED} 1. Your session needs re-authentication (typical after a break) —${NC}" echo -e "${RED} re-authorize Cloud Shell when prompted, or run 'gcloud auth login', then '. bk' again.${NC}" - echo -e "${RED} 2. PROJECT_ID in vars.local.sh is wrong.${NC}" + echo -e "${RED} 2. The wrong project is selected — switch it in the Cloud Shell project selector.${NC}" return 1 fi - gcloud config set project "$PROJECT_ID" --quiet 2>/dev/null - - export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" - echo -e "Project for gcloud CLI has been set: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." - - # Persist project-derived state only once it is valid: the stream bootstrap - # scripts guard on BK_INITIALIZED and rely on PROJECT_NUMBER. - if [ -n "$PROJECT_NUMBER" ]; then - "$BK_SET_VAR" --comment "Numeric GCP project number (derived from PROJECT_ID)" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" - "$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 + # PROJECT_NUMBER is the only project value NOT available from the environment, + # so cache it in vars.local.sh. Recompute only when it is missing or when the + # live project changed; every new terminal then reads it with no network call. + if [ -z "$PROJECT_NUMBER" ] || [ "$PROJECT_ID" != "${BK_PROJECT_NUMBER_OF:-}" ]; then + PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" + if [ -z "$PROJECT_NUMBER" ]; then + err "Could not read the project number for $PROJECT_ID — please run '. bk' again." + return 1 + fi + "$BK_SET_VAR" --comment "Numeric project number, cached for the current project" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" + "$BK_SET_VAR" --comment "Internal: the project the cached PROJECT_NUMBER belongs to" "$LOCAL" BK_PROJECT_NUMBER_OF "$PROJECT_ID" fi + export PROJECT_NUMBER + echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." + + # Mark validated (persisted): the stream bootstraps guard on BK_INITIALIZED. + "$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 fi unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS -unset detected_project detected_account _origin _actual _branch +unset detected_account _origin _actual _branch unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME if [ -z "$BK_WAS_INITIALIZED" ]; then diff --git a/content/agenticdata/TUTORIAL.md b/content/agenticdata/TUTORIAL.md index f6f26a9..448181e 100644 --- a/content/agenticdata/TUTORIAL.md +++ b/content/agenticdata/TUTORIAL.md @@ -56,7 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) +If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. If neither is `None`, press the `START` button below to get started! diff --git a/content/data/TUTORIAL.md b/content/data/TUTORIAL.md index b89a982..390b47e 100644 --- a/content/data/TUTORIAL.md +++ b/content/data/TUTORIAL.md @@ -56,7 +56,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) +If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. If neither is `None`, press the `START` button below to get started! diff --git a/content/devex/TUTORIAL.md b/content/devex/TUTORIAL.md index 93f03f7..f1aca95 100644 --- a/content/devex/TUTORIAL.md +++ b/content/devex/TUTORIAL.md @@ -55,7 +55,7 @@ Now, your * `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) +If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. If neither is `None`, press the `START` button below to get started! diff --git a/vars.sh b/vars.sh index f7a46f6..6ffddbb 100644 --- a/vars.sh +++ b/vars.sh @@ -18,10 +18,10 @@ # Your (first) name, shown in the tutorial greeting. Optional. Example: Ada export MY_NAME="" -# Google Cloud project and account. Leave empty to auto-detect from Cloud Shell; -# set a value to override what `. bk` detects. -# Examples: bootkon-data-3472 / devstar3110@gcplab.me -export PROJECT_ID="" +# Your Google Cloud account. Leave empty to auto-detect from Cloud Shell. +# (PROJECT_ID is NOT set here: bk derives it live from the Cloud Shell project +# picker. To use a different project, switch it in the picker.) +# Example: devstar3110@gcplab.me export GCP_USERNAME="" # Deployment region. Do not change this value. From 4c3f0a5681c7c2c479442178983ec1f0c5786827 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:03:24 +0200 Subject: [PATCH 07/28] bk: fail fast when no Cloud Shell project is selected Every lab needs a project and PROJECT_ID is derived live from the Cloud Shell picker, so bk now checks it up front: if no project is selected (DEVSHELL_PROJECT_ID/GOOGLE_CLOUD_PROJECT empty) it errors and returns BEFORE cloning or opening the editor, instead of setting up a project-less dead end and launching the tutorial anyway. Because the tutorial-without-a-project path is gone, BK_INITIALIZED is now exported only AFTER the project validates -- fixing the regression where the stream-bootstrap guard was defeated on a failed/aborted setup (bk-bootstrap would run with an empty PROJECT_NUMBER and silently skip the Datastream service-agent grant). --- .scripts/bk | 85 +++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index b2775f2..738a77b 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -88,6 +88,20 @@ export BK_REPO_URL="https://github.com/${BK_REPO}.git" # BK_DIR (e.g. contributors juggling several checkouts) is honored. export BK_DIR="${BK_DIR:-$HOME/bootkon}" +# A Google Cloud project must be selected in the Cloud Shell project picker before +# we do anything -- every lab needs it and PROJECT_ID is derived live from it. +# Fail fast HERE, before cloning or opening the editor, instead of setting up a +# project-less dead end. (DEVSHELL_PROJECT_ID / GOOGLE_CLOUD_PROJECT are empty when +# no project is selected; Cloud Shell refreshes them on every prompt.) +export PROJECT_ID="${DEVSHELL_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}}" +[ "$PROJECT_ID" = "(unset)" ] && PROJECT_ID="" +if [ -z "$PROJECT_ID" ]; then + err 'No Google Cloud project is selected.' + err 'Pick your event project in the project selector at the top of the Cloud' + err 'Shell window, then paste the setup command again.' + return 1 +fi + if ! command -v git &> /dev/null; then sudo apt update sudo apt install -y git @@ -150,11 +164,10 @@ fi # Reload so this shell sees any freshly detected values. source "$LOCAL" -# PROJECT_ID is LIVE from Cloud Shell -- it always mirrors the project selected -# in the Cloud Shell project picker (DEVSHELL_PROJECT_ID, which Cloud Shell -# refreshes on every prompt). Derived AFTER the source so a stale persisted value -# can never override it, and never written to vars.local.sh: nothing to edit, -# nothing to go stale. To use a different project, switch it in the picker. +# Re-assert the live PROJECT_ID after the source: an older vars.local.sh may still +# carry a persisted PROJECT_ID line that would otherwise clobber the value we +# derived (and validated) up front. It stays live from the picker -- never written +# to vars.local.sh, nothing to edit, nothing to go stale. PROJECT_ID="${DEVSHELL_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}}" [ "$PROJECT_ID" = "(unset)" ] && PROJECT_ID="" export PROJECT_ID @@ -218,44 +231,38 @@ export PROJECT_ID="\${DEVSHELL_PROJECT_ID:-\$GOOGLE_CLOUD_PROJECT}" # live from # <<< bootkon <<< EOF -# Mark the environment initialized in this shell so bk-start opens the tutorial -# even before a project is set (the tutorial is what guides the user to set it). -export BK_INITIALIZED=1 +# The project is guaranteed selected (checked before the clone). Validate it now +# -- this also triggers the "Authorize Cloud Shell" popup at setup rather than +# mid-lab. gcloud already points at the picker's project, so no `config set`. +if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then + echo -e "${RED}ERROR: Unable to access project $PROJECT_ID.${NC}" + echo -e "${RED}Two common causes:${NC}" + echo -e "${RED} 1. Your session needs re-authentication (typical after a break) —${NC}" + echo -e "${RED} re-authorize Cloud Shell when prompted, or run 'gcloud auth login', then '. bk' again.${NC}" + echo -e "${RED} 2. The wrong project is selected — switch it in the Cloud Shell project selector.${NC}" + return 1 +fi -if [ -z "$PROJECT_ID" ]; then - echo -e "${YELLOW}No Google Cloud project is selected. Pick your event project in the${NC}" - echo -e "${YELLOW}Cloud Shell project selector (top of the window), then run '. bk' again.${NC}" -else - # Validate the project (this also triggers the "Authorize Cloud Shell" popup - # now, at setup, rather than mid-lab). gcloud is already pointed at the - # picker's project, so no `gcloud config set project` is needed. - if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then - echo -e "${RED}ERROR: Unable to access project $PROJECT_ID.${NC}" - echo -e "${RED}Two common causes:${NC}" - echo -e "${RED} 1. Your session needs re-authentication (typical after a break) —${NC}" - echo -e "${RED} re-authorize Cloud Shell when prompted, or run 'gcloud auth login', then '. bk' again.${NC}" - echo -e "${RED} 2. The wrong project is selected — switch it in the Cloud Shell project selector.${NC}" +# PROJECT_NUMBER is the only project value NOT available from the environment, so +# cache it in vars.local.sh. Recompute only when missing or when the live project +# changed; every new terminal then reads it with no network call. +if [ -z "$PROJECT_NUMBER" ] || [ "$PROJECT_ID" != "${BK_PROJECT_NUMBER_OF:-}" ]; then + PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" + if [ -z "$PROJECT_NUMBER" ]; then + err "Could not read the project number for $PROJECT_ID — please run '. bk' again." return 1 fi - - # PROJECT_NUMBER is the only project value NOT available from the environment, - # so cache it in vars.local.sh. Recompute only when it is missing or when the - # live project changed; every new terminal then reads it with no network call. - if [ -z "$PROJECT_NUMBER" ] || [ "$PROJECT_ID" != "${BK_PROJECT_NUMBER_OF:-}" ]; then - PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" - if [ -z "$PROJECT_NUMBER" ]; then - err "Could not read the project number for $PROJECT_ID — please run '. bk' again." - return 1 - fi - "$BK_SET_VAR" --comment "Numeric project number, cached for the current project" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" - "$BK_SET_VAR" --comment "Internal: the project the cached PROJECT_NUMBER belongs to" "$LOCAL" BK_PROJECT_NUMBER_OF "$PROJECT_ID" - fi - export PROJECT_NUMBER - echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." - - # Mark validated (persisted): the stream bootstraps guard on BK_INITIALIZED. - "$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 + "$BK_SET_VAR" --comment "Numeric project number, cached for the current project" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" + "$BK_SET_VAR" --comment "Internal: the project the cached PROJECT_NUMBER belongs to" "$LOCAL" BK_PROJECT_NUMBER_OF "$PROJECT_ID" fi +export PROJECT_NUMBER +echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." + +# Only NOW mark the environment initialized -- AFTER the project validated, so the +# stream bootstraps (which guard on BK_INITIALIZED and rely on PROJECT_NUMBER) +# never run on a failed setup. bk-start (wget path) runs after this line. +export BK_INITIALIZED=1 +"$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS unset detected_account _origin _actual _branch From 7fef4b0a150dbbd45821452a7bdc8483012a8deb Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:17:25 +0200 Subject: [PATCH 08/28] bk: optional per-stream init hook (bk-init); generate secrets before first render `. bk` now runs content/$BK_STREAM/bk-init (if present and executable) on every initialization and reload -- after project validation and BEFORE the first tutorial render -- then re-sources vars.local.sh. Hook contract: idempotent (write via bk-set-var --if-empty), local-only and instant; heavy, visible GCP work stays in the participant-run bk-bootstrap. Streams without a hook are unaffected; a failing hook aborts with an actionable message. agenticdata uses the hook to generate BK_DB_PASSWORD/BK_DS_PASSWORD and write the deterministic ADK/agy runtime config. This closes the empty-password render window: {{ BK_DB_PASSWORD }} in Lab 2 renders correctly from the very first render, so Lab 1 no longer needs the "run '. bk', then 'bk-start' to load the passwords" reload step -- bk-bootstrap keeps only APIs, IAM, service accounts, pip and the agy config. --- .scripts/bk | 17 ++++++++++ content/agenticdata/bk-bootstrap | 33 +++---------------- content/agenticdata/bk-init | 29 ++++++++++++++++ .../agenticdata/labs/1_environment_setup.md | 14 ++------ content/agenticdata/src/datagen/simulate.py | 2 +- 5 files changed, 54 insertions(+), 41 deletions(-) create mode 100755 content/agenticdata/bk-init diff --git a/.scripts/bk b/.scripts/bk index 738a77b..36c8c10 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -264,6 +264,23 @@ echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW export BK_INITIALIZED=1 "$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 +# Optional per-stream init hook: content/$BK_STREAM/bk-init. Runs on every `. bk` +# once the project is validated -- and, crucially, BEFORE the first tutorial +# render, so stream secrets already exist when the labs are rendered. Contract: +# idempotent (write via `bk-set-var --if-empty`), local-only and instant (no +# gcloud calls); heavy, visible GCP work belongs in the participant-run +# bk-bootstrap instead. Streams without a hook simply skip this. +if [ -x "$BK_DIR/content/$BK_STREAM/bk-init" ]; then + if ! "$BK_DIR/content/$BK_STREAM/bk-init"; then + err "The $BK_STREAM stream's init script (content/$BK_STREAM/bk-init) failed." + err "Please run '. bk' again; if it keeps failing, ask the event staff." + return 1 + fi + # Load what the hook just wrote (e.g. generated passwords) into this shell, + # so the upcoming render and any lab command here already see them. + source "$LOCAL" +fi + unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS unset detected_account _origin _actual _branch unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index 9e65cc1..ea39d1d 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -95,30 +95,10 @@ for role in "${dataquality_roles[@]}"; do --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" >>/dev/null done -# Per-participant stream config, written to vars.local.sh so every new terminal -# picks it up automatically (bk's ~/.bashrc block sources that file). Only -# secrets and Python/ADK runtime config live here; static values (project, -# region, the reserved endpoint IP, instance/db names) are rendered into the -# lab commands via Jinja2 or as literals, so they are NOT duplicated. -# Passwords are generated once (--if-empty); the rest are deterministic and -# always refreshed. bk-set-var is on PATH via the same ~/.bashrc block. -LOCAL="$BK_DIR/vars.local.sh" -# Resolve by absolute path (not via PATH): if this script runs in a shell where -# the ~/.bashrc block was not sourced, a bare `bk-set-var` would fail with 127 -# and, since there is no `set -e` here, silently write nothing. -SET_VAR="$BK_DIR/.scripts/bk-set-var" -echo "Writing stream configuration to vars.local.sh (generating passwords if needed) ..." -"$SET_VAR" --if-empty --comment "Cloud SQL (Postgres) root password -- generated once" "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" -"$SET_VAR" --if-empty --comment "Datastream connection password -- generated once" "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" -# GOOGLE_CLOUD_PROJECT is intentionally NOT set here: Cloud Shell already exports -# it (= the active project = PROJECT_ID), so the google-genai/Vertex SDK and agy -# read it from there. bootkon's own code uses PROJECT_ID. -"$SET_VAR" --comment "Route google-genai through Vertex AI" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE -"$SET_VAR" --comment "Location for the GenAI/Vertex client" "$LOCAL" GOOGLE_CLOUD_LOCATION global -"$SET_VAR" --comment "Gemini model id used by the Cymbal agents" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash -"$SET_VAR" --comment "Conversational Analytics data agent id" "$LOCAL" BK_DATA_AGENT_ID cymbal-data-agent -"$SET_VAR" --comment "Cymbal DB host (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_HOST localhost -"$SET_VAR" --comment "Cymbal DB port (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_PORT 5432 +# NOTE: the stream's secrets (BK_DB_PASSWORD/BK_DS_PASSWORD) and runtime config +# are NOT written here -- they are created by content/agenticdata/bk-init, which +# `. bk` runs at initialization, before the first tutorial render. This script +# keeps only the heavy, visible GCP work: APIs, IAM, service accounts, pip, agy. # Pre-configure Antigravity CLI (agy). It is already installed in Cloud Shell # and authenticates with the ambient Cloud Shell credentials (ADC); it reads @@ -165,7 +145,4 @@ else fi echo -echo "Bootstrap complete. The stream configuration is in vars.local.sh." -echo "Load it into this terminal with:" -echo " . bk" -echo "(New terminals pick it up automatically.)" +echo "Bootstrap complete." diff --git a/content/agenticdata/bk-init b/content/agenticdata/bk-init new file mode 100755 index 0000000..b9dfbfd --- /dev/null +++ b/content/agenticdata/bk-init @@ -0,0 +1,29 @@ +#!/bin/bash +# Stream init hook for agenticdata, run by `. bk` on every initialization and +# reload -- AFTER project validation and BEFORE the first tutorial render, so +# everything written here is already available when the labs are rendered. +# +# Contract (see .scripts/bk): idempotent, local-only, instant. Secrets are +# written once (--if-empty); deterministic runtime config is upserted. Heavy, +# visible GCP work (APIs, IAM, service accounts, pip) lives in bk-bootstrap, +# which participants run in Lab 1. + +set -eu + +LOCAL="$BK_DIR/vars.local.sh" +SET_VAR="$BK_DIR/.scripts/bk-set-var" + +# Generated once: Lab 1 creates the Cloud SQL instance with BK_DB_PASSWORD and +# Lab 2 the Datastream connection profile with BK_DS_PASSWORD. +"$SET_VAR" --if-empty --comment "Cloud SQL (Postgres) root password -- generated once" "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" +"$SET_VAR" --if-empty --comment "Datastream connection password -- generated once" "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" + +# Deterministic runtime config for the ADK / Antigravity (agy) tooling. +# GOOGLE_CLOUD_PROJECT is intentionally NOT set: Cloud Shell already exports it +# (= the active project = PROJECT_ID); bootkon's own code uses PROJECT_ID. +"$SET_VAR" --comment "Route google-genai through Vertex AI" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE +"$SET_VAR" --comment "Location for the GenAI/Vertex client" "$LOCAL" GOOGLE_CLOUD_LOCATION global +"$SET_VAR" --comment "Gemini model id used by the Cymbal agents" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash +"$SET_VAR" --comment "Conversational Analytics data agent id" "$LOCAL" BK_DATA_AGENT_ID cymbal-data-agent +"$SET_VAR" --comment "Cymbal DB host (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_HOST localhost +"$SET_VAR" --comment "Cymbal DB port (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_PORT 5432 diff --git a/content/agenticdata/labs/1_environment_setup.md b/content/agenticdata/labs/1_environment_setup.md index fd37118..f0a79af 100644 --- a/content/agenticdata/labs/1_environment_setup.md +++ b/content/agenticdata/labs/1_environment_setup.md @@ -37,23 +37,13 @@ One rule for today: **you** run the infrastructure commands, **agy** writes code ### Assign permissions -Execute the following script. It installs Python dependencies, grants IAM roles, creates a service account for data-quality scans, and generates the database passwords for your project. It runs for about two minutes — you can inspect bk-bootstrap while it works: +Execute the following script. It installs Python dependencies, grants IAM roles, creates a service account for data-quality scans, and pre-configures your AI co-engineer. It runs for about two minutes — you can inspect bk-bootstrap while it works: ```bash content/agenticdata/bk-bootstrap ``` -The script wrote your generated database passwords and agent settings to `vars.local.sh`, so **every new terminal picks them up automatically**. Reload this already-open one: - -```bash -. bk -``` - -Then reload the tutorial once — this renders your freshly generated passwords directly into the commands of the next labs: - -```bash -bk-start -``` +Your database passwords were already generated during setup and live in `vars.local.sh` — the commands below use them as `$BK_DB_PASSWORD`, and **every terminal picks them up automatically**. ### Create the network path diff --git a/content/agenticdata/src/datagen/simulate.py b/content/agenticdata/src/datagen/simulate.py index c46ad5e..ea62868 100755 --- a/content/agenticdata/src/datagen/simulate.py +++ b/content/agenticdata/src/datagen/simulate.py @@ -39,7 +39,7 @@ def log(action, detail): def main(): password = os.environ.get("BK_DB_PASSWORD") if not password: - sys.exit("BK_DB_PASSWORD is not set. Run '. bk' to reload it (written to vars.local.sh by bk-bootstrap).") + sys.exit("BK_DB_PASSWORD is not set. Run '. bk' to reload it (written to vars.local.sh at setup by bk-init).") host = os.environ.get("BK_CYMBAL_DB_HOST", "localhost") port = int(os.environ.get("BK_CYMBAL_DB_PORT", "5432")) From 34a74d03e0e5345013c29907ff9d32e9d0f45388 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:28:03 +0200 Subject: [PATCH 09/28] readme: make a failed bootkon download loud instead of silent Review item #1 (P0). `wget -qO-` swallows every error (404, DNS, proxy, GitHub outage) and `. <(...)` happily sources the empty stream with exit 0 -- a participant pasting the launch command saw literally nothing happen. The fallback echo inside the process substitution prints a clear error with a recovery instruction instead. Identical in all five stream READMEs. --- content/agenticdata/README.md | 3 ++- content/agents/README.md | 3 ++- content/data/README.md | 3 ++- content/devex/README.md | 3 ++- content/example/README.md | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/content/agenticdata/README.md b/content/agenticdata/README.md index 91abde7..9d32114 100644 --- a/content/agenticdata/README.md +++ b/content/agenticdata/README.md @@ -172,7 +172,8 @@ And copy & paste the following command and press return: ```bash MY_NAME="" # your (first) name, shown in the greeting (optional) -BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/agents/README.md b/content/agents/README.md index 92b9138..4deaf8c 100644 --- a/content/agents/README.md +++ b/content/agents/README.md @@ -80,7 +80,8 @@ And copy & paste the following command and press return: ```bash MY_NAME="" # your (first) name, shown in the greeting (optional) -BK_STREAM=agents BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=agents BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/data/README.md b/content/data/README.md index ad5906d..cf9a065 100644 --- a/content/data/README.md +++ b/content/data/README.md @@ -134,7 +134,8 @@ And copy & paste the following command and press return: ```bash MY_NAME="" # your (first) name, shown in the greeting (optional) -BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/devex/README.md b/content/devex/README.md index bf9701c..3ad299e 100644 --- a/content/devex/README.md +++ b/content/devex/README.md @@ -80,7 +80,8 @@ And copy & paste the following command and press return: ```bash MY_NAME="" # your (first) name, shown in the greeting (optional) -BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/example/README.md b/content/example/README.md index 90f8caa..72d144b 100644 --- a/content/example/README.md +++ b/content/example/README.md @@ -1,4 +1,5 @@ ```bash MY_NAME="" # your (first) name, shown in the greeting (optional) -BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` \ No newline at end of file From 1ac3ec419b0f5b63bd10419255f69be82c9eeded Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:28:24 +0200 Subject: [PATCH 10/28] bk-start: fail with one clear message when the checkout is missing Review item #12 (P1). `cd $BK_DIR` was unquoted and unchecked: with a deleted checkout it printed the raw shell error, carried on in $HOME, tracebacked in the renderer, opened a stale tutorial and then opened $HOME as the workspace -- three misleading symptoms for one cause. The initialization guard now runs first (so an unloaded environment gets the '. bk' hint instead of a confusing cd error), and a missing folder stops immediately with a reinstall instruction. --- .scripts/bk-start | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.scripts/bk-start b/.scripts/bk-start index cdc5d6a..e22540c 100755 --- a/.scripts/bk-start +++ b/.scripts/bk-start @@ -1,7 +1,5 @@ #!/bin/sh -cd $BK_DIR - if [ -z "$BK_INITIALIZED" ]; then echo "Bootkon has not been initialized." echo "Please execute: " @@ -9,7 +7,13 @@ if [ -z "$BK_INITIALIZED" ]; then exit 1 fi -bk-tutorial content/${BK_STREAM}/TUTORIAL.md +cd "$BK_DIR" || { + echo "ERROR: the bootkon folder $BK_DIR is missing." >&2 + echo "Paste the setup command from the workshop instructions again to reinstall." >&2 + exit 1 +} + +bk-tutorial "content/${BK_STREAM}/TUTORIAL.md" if [ "${BK_STREAM}" != "devex" ]; then cloudshell open-workspace . fi From a28ea9aac4b498982e7ca525d24f3d7590ff24ec Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:28:47 +0200 Subject: [PATCH 11/28] bk-tutorial: guard the reload memory and stop depending on the caller's cwd Review item #11 (P1) plus the P3 double-launch note. Two confirmed failure modes: (a) a bare `bk-tutorial` with no memory file wrote an empty line into .tutorial.memory BEFORE rendering, dumped a raw Python traceback and still launched the stale /tmp/bootkon.md -- and every later bare invocation reproduced the traceback; (b) the memory stores a checkout-relative path and the renderer's include searchpath is a relative 'content', so invoking it from a lab directory (src/dataform, src/adk) tracebacked as well. Now: cd "$BK_DIR" first (with a clear message when the env is not loaded or the checkout is gone), refuse cleanly when there is nothing to open, write the memory only after a successful render, and quote all paths. The duplicated launch-tutorial call is now documented as a deliberate workaround. --- .scripts/bk-tutorial | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/.scripts/bk-tutorial b/.scripts/bk-tutorial index 022bbf3..fad8630 100755 --- a/.scripts/bk-tutorial +++ b/.scripts/bk-tutorial @@ -8,21 +8,44 @@ # # Usage: bk-tutorial -MEMORY="${BK_DIR}/.tutorial.memory" +if [[ -z "${BK_DIR}" ]]; then + echo "The bootkon environment is not loaded. Please run: . bk" >&2 + exit 1 +fi + +# Tutorial paths (argument and memory) are relative to the checkout, and the +# renderer resolves includes relative to ./content -- so always work from +# $BK_DIR, no matter which lab directory the participant is currently in. +cd "$BK_DIR" || { + echo "ERROR: the bootkon folder $BK_DIR is missing." >&2 + echo "Paste the setup command from the workshop instructions again to reinstall." >&2 + exit 1 +} + +MEMORY="$BK_DIR/.tutorial.memory" RENDERED=/tmp/bootkon.md FILE=$1 -if [[ -z "${FILE}" ]]; then - if [[ -f "${MEMORY}" ]]; then - FILE=$(cat $MEMORY) - echo "No tutorial specified. Reloading last opened file ${FILE}" - fi +if [[ -z "$FILE" && -f "$MEMORY" ]]; then + FILE="$(cat "$MEMORY")" + echo "No tutorial specified. Reloading last opened file $FILE" fi -echo "$FILE" > $MEMORY +if [[ -z "$FILE" || ! -f "$FILE" ]]; then + echo "No tutorial to open. Type bk-start to open your stream's tutorial." >&2 + exit 1 +fi + +bk-render-jinja2 "$FILE" "$RENDERED" || exit 1 + +# Remember the file only after a successful render, so a bad invocation can +# never poison the reload memory. +echo "$FILE" > "$MEMORY" -bk-render-jinja2 $FILE $RENDERED -cloudshell launch-tutorial $RENDERED +# Launching twice with a pause works around the tutorial pane sometimes not +# picking up the new content on the first call (observed in Cloud Shell). +# If a future Cloud Shell release makes the second call unnecessary, drop it. +cloudshell launch-tutorial "$RENDERED" sleep 1.5 -cloudshell launch-tutorial $RENDERED +cloudshell launch-tutorial "$RENDERED" From 167017f2c5412c74afb6f8aaaf15979da7b21c56 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:29:14 +0200 Subject: [PATCH 12/28] bk-render-jinja2: render unset scalars as empty and warn on empty secrets Review item #8 renderer half (P1) and the residual belt-and-braces from item #18. os.getenv() returns Python None for unset variables, and Jinja rendered {{ PROJECT_NUMBER }} as the literal string 'None' inside copy-paste gcloud commands (service-None@gcp-sa-dataplex...) -- unlike undefined variables, which render empty. All scalar context values are now coerced to ''. Additionally, any BK_*PASSWORD/SECRET/TOKEN/KEY variable referenced by the template that is empty at render time now produces a loud stderr warning with the recovery command, instead of silently baking blank credentials into the labs. --- .scripts/bk-render-jinja2 | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.scripts/bk-render-jinja2 b/.scripts/bk-render-jinja2 index 8ac8bd7..dca0895 100755 --- a/.scripts/bk-render-jinja2 +++ b/.scripts/bk-render-jinja2 @@ -30,16 +30,27 @@ def render_tutorial(content): # Pass all variables starting with BK_ to the template context context = {k: v for k, v in os.environ.items() if k.startswith("BK_")} + # Unset scalars must render as '' (like undefined variables do), never as + # the literal string 'None' inside copy-paste commands. context.update( ON_ARGOLIS=(os.getenv("GCP_USERNAME") or "").endswith("altostrat.com"), - PROJECT_ID=os.getenv("PROJECT_ID"), - PROJECT_NUMBER=os.getenv("PROJECT_NUMBER"), - REGION=os.getenv("REGION"), - MY_NAME=os.getenv("MY_NAME"), - GCP_USERNAME=os.getenv("GCP_USERNAME"), + PROJECT_ID=os.getenv("PROJECT_ID") or "", + PROJECT_NUMBER=os.getenv("PROJECT_NUMBER") or "", + REGION=os.getenv("REGION") or "", + MY_NAME=os.getenv("MY_NAME") or "", + GCP_USERNAME=os.getenv("GCP_USERNAME") or "", author=partial(render_author), ) + # A secret that renders empty produces labs with blank passwords in the + # copy-paste commands -- warn loudly instead of failing an hour later. + for name in sorted(set(re.findall( + r"{{\s*(BK_\w*(?:PASSWORD|SECRET|TOKEN|KEY)\w*)\s*}}", content))): + if not os.getenv(name): + print(f"WARNING: {name} is referenced by this tutorial but is " + f"empty -- commands using it will not work. Run '. bk' and " + f"then bk-start to refresh.", file=sys.stderr) + rendered = template.render(**context) rendered = re.sub( From 036f95a9af6b82127f127d1ccc08b8b708a8dccb Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:29:59 +0200 Subject: [PATCH 13/28] bk-set-var: harden the comment flag, appends, and file creation Review items #26 and #27 (P3), plus the defensive chmod from item #4. Three edge cases: (a) a --comment containing a newline would write its second line outside the '# ' prefix -- executed by every terminal that sources vars.local.sh -- and (b) '--comment' as the last argument died on set -u with a cryptic message; both are now rejected with a clear error. (c) Appending to a hand-edited file without a trailing newline concatenated two exports onto one line; a newline is now ensured first. Additionally, when bk-set-var itself creates the target file it now chmods it to 600 immediately: the file may receive secrets later, and the default umask would leave it world-readable. --- .scripts/bk-set-var | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.scripts/bk-set-var b/.scripts/bk-set-var index 5b9871a..dc72f80 100755 --- a/.scripts/bk-set-var +++ b/.scripts/bk-set-var @@ -39,11 +39,27 @@ comment="" while [ $# -gt 0 ]; do case "$1" in --if-empty) if_empty=1; shift ;; - --comment) comment="$2"; shift 2 ;; # one-line description, written above the export + --comment) # one-line description, written above the export + if [ $# -lt 2 ]; then + echo "bk-set-var: --comment requires an argument" >&2 + exit 1 + fi + comment="$2"; shift 2 ;; *) break ;; esac done +# A multi-line comment would put its second line outside the '# ' prefix -- +# executed by every new terminal that sources the file. Reject it like a +# newline in the value. +case "$comment" in + *" +"*) + echo "bk-set-var: comment may not contain a newline" >&2 + exit 1 + ;; +esac + file="${1:?bk-set-var: FILE required}" name="${2:?bk-set-var: NAME required}" value="${3-}" @@ -66,7 +82,12 @@ case "$value" in ;; esac -[ -f "$file" ] || touch "$file" +# If we create the file it may receive secrets later, so make it private +# from the start (a file created via the default umask would be 644). +if [ ! -f "$file" ]; then + touch "$file" + chmod 600 "$file" +fi match="^[[:space:]]*export[[:space:]]+${name}=" @@ -108,6 +129,11 @@ if grep -qE "$match" "$file"; then ' "$file" > "$tmp" else cat "$file" > "$tmp" + # A hand-edited file may lack a trailing newline; appending directly would + # concatenate two exports on one line. + if [ -s "$tmp" ] && [ -n "$(tail -c1 "$tmp")" ]; then + echo >> "$tmp" + fi [ -n "$comment" ] && printf '# %s\n' "$comment" >> "$tmp" printf '%s\n' "$new_line" >> "$tmp" fi From fbf5b01c4d17b2cc16c328785540af30b71fb5df Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:30:24 +0200 Subject: [PATCH 14/28] bk-remove-block: resolve symlinks before the atomic replace Review item #28 (P3). The atomic temp-file + mv rewrite replaced a symlinked ~/.bashrc with a regular file, silently detaching dotfile-managed setups from their repository. Resolve the target with readlink -f first so the rewrite lands in the real file and the symlink stays intact. --- .scripts/bk-remove-block | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.scripts/bk-remove-block b/.scripts/bk-remove-block index 0e569d7..9d2f6a9 100755 --- a/.scripts/bk-remove-block +++ b/.scripts/bk-remove-block @@ -30,6 +30,10 @@ set -eu file="${1:?bk-remove-block: FILE required}" [ -f "$file" ] || exit 0 +# Operate on the real file: replacing a symlinked ~/.bashrc with a regular +# file would silently detach dotfile setups. +file="$(readlink -f "$file")" + start='# >>> bootkon >>>' end='# <<< bootkon <<<' From dec05e368237c604d8ea3e371bad6aebcf1277ef Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:31:57 +0200 Subject: [PATCH 15/28] bk: drop BK_INITIALIZED -- a non-empty PROJECT_NUMBER is the ready signal Review item #25b (P2). Since the no-project fail-fast landed, BK_INITIALIZED was set in the same validated branch as PROJECT_NUMBER, making the two signals exactly equivalent -- a second flag carrying the same bit, and one more line in the participant-visible vars.local.sh. Consumers now guard on the precondition they actually consume: * stream bootstraps: [[ -z "$PROJECT_NUMBER" ]] -- strictly better, since the service-agent emails they create are built from $PROJECT_NUMBER; this also closes item #2's belt-and-braces gap (a bootstrap running with an empty project number) for free. data's pip install now runs after the guard instead of before it. * bk-start: same check, same message. * bk's first-run-vs-reload detection captures ${PROJECT_NUMBER:-} (present on reload via the ~/.bashrc source, absent on a first or failed run -- so a retry after failed validation shows the full welcome banner again). * bk-info drops the redundant display line; bk-deactivate's hint says 'unset PROJECT_NUMBER'. Older vars.local.sh files are migrated opportunistically: the stale BK_INITIALIZED line (and its comment) is deleted; nothing reads it anymore. --- .scripts/bk | 26 +++++++++++++++++--------- .scripts/bk-deactivate | 2 +- .scripts/bk-info | 1 - .scripts/bk-start | 2 +- content/agenticdata/bk-bootstrap | 4 +++- content/data/bk-bootstrap | 7 +++++-- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 36c8c10..e007ea2 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -73,10 +73,12 @@ if [ -z "$BK_STREAM" ]; then return 1 fi -# Remember whether this is a first run or a reload (before we set the flag), and -# the explicitly requested repo/stream (env or argument) so they can win over a -# stale persisted value further below. -BK_WAS_INITIALIZED="${BK_INITIALIZED:-}" +# Remember whether this is a first run or a reload, and the explicitly +# requested repo/stream (env or argument) so they can win over a stale +# persisted value further below. A non-empty PROJECT_NUMBER is THE "bk has +# validated a project" signal: it is set and persisted only after successful +# validation (and reaches new terminals via the ~/.bashrc block). +BK_WAS_INITIALIZED="${PROJECT_NUMBER:-}" BK_REQ_REPO="$BK_REPO" BK_REQ_STREAM="$BK_STREAM" BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it @@ -145,6 +147,13 @@ if [ ! -f "$LOCAL" ]; then chmod 600 "$LOCAL" fi +# Migrate older vars.local.sh files: BK_INITIALIZED is gone (a non-empty +# PROJECT_NUMBER is the "environment is ready" signal now). Nothing reads the +# stale line anymore; drop it and its comment to keep the file clean. +if grep -q '^export BK_INITIALIZED=' "$LOCAL" 2>/dev/null; then + sed -i '/^# Set to 1 once bk validated the project/d; /^export BK_INITIALIZED=/d' "$LOCAL" +fi + echo -e "Sourcing $(readlink -f "$BK_DIR/vars.sh")" source "$BK_DIR/vars.sh" echo -e "Sourcing $(readlink -f "$LOCAL")" @@ -258,11 +267,10 @@ fi export PROJECT_NUMBER echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." -# Only NOW mark the environment initialized -- AFTER the project validated, so the -# stream bootstraps (which guard on BK_INITIALIZED and rely on PROJECT_NUMBER) -# never run on a failed setup. bk-start (wget path) runs after this line. -export BK_INITIALIZED=1 -"$BK_SET_VAR" --comment "Set to 1 once bk validated the project; stream bootstraps guard on it" "$LOCAL" BK_INITIALIZED 1 +# There is deliberately no separate "initialized" flag: the PROJECT_NUMBER +# export/persist above happens only on this validated path, so bk-start and +# the stream bootstraps guard on a non-empty PROJECT_NUMBER -- the exact +# precondition they consume. A failed or aborted setup never sets it. # Optional per-stream init hook: content/$BK_STREAM/bk-init. Runs on every `. bk` # once the project is validated -- and, crucially, BEFORE the first tutorial diff --git a/.scripts/bk-deactivate b/.scripts/bk-deactivate index c4de44e..c34556a 100755 --- a/.scripts/bk-deactivate +++ b/.scripts/bk-deactivate @@ -22,4 +22,4 @@ fi echo "Removed the bootkon block from ~/.bashrc." echo "Open a new terminal for a clean environment (this one keeps the variables" -echo "until you 'unset BK_INITIALIZED' or start a new shell)." +echo "until you 'unset PROJECT_NUMBER' or start a new shell)." diff --git a/.scripts/bk-info b/.scripts/bk-info index d1387e1..4683250 100755 --- a/.scripts/bk-info +++ b/.scripts/bk-info @@ -14,7 +14,6 @@ echo -e "BK_REPO_URL: ${GREEN}$BK_REPO_URL${NC}" echo -e "BK_DIR: ${GREEN}$BK_DIR${NC}" echo -e "BK_STREAM: ${GREEN}$BK_STREAM${NC}" echo -e "BK_BRANCH: ${GREEN}$BK_BRANCH${NC}" -echo -e "BK_INITIALIZED: ${GREEN}$BK_INITIALIZED${NC}" echo -e "config file: ${GREEN}$BK_DIR/vars.local.sh${NC}" echo -e "PROJECT_ID: ${GREEN}$PROJECT_ID${NC}" echo -e "PROJECT_NUMBER: ${GREEN}$PROJECT_NUMBER${NC}" diff --git a/.scripts/bk-start b/.scripts/bk-start index e22540c..f6acba8 100755 --- a/.scripts/bk-start +++ b/.scripts/bk-start @@ -1,6 +1,6 @@ #!/bin/sh -if [ -z "$BK_INITIALIZED" ]; then +if [ -z "$PROJECT_NUMBER" ]; then echo "Bootkon has not been initialized." echo "Please execute: " echo " . bk (including the dot)" diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index ea39d1d..28ef7b7 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -6,7 +6,9 @@ # those commands live inline in the labs, so the connectivity variant can be # swapped by applying option1-vpc-peering-proxy.diff to the lab markdown. -[[ "$BK_INITIALIZED" != "1" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 +# PROJECT_NUMBER is set by `. bk` only after successful project validation -- +# and it is exactly what the service-agent emails below are built from. +[[ -z "$PROJECT_NUMBER" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 # The Datastream service agent must exist and hold its service-agent role # BEFORE Lab 1 creates the private connection: on a fresh project the agent diff --git a/content/data/bk-bootstrap b/content/data/bk-bootstrap index 712e711..a97ccef 100755 --- a/content/data/bk-bootstrap +++ b/content/data/bk-bootstrap @@ -1,9 +1,12 @@ #!/bin/bash # author: Fabian Hirschmann -pip install -r content/data/requirements.txt +# PROJECT_NUMBER is set by `. bk` only after successful project validation -- +# and the service-account emails below are built from it. Guard before doing +# any work (the pip install used to run before this check). +[[ -z "$PROJECT_NUMBER" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 -[[ "$BK_INITIALIZED" != "1" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 +pip install -r content/data/requirements.txt # Create Knowledge Catalog DQ Service Account first here to avoid potential timing issues during role assignment later DQ_SCAN_SERVICE_ACCOUNT="dataquality-service-account" From 69be9cc627ae334e6d8d98681388434d4de56f59 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:32:17 +0200 Subject: [PATCH 16/28] bk: replace the dead apt/git install block with a hard error Review item #24 (P2). The block was unreachable behind the CLOUD_SHELL guard (Cloud Shell ships git) -- and had it ever run, it would have greeted a first-day participant with a sudo password prompt and 30 seconds of apt noise. A missing git now produces one clear error pointing back to a fresh Cloud Shell. --- .scripts/bk | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index e007ea2..21dd8d9 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -104,9 +104,12 @@ if [ -z "$PROJECT_ID" ]; then return 1 fi +# Cloud Shell ships git; if it is ever missing something is deeply wrong with +# the environment -- do not greet a novice with a sudo prompt and apt noise. if ! command -v git &> /dev/null; then - sudo apt update - sudo apt install -y git + err 'git is not available in this environment. Please open a fresh Cloud Shell' + err 'at https://shell.cloud.google.com and paste the setup command again.' + return 1 fi if [ -d "$BK_DIR/.git" ]; then From a57b1bf8b21b7869780b70a86a669962ae4070c0 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:33:17 +0200 Subject: [PATCH 17/28] bk: one clear instruction per failure, for people who never used a terminal Review items #3 (P0), #13 and #31 (P1/P3). The worst confirmed dead-end: rejecting the 'Authorize Cloud Shell' popup fails validation, the auto bk-start never runs, and no message anywhere mentioned bk-start -- the participant ended up authorized, environment loaded, staring at a bare prompt. Fixes: * Validation failure now disambiguates auth-vs-wrong-project itself (via gcloud auth print-access-token) and prints exactly one instruction, always ending with 'type bk-start to open the tutorial'. * The reload message gains '-- tutorial closed? Type bk-start.' * The stream-typo error lists only real streams (directories, minus common/contributing/example -- it previously offered 'contributing' and even CONCEPT.md) and says how to recover: fix BK_STREAM, paste again. * 'Script is not sourced. Please source it.' and 'Please run this script in Cloud Shell.' are reworded without jargon. --- .scripts/bk | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 21dd8d9..d6bc044 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -46,13 +46,15 @@ err() { echo -e "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" if [ -z "$CLOUD_SHELL" ]; then - err 'Please run this script in Cloud Shell.' + err 'bootkon runs inside Google Cloud Shell.' + err 'Open https://shell.cloud.google.com and paste the setup command there.' return 1 fi if [ "${BASH_SOURCE[0]}" == "$0" ]; then - err 'Script is not sourced. Please source it.' - err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' + err 'This command must start with a dot and a space -- the dot loads bootkon' + err 'into your current terminal. Example:' + err ' . bk' exit 1 fi @@ -124,7 +126,8 @@ cd "$BK_DIR" if [ ! -d "content/${BK_STREAM}" ]; then err "Stream '${BK_STREAM}' does not exist (no content/${BK_STREAM} directory)." - err "Available streams: $(ls content | grep -vx common | tr '\n' ' ')" + err "Available streams: $(cd content && ls -d */ | tr -d '/' | grep -vE '^(common|contributing|example)$' | tr '\n' ' ')" + err "To fix this, correct BK_STREAM in the command you pasted, then paste it again." return 1 fi @@ -246,12 +249,20 @@ EOF # The project is guaranteed selected (checked before the clone). Validate it now # -- this also triggers the "Authorize Cloud Shell" popup at setup rather than # mid-lab. gcloud already points at the picker's project, so no `config set`. +# On failure, disambiguate the cause (auth vs. wrong project) so the +# participant gets exactly ONE instruction -- and always say how to get the +# tutorial afterwards, because on this path it was never opened. if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then - echo -e "${RED}ERROR: Unable to access project $PROJECT_ID.${NC}" - echo -e "${RED}Two common causes:${NC}" - echo -e "${RED} 1. Your session needs re-authentication (typical after a break) —${NC}" - echo -e "${RED} re-authorize Cloud Shell when prompted, or run 'gcloud auth login', then '. bk' again.${NC}" - echo -e "${RED} 2. The wrong project is selected — switch it in the Cloud Shell project selector.${NC}" + if ! gcloud auth print-access-token >/dev/null 2>&1; then + err "Your Cloud Shell session is not authorized (this happens after clicking" + err "Reject on the authorization popup, or after a longer break)." + err "Run '. bk' again and click 'Authorize' this time." + else + err "Your account has no access to project $PROJECT_ID." + err "Pick your event project in the project selector at the top of the" + err "Cloud Shell window, then run '. bk' again." + fi + err "Afterwards, type bk-start to open the tutorial." return 1 fi @@ -309,7 +320,7 @@ if [ -z "$BK_WAS_INITIALIZED" ]; then echo -e ' `---''---`' echo else - echo -e "${GREEN}Reloaded bootkon environment${NC} (BK_STREAM=${YELLOW}$BK_STREAM${NC} BK_BRANCH=${YELLOW}$BK_BRANCH${NC})." + echo -e "${GREEN}Reloaded bootkon environment${NC} (BK_STREAM=${YELLOW}$BK_STREAM${NC} BK_BRANCH=${YELLOW}$BK_BRANCH${NC}) — tutorial closed? Type ${GREEN}bk-start${NC}." fi unset BK_WAS_INITIALIZED From 0725ffd537810f7b36155178a002de2f7e49abe0 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:34:16 +0200 Subject: [PATCH 18/28] bk: probe repo/branch/stream before cloning; never trust a broken checkout Review items #19b, #4 (P0) and #16 (P1), all confirmed with repros. * Pre-clone probe: one HEAD request against the raw content/$BK_STREAM/TUTORIAL.md validates BK_REPO, BK_BRANCH and BK_STREAM at once, in about a second, BEFORE any clone happens or any workspace opens. A typo now fails with one clear message and leaves nothing behind (works for forks too -- the probe uses the launch-provided BK_REPO). The post-clone stream check stays as belt and braces. * The clone itself is checked; a failure explains the recovery instead of letting the sourced script keep running without set -e. * The reuse guard now requires .git AND vars.sh: a clone that died between the two (disk full, closed tab) used to pass the old .git-only test, print a reassuring green message, and then cascade -- cp failed, sources failed, vars.local.sh got recreated world-readable via touch, and bk-start opened a blank tutorial. Such a directory is now rejected with an explicit rm -rf recovery line. * Re-pasting the launch command (the documented way to pick up mid-event content fixes) now fast-forwards a clean existing checkout quietly; plain '. bk' reloads stay offline. Local changes are left alone with a yellow note. --- .scripts/bk | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index d6bc044..69f0e49 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -114,12 +114,48 @@ if ! command -v git &> /dev/null; then return 1 fi -if [ -d "$BK_DIR/.git" ]; then - echo -e "${GREEN}Not cloning $BK_REPO_URL because $BK_DIR already exists.${NC}" +# Reuse a healthy checkout, reject a broken one (an interrupted first clone +# used to wedge the participant permanently: the .git test passed, the copy of +# vars.sh failed, and every re-paste printed a reassuring green message), or +# clone fresh -- after a one-second remote probe that catches repo/branch/ +# stream typos BEFORE any clone happens or any workspace opens. +BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten +if [ -d "$BK_DIR/.git" ] && [ -f "$BK_DIR/vars.sh" ]; then + echo -e "${GREEN}Using the existing checkout in $BK_DIR.${NC}" + # Re-pasting the launch command is the documented way to pick up mid-event + # content fixes, so on that path -- and only there; plain `. bk` reloads + # stay offline and instant -- fast-forward a clean tree quietly. + if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then + if [ -n "$(git -C "$BK_DIR" status --porcelain 2>/dev/null)" ]; then + echo -e "${YELLOW}Not updating it: you have local changes (to update: git -C $BK_DIR pull).${NC}" + elif ! git -C "$BK_DIR" pull --ff-only --quiet 2>/dev/null; then + echo -e "${YELLOW}Could not update it; continuing with the current files.${NC}" + fi + fi +elif [ -e "$BK_DIR" ]; then + err "$BK_DIR exists but does not look like a working bootkon checkout" + err "(usually a previous setup was interrupted). To start over, run:" + err " rm -rf $BK_DIR" + err "and then paste the setup command again." + return 1 else - BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten + # One HEAD request against the raw stream tutorial validates BK_REPO, + # BK_BRANCH and BK_STREAM at once. (--spider returns non-zero on a 404; + # curl -fsIL is the fallback in case the wget build GETs instead.) + _probe="https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/content/${BK_STREAM}/TUTORIAL.md" + if ! wget -q --spider "$_probe" 2>/dev/null && ! curl -fsIL "$_probe" >/dev/null 2>&1; then + err "Could not find stream '${BK_STREAM}' on ${BK_REPO}@${BK_BRANCH} (or the network is down)." + err "Check BK_STREAM / BK_REPO / BK_BRANCH in the command you pasted, then paste it again." + err "(Tried: $_probe)" + return 1 + fi echo -e "Cloning branch $BK_BRANCH from $BK_REPO_URL into $BK_DIR..." - git clone --branch "$BK_BRANCH" "$BK_REPO_URL" "$BK_DIR" + if ! git clone --branch "$BK_BRANCH" "$BK_REPO_URL" "$BK_DIR"; then + err "Cloning $BK_REPO_URL failed -- see the git message above." + err "Usually this is a network hiccup: paste the setup command again." + err "If $BK_DIR was partially created, remove it first with: rm -rf $BK_DIR" + return 1 + fi fi cd "$BK_DIR" @@ -304,7 +340,7 @@ if [ -x "$BK_DIR/content/$BK_STREAM/bk-init" ]; then fi unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS -unset detected_account _origin _actual _branch +unset detected_account _origin _actual _branch _probe unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME if [ -z "$BK_WAS_INITIALIZED" ]; then From 501fee58deb725679c6b123e905966e1dde4ffbd Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:35:14 +0200 Subject: [PATCH 19/28] bk: validate vars.local.sh before sourcing it -- everywhere Review item #6 (P1, reproduced). The tutorial explicitly sends beginners into vars.local.sh; a single lost quote meant every new terminal printed a raw 'unexpected EOF' bash error and silently stopped loading everything below the broken line (stream, passwords) -- with every follow-up error ('not initialized', 'BK_STREAM is not set') pointing away from the cause. bk itself happily re-sourced the broken file. Both consumers now run bash -n first: bk fails fast quoting the offending line and saying exactly what to do; the ~/.bashrc block warns once per terminal with the same instruction instead of spraying parser errors. --- .scripts/bk | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.scripts/bk b/.scripts/bk index 69f0e49..0de2861 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -189,6 +189,19 @@ if [ ! -f "$LOCAL" ]; then chmod 600 "$LOCAL" fi +# The tutorial explicitly sends beginners into vars.local.sh; one lost quote +# would otherwise surface as a raw 'unexpected EOF' in every new terminal, +# with everything below the broken line (stream, secrets) silently unloaded +# and every follow-up error pointing away from the cause. Check the file and +# name the actual problem before sourcing it. (The ~/.bashrc block written +# below carries the same guard.) +if ! bash -n "$LOCAL" 2>/dev/null; then + err "Your configuration file has a syntax error (usually a missing quote):" + err " $(bash -n "$LOCAL" 2>&1 | head -n 1)" + err "Open $LOCAL in the editor, fix that line, save it, then run '. bk' again." + return 1 +fi + # Migrate older vars.local.sh files: BK_INITIALIZED is gone (a non-empty # PROJECT_NUMBER is the "environment is ready" signal now). Nothing reads the # stale line anymore; drop it and its comment to keep the file clean. @@ -277,7 +290,14 @@ cat >> "$BASHRC" </dev/null; then + source "\$BK_DIR/vars.local.sh" + else + echo "bootkon: vars.local.sh has a syntax error (usually a missing quote) --" >&2 + echo "bootkon: open it in the editor, fix the marked line, then run: . bk" >&2 + fi +fi export PROJECT_ID="\${DEVSHELL_PROJECT_ID:-\$GOOGLE_CLOUD_PROJECT}" # live from Cloud Shell # <<< bootkon <<< EOF From caf51ec9e69286d9bda37020a7b034671e7b2cd8 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:35:33 +0200 Subject: [PATCH 20/28] bk: sync a stale GOOGLE_CLOUD_PROJECT with the validated project Review item #9 (P1). After a project switch in the picker, the current shell's ambient GOOGLE_CLOUD_PROJECT still names the old project until the next prompt refresh -- and agy plus the google-genai/Vertex SDKs read exactly that variable (the agenticdata stream deliberately no longer pins it). Re-export it to the freshly validated PROJECT_ID when the two differ, so SDK calls made in this very session land in the right project. --- .scripts/bk | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.scripts/bk b/.scripts/bk index 0de2861..52ac9ab 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -322,6 +322,15 @@ if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then return 1 fi +# agy and the google-genai/Vertex SDKs read GOOGLE_CLOUD_PROJECT, and the +# agenticdata stream deliberately does not pin it. Cloud Shell refreshes the +# variable on every new prompt, but THIS shell still holds the pre-switch +# value right after a project change -- align it with the project we just +# validated so SDK calls in this very session hit the right project. +if [ -n "${GOOGLE_CLOUD_PROJECT:-}" ] && [ "$GOOGLE_CLOUD_PROJECT" != "$PROJECT_ID" ]; then + export GOOGLE_CLOUD_PROJECT="$PROJECT_ID" +fi + # PROJECT_NUMBER is the only project value NOT available from the environment, so # cache it in vars.local.sh. Recompute only when missing or when the live project # changed; every new terminal then reads it with no network call. From fce580bc512f149b5694678ab4e7f2cac6b1c340 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:36:56 +0200 Subject: [PATCH 21/28] bk: stop persisting the three derived repo variables Review items #25 and #29 (P2). BK_GITHUB_USERNAME, BK_GITHUB_REPOSITORY and BK_REPO_URL have no consumer outside the current session (verified by full-repo grep): bk recomputes them from BK_REPO on every run and the persisted copies were never read back -- they only buried the two lines in vars.local.sh participants actually edit. The in-session exports stay (they cost nothing). Old files are migrated: the stale lines are dropped, which also prevents a stale BK_REPO_URL from clobbering the freshly derived value when vars.local.sh is re-sourced after the stream init hook. Fork safety re-checked in the review: forks rely on BK_REPO + BK_BRANCH (derived from the checkout's git origin), not on these three -- rendered image links keep following the fork automatically. bk-info: shows BK_REPO plus the derived clone URL instead of three redundant lines, prints an explicit 'run . bk' hint when the environment is not loaded (instead of a wall of blank values), and the bundled-dataset check is now gated to the data stream with neutral wording -- healthy agenticdata/devex/agents setups no longer see a red 'does not exist'. --- .scripts/bk | 24 ++++++++++++++++-------- .scripts/bk-info | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 52ac9ab..b4aa732 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -202,11 +202,18 @@ if ! bash -n "$LOCAL" 2>/dev/null; then return 1 fi -# Migrate older vars.local.sh files: BK_INITIALIZED is gone (a non-empty -# PROJECT_NUMBER is the "environment is ready" signal now). Nothing reads the -# stale line anymore; drop it and its comment to keep the file clean. -if grep -q '^export BK_INITIALIZED=' "$LOCAL" 2>/dev/null; then - sed -i '/^# Set to 1 once bk validated the project/d; /^export BK_INITIALIZED=/d' "$LOCAL" +# Migrate older vars.local.sh files: drop lines bk no longer persists -- +# BK_INITIALIZED (a non-empty PROJECT_NUMBER is the "environment is ready" +# signal now) and the three repo values derived fresh from BK_REPO on every +# run. Besides burying the two lines participants actually edit, a stale +# persisted copy would clobber the freshly derived value when the file is +# re-sourced later in this script. +if grep -qE '^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_REPO_URL)=' "$LOCAL" 2>/dev/null; then + sed -i -E -e '/^# Set to 1 once bk validated the project/d' \ + -e '/^# GitHub org\/user \(first half of BK_REPO\)/d' \ + -e '/^# GitHub repo name \(second half of BK_REPO\)/d' \ + -e '/^# Clone URL derived from BK_REPO/d' \ + -e '/^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_REPO_URL)=/d' "$LOCAL" fi echo -e "Sourcing $(readlink -f "$BK_DIR/vars.sh")" @@ -274,9 +281,10 @@ echo -e "Config: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP "$BK_SET_VAR" --comment "Selected stream; picks content//" "$LOCAL" BK_STREAM "$BK_STREAM" "$BK_SET_VAR" --comment "GitHub repo the labs are served from (user/repo)" "$LOCAL" BK_REPO "$BK_REPO" "$BK_SET_VAR" --comment "Checkout branch; used for rendered image links" "$LOCAL" BK_BRANCH "$BK_BRANCH" -"$BK_SET_VAR" --comment "GitHub org/user (first half of BK_REPO)" "$LOCAL" BK_GITHUB_USERNAME "$BK_GITHUB_USERNAME" -"$BK_SET_VAR" --comment "GitHub repo name (second half of BK_REPO)" "$LOCAL" BK_GITHUB_REPOSITORY "$BK_GITHUB_REPOSITORY" -"$BK_SET_VAR" --comment "Clone URL derived from BK_REPO" "$LOCAL" BK_REPO_URL "$BK_REPO_URL" +# BK_GITHUB_USERNAME / BK_GITHUB_REPOSITORY / BK_REPO_URL are NOT persisted: +# they are recomputed from BK_REPO above on every run and have no consumer +# outside this session -- persisting them only buried the lines participants +# actually care about. # Install/refresh a single guarded block in ~/.bashrc so every new terminal # loads the environment by sourcing vars.local.sh. bk-remove-block strips any diff --git a/.scripts/bk-info b/.scripts/bk-info index 4683250..374ae15 100755 --- a/.scripts/bk-info +++ b/.scripts/bk-info @@ -7,10 +7,13 @@ MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color +if [ -z "$BK_DIR" ] || [ -z "$BK_STREAM" ]; then + echo "The bootkon environment is not loaded. Please run: . bk" + exit 1 +fi + echo -e "${CYAN}Variables:${NC}" -echo -e "BK_GITHUB_USERNAME: ${GREEN}$BK_GITHUB_USERNAME${NC}" -echo -e "BK_GITHUB_REPOSITORY: ${GREEN}$BK_GITHUB_REPOSITORY${NC}" -echo -e "BK_REPO_URL: ${GREEN}$BK_REPO_URL${NC}" +echo -e "BK_REPO: ${GREEN}$BK_REPO${NC} (https://github.com/${BK_REPO}.git)" echo -e "BK_DIR: ${GREEN}$BK_DIR${NC}" echo -e "BK_STREAM: ${GREEN}$BK_STREAM${NC}" echo -e "BK_BRANCH: ${GREEN}$BK_BRANCH${NC}" @@ -22,11 +25,14 @@ echo echo -e "${CYAN}Info:${NC}" -if [ ! -d "${BK_DIR}/data" ]; then - echo -e "data: ${RED}does not exist${NC}" -else - echo -e "data: ${GREEN}exists${NC}" - du -sh "${BK_DIR}/data" +# The bundled dataset exists only for the data stream; other streams should +# not see an alarming red line about it. +if [ "$BK_STREAM" = "data" ]; then + if [ -d "${BK_DIR}/data" ]; then + echo -e "data: ${GREEN}exists${NC} ($(du -sh "${BK_DIR}/data" 2>/dev/null | cut -f1))" + else + echo -e "data: not downloaded yet (the data-stream labs fetch it when needed)" + fi fi -echo -e "git origin: ${GREEN}$(git -C "${BK_DIR}" config --get remote.origin.url 2>/dev/null)${NC}" \ No newline at end of file +echo -e "git origin: ${GREEN}$(git -C "${BK_DIR}" config --get remote.origin.url 2>/dev/null)${NC}" From 0431617ab1a5dc90b5c1daf3b8ecd369636390d2 Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:37:54 +0200 Subject: [PATCH 22/28] bk: stop teleporting the caller's shell to the repo root Review item #14 (P1, confirmed). Because bk is sourced, its mid-script cd changed the participant's shell -- on every reload and every error path. Labs put people into src/dataform or src/adk, and the next relative lab command after a '. bk' reload failed with 'No such file or directory', with nothing connecting the failure to the reload. The script now works from absolute paths throughout and only a successful FIRST run ends with a cd to the repo root (where the labs' relative commands are meant to start). Reloads and all failure paths leave the current directory untouched. --- .scripts/bk | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index b4aa732..59c20ac 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -14,7 +14,8 @@ # PROJECT_ID/GCP_USERNAME, render and open the tutorial. # * Re-run (already initialized): a fast reload -- re-source vars.local.sh, # fill still-empty config from the environment, refresh PROJECT_NUMBER and -# the active gcloud project. Handy after editing vars.local.sh. +# the active gcloud project. Handy after editing vars.local.sh. A reload +# never changes the caller's working directory. # # Author: Fabian Hirschmann # @@ -158,11 +159,13 @@ else fi fi -cd "$BK_DIR" - -if [ ! -d "content/${BK_STREAM}" ]; then +# NOTE: this script never cd's the caller's shell mid-run -- it is sourced, so +# a cd here would teleport the participant out of their lab directory on every +# reload and every error path. Everything below works from absolute paths; only +# a successful FIRST run ends with a cd to the repo root (see the bottom). +if [ ! -d "$BK_DIR/content/${BK_STREAM}" ]; then err "Stream '${BK_STREAM}' does not exist (no content/${BK_STREAM} directory)." - err "Available streams: $(cd content && ls -d */ | tr -d '/' | grep -vE '^(common|contributing|example)$' | tr '\n' ' ')" + err "Available streams: $(cd "$BK_DIR/content" && ls -d */ | tr -d '/' | grep -vE '^(common|contributing|example)$' | tr '\n' ' ')" err "To fix this, correct BK_STREAM in the command you pasted, then paste it again." return 1 fi @@ -392,6 +395,9 @@ if [ -z "$BK_WAS_INITIALIZED" ]; then echo -e " _)( )(_ This will open the tutorial and IDE workspace." echo -e ' `---''---`' echo + # A first run ends at the repo root: that is where the labs' relative + # commands are meant to run from. + cd "$BK_DIR" else echo -e "${GREEN}Reloaded bootkon environment${NC} (BK_STREAM=${YELLOW}$BK_STREAM${NC} BK_BRANCH=${YELLOW}$BK_BRANCH${NC}) — tutorial closed? Type ${GREEN}bk-start${NC}." fi From d66cfcb8d50c363f63f2225f196057bf6ba8adab Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:39:40 +0200 Subject: [PATCH 23/28] bk: quiet output -- narration behind BK_DEBUG, one summary line, 1-line reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review item #23 (P2). A first run used to open with the readlink'ed /proc/self/fd/... pipe path (literally the first terminal output a novice ever sees), two 'Sourcing /home/...' lines, a PATH notice and duplicated project/config lines; the closing banner said 'type bk-start' although on the wget path bk-start auto-runs one second later -- many participants dutifully typed it again. Internal narration now sits behind a BK_DEBUG gate (dbg helper). A first run prints one merged summary line (project · account · stream) plus the welcome banner, whose call-to-action is now path-aware: 'Opening the tutorial now -- if it ever closes, type bk-start' on the wget path. A reload prints exactly one line, ending with the bk-start hint. The existing-checkout notice only appears when re-pasting the launch command, not on every reload. --- .scripts/bk | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 59c20ac..1b01e1f 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -44,7 +44,14 @@ err() { echo -e "${RED}Error: $1${NC}" >&2 } -echo -e "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" +# Internal narration (paths, sourcing, PATH bookkeeping) is hidden unless +# BK_DEBUG is set: a /proc/self/fd/... pipe path must not be the first thing +# a first-day participant ever sees in a terminal. +dbg() { + if [ -n "${BK_DEBUG:-}" ]; then echo -e "$1"; fi +} + +dbg "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" if [ -z "$CLOUD_SHELL" ]; then err 'bootkon runs inside Google Cloud Shell.' @@ -122,11 +129,11 @@ fi # stream typos BEFORE any clone happens or any workspace opens. BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten if [ -d "$BK_DIR/.git" ] && [ -f "$BK_DIR/vars.sh" ]; then - echo -e "${GREEN}Using the existing checkout in $BK_DIR.${NC}" # Re-pasting the launch command is the documented way to pick up mid-event # content fixes, so on that path -- and only there; plain `. bk` reloads - # stay offline and instant -- fast-forward a clean tree quietly. + # stay offline, instant and quiet -- fast-forward a clean tree. if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then + echo -e "${GREEN}Using the existing checkout in $BK_DIR.${NC}" if [ -n "$(git -C "$BK_DIR" status --porcelain 2>/dev/null)" ]; then echo -e "${YELLOW}Not updating it: you have local changes (to update: git -C $BK_DIR pull).${NC}" elif ! git -C "$BK_DIR" pull --ff-only --quiet 2>/dev/null; then @@ -175,7 +182,7 @@ fi BK_SCRIPTS="$BK_DIR/.scripts" case ":$PATH:" in *":$BK_SCRIPTS:"*) ;; - *) echo -e "${MAGENTA}Adding $BK_SCRIPTS to your current session's PATH${NC}" + *) dbg "${MAGENTA}Adding $BK_SCRIPTS to your current session's PATH${NC}" export PATH="$BK_SCRIPTS:$PATH" ;; esac @@ -187,7 +194,7 @@ BK_REMOVE_BLOCK="$BK_SCRIPTS/bk-remove-block" # the checked-in template on first run; it is git-ignored, never pushed, and # holds generated secrets, so keep it private. if [ ! -f "$LOCAL" ]; then - echo -e "Creating $(readlink -f "$LOCAL") from vars.sh" + echo -e "Creating your personal config file: $LOCAL" cp "$BK_DIR/vars.sh" "$LOCAL" chmod 600 "$LOCAL" fi @@ -219,9 +226,9 @@ if grep -qE '^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_ -e '/^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_REPO_URL)=/d' "$LOCAL" fi -echo -e "Sourcing $(readlink -f "$BK_DIR/vars.sh")" +dbg "Sourcing $(readlink -f "$BK_DIR/vars.sh")" source "$BK_DIR/vars.sh" -echo -e "Sourcing $(readlink -f "$LOCAL")" +dbg "Sourcing $(readlink -f "$LOCAL")" source "$LOCAL" # GCP_USERNAME: autodetect once, filling only if still empty (a manual edit wins). @@ -276,8 +283,6 @@ if [ -z "$_branch" ] || [ "$_branch" = "HEAD" ]; then fi export BK_BRANCH="$_branch" -echo -e "Config: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP_USERNAME${NC} REGION=${YELLOW}$REGION${NC}" - # Persist the managed variables and install the ~/.bashrc block BEFORE the # project is validated below: even if validation fails (expired auth, wrong # project) a freshly opened terminal must still have PATH and the seed vars. @@ -355,7 +360,6 @@ if [ -z "$PROJECT_NUMBER" ] || [ "$PROJECT_ID" != "${BK_PROJECT_NUMBER_OF:-}" ]; "$BK_SET_VAR" --comment "Internal: the project the cached PROJECT_NUMBER belongs to" "$LOCAL" BK_PROJECT_NUMBER_OF "$PROJECT_ID" fi export PROJECT_NUMBER -echo -e "Project: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." # There is deliberately no separate "initialized" flag: the PROJECT_NUMBER # export/persist above happens only on this validated path, so bk-start and @@ -384,6 +388,7 @@ unset detected_account _origin _actual _branch _probe unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME if [ -z "$BK_WAS_INITIALIZED" ]; then + echo -e "Project: ${YELLOW}$PROJECT_ID${NC} (number $PROJECT_NUMBER) · Account: ${YELLOW}$GCP_USERNAME${NC} · Stream: ${YELLOW}$BK_STREAM${NC}" echo echo -e " __ ---------------------------------------" echo -e " _(\ |${RED}@@${NC}| | |" @@ -391,15 +396,20 @@ if [ -z "$BK_WAS_INITIALIZED" ]; then echo -e " \___|----| | __ | |" echo -e " \ }{ /\ )_ / _\ ---------------------------------------" echo -e " /\__/\ \__O (__" - echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." - echo -e " _)( )(_ This will open the tutorial and IDE workspace." + if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then + echo -e " (--/\--) \__/ Opening the ${BK_STREAM} tutorial and IDE workspace now." + echo -e " _)( )(_ If the tutorial ever closes, type ${GREEN}\e[1mbk-start\e[0m${NC} to reopen it." + else + echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." + echo -e " _)( )(_ This will open the tutorial and IDE workspace." + fi echo -e ' `---''---`' echo # A first run ends at the repo root: that is where the labs' relative # commands are meant to run from. cd "$BK_DIR" else - echo -e "${GREEN}Reloaded bootkon environment${NC} (BK_STREAM=${YELLOW}$BK_STREAM${NC} BK_BRANCH=${YELLOW}$BK_BRANCH${NC}) — tutorial closed? Type ${GREEN}bk-start${NC}." + echo -e "${GREEN}Reloaded bootkon${NC}: project ${YELLOW}$PROJECT_ID${NC} · account ${YELLOW}$GCP_USERNAME${NC} · stream ${YELLOW}$BK_STREAM${NC}@${BK_BRANCH} — tutorial closed? Type ${GREEN}bk-start${NC}." fi unset BK_WAS_INITIALIZED From aa7c22486f87b7d31f2b9978c074d9da1988961e Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:41:34 +0200 Subject: [PATCH 24/28] bk: interactive first-run onboarding -- the robot greets and asks the name Review item *20* (P2 headline; supersedes #20, absorbs #33/#49). Editing MY_NAME inside a pasted quoted string was the single most error-prone moment of the launch: deleting a quote left a novice at a bare dquote> prompt with both lines swallowed, and retyping the value unquoted ran their name as a command. That edit no longer exists. On the very first launch (no vars.local.sh yet) the ASCII robot now greets UP FRONT, confirms the project it is about to use (with a pointer to the picker if it is the wrong one), and asks 'What should I call you?' via read. This works on the wget launch because '. <(wget ...)' sources through a process substitution and stdin stays on the tty -- unlike wget | bash. Guards: prompt only on a true first run AND an interactive stdin AND an empty MY_NAME; MY_NAME=... in the launch env still short-circuits it, so organizer/scripted/headless flows behave as before. The answer is persisted via the existing BK_REQ_MY_NAME capture (manual edits keep winning), and is sanitized either way -- newlines would corrupt vars.local.sh, quotes would look broken in the rendered greeting. The first run now closes with a compact 'All set, !' line that is path-aware (the wget path opens the tutorial itself; only the manual path says to type bk-start). The MY_NAME line is gone from all five READMEs -- the launch is a single, edit-free paste again. --- .scripts/bk | 66 ++++++++++++++++++++++++----------- content/agenticdata/README.md | 1 - content/agents/README.md | 1 - content/data/README.md | 1 - content/devex/README.md | 1 - content/example/README.md | 1 - 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 1b01e1f..64c94d4 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -83,12 +83,8 @@ if [ -z "$BK_STREAM" ]; then return 1 fi -# Remember whether this is a first run or a reload, and the explicitly -# requested repo/stream (env or argument) so they can win over a stale -# persisted value further below. A non-empty PROJECT_NUMBER is THE "bk has -# validated a project" signal: it is set and persisted only after successful -# validation (and reaches new terminals via the ~/.bashrc block). -BK_WAS_INITIALIZED="${PROJECT_NUMBER:-}" +# Remember the explicitly requested repo/stream (env or argument) so they can +# win over a stale persisted value further below. BK_REQ_REPO="$BK_REPO" BK_REQ_STREAM="$BK_STREAM" BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it @@ -114,6 +110,42 @@ if [ -z "$PROJECT_ID" ]; then return 1 fi +# ── Interactive first-run onboarding ────────────────────────────────────── +# On the very first launch (no vars.local.sh yet) the robot greets UP FRONT +# and asks for the participant's name, instead of silently running the whole +# setup and dropping a banner at the end. Because the name is collected here, +# the launch command needs no MY_NAME= edit anymore -- a single, edit-free +# paste, which kills the whole editing-inside-a-quoted-string trap (deleting +# a quote left novices at a bare dquote> prompt as their first terminal +# experience). Guards: +# * first run only -- never on `. bk` reloads or in new terminals; +# * interactive only ([ -t 0 ]: sourcing `. <(wget ...)` keeps stdin on +# the tty, unlike `wget | bash`, so read works on the launch path); +# * MY_NAME=... in the launch env still short-circuits the prompt, so +# organizer/scripted/headless flows are unaffected. +BK_FIRST_RUN="" +if [ ! -f "$BK_DIR/vars.local.sh" ]; then + BK_FIRST_RUN=1 + echo + echo -e " __ ---------------------------------------" + echo -e " _(\ |${RED}@@${NC}| | |" + echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" + echo -e " \___|----| | __ | |" + echo -e " \ }{ /\ )_ / _\ ---------------------------------------" + echo -e " /\__/\ \__O (__" + echo -e " (--/\--) \__/ Setting you up in project ${YELLOW}${PROJECT_ID}${NC}." + echo -e " _)( )(_ (Wrong project? Switch it in the selector at the" + echo -e ' `---''---`'" top of this window and paste the command again.)" + echo + if [ -z "$BK_REQ_MY_NAME" ] && [ -t 0 ]; then + read -r -p "What should I call you? (first name, or press Enter to skip) " BK_REQ_MY_NAME + fi +fi +# Whether prompted or passed via the launch env: strip characters that must +# never reach vars.local.sh or the greeting (bk-set-var rejects newlines +# outright; stray quotes would just look broken in the rendered markdown). +BK_REQ_MY_NAME="$(printf '%s' "$BK_REQ_MY_NAME" | tr -d "\r\n'\"")" + # Cloud Shell ships git; if it is ever missing something is deeply wrong with # the environment -- do not greet a novice with a sudo prompt and apt noise. if ! command -v git &> /dev/null; then @@ -387,24 +419,16 @@ unset BASHRC LOCAL BK_SET_VAR BK_REMOVE_BLOCK BK_SCRIPTS unset detected_account _origin _actual _branch _probe unset BK_REQ_REPO BK_REQ_STREAM BK_REQ_MY_NAME -if [ -z "$BK_WAS_INITIALIZED" ]; then +if [ -n "$BK_FIRST_RUN" ]; then + # The robot already greeted up front; close the first run with the facts + # and a path-aware call to action (on the wget path bk-start auto-runs). echo -e "Project: ${YELLOW}$PROJECT_ID${NC} (number $PROJECT_NUMBER) · Account: ${YELLOW}$GCP_USERNAME${NC} · Stream: ${YELLOW}$BK_STREAM${NC}" - echo - echo -e " __ ---------------------------------------" - echo -e " _(\ |${RED}@@${NC}| | |" - echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" - echo -e " \___|----| | __ | |" - echo -e " \ }{ /\ )_ / _\ ---------------------------------------" - echo -e " /\__/\ \__O (__" if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then - echo -e " (--/\--) \__/ Opening the ${BK_STREAM} tutorial and IDE workspace now." - echo -e " _)( )(_ If the tutorial ever closes, type ${GREEN}\e[1mbk-start\e[0m${NC} to reopen it." + echo -e "${GREEN}All set${MY_NAME:+, $MY_NAME}!${NC} Opening the ${BK_STREAM} tutorial and IDE workspace now." + echo -e "If the tutorial ever closes, type ${GREEN}bk-start${NC} to reopen it." else - echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." - echo -e " _)( )(_ This will open the tutorial and IDE workspace." + echo -e "${GREEN}All set${MY_NAME:+, $MY_NAME}!${NC} Type ${GREEN}bk-start${NC} to open the ${BK_STREAM} tutorial and IDE workspace." fi - echo -e ' `---''---`' - echo # A first run ends at the repo root: that is where the labs' relative # commands are meant to run from. cd "$BK_DIR" @@ -412,7 +436,7 @@ else echo -e "${GREEN}Reloaded bootkon${NC}: project ${YELLOW}$PROJECT_ID${NC} · account ${YELLOW}$GCP_USERNAME${NC} · stream ${YELLOW}$BK_STREAM${NC}@${BK_BRANCH} — tutorial closed? Type ${GREEN}bk-start${NC}." fi -unset BK_WAS_INITIALIZED +unset BK_FIRST_RUN if [ "$(basename ${BASH_SOURCE[0]})" != "bk" ]; then # This script is run the first time from GitHub diff --git a/content/agenticdata/README.md b/content/agenticdata/README.md index 9d32114..df45dc0 100644 --- a/content/agenticdata/README.md +++ b/content/agenticdata/README.md @@ -171,7 +171,6 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` diff --git a/content/agents/README.md b/content/agents/README.md index 4deaf8c..fd12ddb 100644 --- a/content/agents/README.md +++ b/content/agents/README.md @@ -79,7 +79,6 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=agents BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` diff --git a/content/data/README.md b/content/data/README.md index cf9a065..a095999 100644 --- a/content/data/README.md +++ b/content/data/README.md @@ -133,7 +133,6 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` diff --git a/content/devex/README.md b/content/devex/README.md index 3ad299e..d613ba0 100644 --- a/content/devex/README.md +++ b/content/devex/README.md @@ -79,7 +79,6 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` diff --git a/content/example/README.md b/content/example/README.md index 72d144b..ac4ac92 100644 --- a/content/example/README.md +++ b/content/example/README.md @@ -1,5 +1,4 @@ ```bash -MY_NAME="" # your (first) name, shown in the greeting (optional) BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` \ No newline at end of file From 720c34a0a28f75e4d0e52aad6791c4d1811ca85d Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:45:14 +0200 Subject: [PATCH 25/28] bk: wrap the whole body in _bk_main -- a truncated download runs nothing Review item #17 (P1, confirmed). The launch streams this file through '. <(wget -qO- ...)': with a plain script body, a connection drop mid- download executed everything up to the break. The worst window was the ~/.bashrc heredoc -- bash's at-EOF heredoc behavior would append a bootkon block WITHOUT its end marker, which bk-remove-block then (by design, data-loss safety) preserves forever, so blocks accumulate on every re-run and bk-deactivate refuses to clean up. The entire body now lives in _bk_main, invoked once at the very bottom: a truncated download is a parse error that executes NOTHING (verified in a sandbox: cut at 60% -> 'syntax error: unexpected end of file', zero side effects). Internal working variables became `local`, so early error returns no longer leak a dozen internals into the participant's shell; the helper functions are unset right after the call. The body is deliberately not re-indented to keep the diff and future history readable. Also verified in the sandbox: first run end-to-end (robot, name capture, config seeding, bashrc block, summary), 1-line reload that preserves the caller's cwd, broken-vars.local.sh error path, and status propagation for both the sourced and the executed case. --- .scripts/bk | 67 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/.scripts/bk b/.scripts/bk index 64c94d4..e25a530 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -32,13 +32,23 @@ # BK_STREAM: stream to start (a directory under content/). # BK_DIR: checkout location (defaults to ~/bootkon). -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -MAGENTA='\033[0;35m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color +# The whole body lives in _bk_main, called once at the very bottom. The launch +# streams this file via `. <(wget ...)`: with a plain script body, a connection +# drop would execute everything up to the break (worst case inside the +# ~/.bashrc heredoc, appending a block without its end marker). Wrapped in a +# function, a truncated download is a parse error that executes NOTHING. It +# also lets internal working variables be `local` instead of leaking into the +# participant's shell on early returns. The body is deliberately NOT indented, +# to keep diffs and history readable. +_bk_main() { + +local RED='\033[0;31m' +local GREEN='\033[0;32m' +local YELLOW='\033[0;33m' +local BLUE='\033[0;34m' +local MAGENTA='\033[0;35m' +local CYAN='\033[0;36m' +local NC='\033[0m' # No Color err() { echo -e "${RED}Error: $1${NC}" >&2 @@ -85,9 +95,9 @@ fi # Remember the explicitly requested repo/stream (env or argument) so they can # win over a stale persisted value further below. -BK_REQ_REPO="$BK_REPO" -BK_REQ_STREAM="$BK_STREAM" -BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it +local BK_REQ_REPO="$BK_REPO" +local BK_REQ_STREAM="$BK_STREAM" +local BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it export BK_REPO="$BK_REPO" export BK_STREAM="$BK_STREAM" @@ -123,7 +133,7 @@ fi # the tty, unlike `wget | bash`, so read works on the launch path); # * MY_NAME=... in the launch env still short-circuits the prompt, so # organizer/scripted/headless flows are unaffected. -BK_FIRST_RUN="" +local BK_FIRST_RUN="" if [ ! -f "$BK_DIR/vars.local.sh" ]; then BK_FIRST_RUN=1 echo @@ -182,7 +192,7 @@ else # One HEAD request against the raw stream tutorial validates BK_REPO, # BK_BRANCH and BK_STREAM at once. (--spider returns non-zero on a 404; # curl -fsIL is the fallback in case the wget build GETs instead.) - _probe="https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/content/${BK_STREAM}/TUTORIAL.md" + local _probe="https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/content/${BK_STREAM}/TUTORIAL.md" if ! wget -q --spider "$_probe" 2>/dev/null && ! curl -fsIL "$_probe" >/dev/null 2>&1; then err "Could not find stream '${BK_STREAM}' on ${BK_REPO}@${BK_BRANCH} (or the network is down)." err "Check BK_STREAM / BK_REPO / BK_BRANCH in the command you pasted, then paste it again." @@ -211,16 +221,16 @@ fi # Add the scripts directory to the current session's PATH (idempotent). The # permanent export lives in the ~/.bashrc block written further below. -BK_SCRIPTS="$BK_DIR/.scripts" +local BK_SCRIPTS="$BK_DIR/.scripts" case ":$PATH:" in *":$BK_SCRIPTS:"*) ;; *) dbg "${MAGENTA}Adding $BK_SCRIPTS to your current session's PATH${NC}" export PATH="$BK_SCRIPTS:$PATH" ;; esac -LOCAL="$BK_DIR/vars.local.sh" -BK_SET_VAR="$BK_SCRIPTS/bk-set-var" -BK_REMOVE_BLOCK="$BK_SCRIPTS/bk-remove-block" +local LOCAL="$BK_DIR/vars.local.sh" +local BK_SET_VAR="$BK_SCRIPTS/bk-set-var" +local BK_REMOVE_BLOCK="$BK_SCRIPTS/bk-remove-block" # vars.local.sh is the single per-participant config + state file. Seed it from # the checked-in template on first run; it is git-ignored, never pushed, and @@ -264,7 +274,7 @@ dbg "Sourcing $(readlink -f "$LOCAL")" source "$LOCAL" # GCP_USERNAME: autodetect once, filling only if still empty (a manual edit wins). -detected_account="$(gcloud config get-value account 2>/dev/null || true)" +local detected_account="$(gcloud config get-value account 2>/dev/null || true)" [ "$detected_account" = "(unset)" ] && detected_account="" if [ -z "$GCP_USERNAME" ] && [ -n "$detected_account" ]; then "$BK_SET_VAR" "$LOCAL" GCP_USERNAME "$detected_account" @@ -289,8 +299,8 @@ export PROJECT_ID # clobber it) and from the checkout's own origin remote, so BK_REPO and the # rendered image links always match the files actually on disk -- even if # vars.local.sh is stale or a fork was requested against an existing checkout. -_origin="$(git -C "$BK_DIR" remote get-url origin 2>/dev/null || true)" -_actual="$(printf '%s' "$_origin" | sed -E 's#^git@github\.com:##; s#^https?://github\.com/##; s#\.git$##')" +local _origin="$(git -C "$BK_DIR" remote get-url origin 2>/dev/null || true)" +local _actual="$(printf '%s' "$_origin" | sed -E 's#^git@github\.com:##; s#^https?://github\.com/##; s#\.git$##')" if [ -n "$_actual" ]; then if [ "$_actual" != "$BK_REQ_REPO" ]; then echo -e "${YELLOW}Note: $BK_DIR is a checkout of $_actual, not $BK_REQ_REPO.${NC}" @@ -309,7 +319,7 @@ export BK_STREAM="$BK_REQ_STREAM" # BK_BRANCH is derived from the checkout. Fall back cleanly for a detached HEAD # ('HEAD') or a non-git directory (empty), which are not valid clone targets. -_branch="$(git -C "$BK_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" +local _branch="$(git -C "$BK_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" if [ -z "$_branch" ] || [ "$_branch" = "HEAD" ]; then _branch="${BK_BRANCH:-main}" fi @@ -329,7 +339,7 @@ export BK_BRANCH="$_branch" # Install/refresh a single guarded block in ~/.bashrc so every new terminal # loads the environment by sourcing vars.local.sh. bk-remove-block strips any # existing block robustly; then we append a fresh one. -BASHRC=~/.bashrc +local BASHRC=~/.bashrc "$BK_REMOVE_BLOCK" "$BASHRC" 2>/dev/null || true cat >> "$BASHRC" </dev/null || exit $_bk_rc From 5a244b0294e1b26bb5e46731393f2ab750d1de1e Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:46:18 +0200 Subject: [PATCH 26/28] agenticdata: bk-bootstrap fails loudly instead of succeeding silently Review items #5 (P0, five confirmed issues) and #30 (P3). * (a) No cd, no error tracking: invoked from a fresh tab (cwd=$HOME) the relative pip path failed, the error scrolled away under 25 IAM lines, and the script still ended with 'Bootstrap complete.' It now pins the cwd to $BK_DIR, checks every step, keeps going past individual failures, and only claims success when nothing failed -- otherwise: 'Bootstrap finished with N error(s)', pointing up and at a safe re-run. * (b) The critical Datastream service-agent block -- the very reason this script exists -- was fully silenced (>/dev/null 2>&1, unchecked); its failure resurfaced an hour later as the exact Lab-1 PERMISSION_DENIED it was added to prevent. stderr stays visible and each of the three calls is checked. * (c) Both IAM role loops now pass --condition=None (the service-agent grant already did): any conditional binding in the sandbox project made gcloud stop and prompt interactively mid-bootstrap, up to 18 times, with the prompt half-hidden by the stdout redirect. * (d) Service-account creation is idempotent (describe-before-create): re-running the bootstrap is the natural novice recovery and no longer prints a scary unexplained 'already exists' error. * (e) The guard now requires the exact precondition the script consumes (non-empty PROJECT_NUMBER; landed with the BK_INITIALIZED removal). * #30: agy trustedWorkspaces interpolates $BK_DIR instead of hardcoding $HOME/bootkon. --- content/agenticdata/bk-bootstrap | 71 +++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index 28ef7b7..a047d9a 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -1,28 +1,50 @@ #!/bin/bash # Bootstraps the Agentic Data Bootkon stream: Python deps, APIs, IAM, -# service accounts, and the secrets used across the labs. +# service accounts, and the agy pre-configuration. # # It deliberately does NOT create the Cloud SQL instance or any networking: # those commands live inline in the labs, so the connectivity variant can be # swapped by applying option1-vpc-peering-proxy.diff to the lab markdown. +# +# Error handling: every step is checked and counted. The script keeps going +# so one flaky IAM call does not abort the remaining setup, but it only +# claims success when NOTHING failed -- a silent failure here used to +# resurface an hour later as a cryptic PERMISSION_DENIED in Lab 1. # PROJECT_NUMBER is set by `. bk` only after successful project validation -- # and it is exactly what the service-agent emails below are built from. [[ -z "$PROJECT_NUMBER" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 +# The lab invokes this script by relative path from the repo root, but a fresh +# tab starts in $HOME -- pin the cwd so the relative paths below always work. +cd "$BK_DIR" || { echo "ERROR: could not enter $BK_DIR -- please run '. bk' first." >&2; exit 1; } + +FAILURES=0 +step_failed() { + FAILURES=$((FAILURES + 1)) + echo "ERROR: $1 -- see the message above." >&2 +} + # The Datastream service agent must exist and hold its service-agent role # BEFORE Lab 1 creates the private connection: on a fresh project the agent # is created lazily, and `private-connections create` then fails with # PERMISSION_DENIED on the network attachment (verified live). The IAM grant # also needs a few minutes to propagate -- doing this FIRST, before the -# pip install and the role loop below, buys that time for free. -gcloud services enable datastream.googleapis.com >/dev/null 2>&1 -gcloud beta services identity create --service=datastream.googleapis.com --project=$PROJECT_ID >/dev/null 2>&1 +# pip install and the role loop below, buys that time for free. This block +# is the reason this script exists: keep its errors loud. +echo "Preparing the Datastream service agent (Lab 1 depends on it) ..." +gcloud services enable datastream.googleapis.com \ + || step_failed "enabling the Datastream API failed" +gcloud beta services identity create --service=datastream.googleapis.com \ + --project="$PROJECT_ID" >/dev/null \ + || step_failed "creating the Datastream service identity failed" gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-datastream.iam.gserviceaccount.com" \ - --role="roles/datastream.serviceAgent" --condition=None >>/dev/null + --role="roles/datastream.serviceAgent" --condition=None >/dev/null \ + || step_failed "granting the Datastream service-agent role failed" -pip install -r content/agenticdata/requirements.txt +pip install -r content/agenticdata/requirements.txt \ + || step_failed "installing the Python dependencies (pip) failed" echo "Enabling APIs (idempotent; the tutorial enables these too) ..." gcloud services enable \ @@ -44,13 +66,20 @@ gcloud services enable \ serviceusage.googleapis.com \ cloudresourcemanager.googleapis.com \ iam.googleapis.com \ - artifactregistry.googleapis.com + artifactregistry.googleapis.com \ + || step_failed "enabling the APIs failed" # Knowledge Catalog data-quality service account: profile/quality scans in -# Lab 4 run as this SA (custom execution identity). +# Lab 4 run as this SA (custom execution identity). Creation is idempotent: +# re-running the bootstrap is the natural novice recovery and must not greet +# them with an unexplained 'already exists' error. DQ_SCAN_SERVICE_ACCOUNT="dataquality-service-account" DQ_SCAN_SA_EMAIL="${DQ_SCAN_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" -gcloud iam service-accounts create $DQ_SCAN_SERVICE_ACCOUNT --display-name="Knowledge Catalog DQ Service Account" +if ! gcloud iam service-accounts describe "$DQ_SCAN_SA_EMAIL" >/dev/null 2>&1; then + gcloud iam service-accounts create "$DQ_SCAN_SERVICE_ACCOUNT" \ + --display-name="Knowledge Catalog DQ Service Account" \ + || step_failed "creating the data-quality service account failed" +fi # A freshly created SA can take a few seconds to become visible to IAM; # granting against it too early fails with "does not exist". for i in $(seq 1 15); do @@ -60,6 +89,9 @@ done # Roles for the participant. Sandbox users are typically project owners # already; the explicit list keeps the stream portable to other setups. +# --condition=None: a conditional binding anywhere in the project's policy +# would otherwise make gcloud stop and prompt interactively -- mid-bootstrap, +# once per role, with the actual question hidden by the stdout redirect. declare -a user_roles=( "roles/cloudsql.admin" # create/manage the Cymbal instance "roles/datastream.admin" # profiles, private connection, stream @@ -79,7 +111,8 @@ declare -a user_roles=( for role in "${user_roles[@]}"; do echo "Assigning role $role to $GCP_USERNAME in project $PROJECT_ID..." gcloud projects add-iam-policy-binding "$PROJECT_ID" \ - --member="user:$GCP_USERNAME" --role="$role" >>/dev/null + --member="user:$GCP_USERNAME" --role="$role" --condition=None >>/dev/null \ + || step_failed "assigning $role to $GCP_USERNAME failed" done # Roles for the data-quality service account. @@ -94,7 +127,8 @@ declare -a dataquality_roles=( for role in "${dataquality_roles[@]}"; do echo "Assigning role $role to $DQ_SCAN_SA_EMAIL in project $PROJECT_ID..." gcloud projects add-iam-policy-binding "$PROJECT_ID" \ - --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" >>/dev/null + --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" --condition=None >>/dev/null \ + || step_failed "assigning $role to $DQ_SCAN_SA_EMAIL failed" done # NOTE: the stream's secrets (BK_DB_PASSWORD/BK_DS_PASSWORD) and runtime config @@ -125,9 +159,9 @@ if [ ! -f "$AGY_DIR/settings.json" ]; then "security": { "auth": { "selectedType": "cloud-shell" } }, "selectedAuthType": "cloud-shell", "trustedWorkspaces": [ - "$HOME/bootkon", - "$HOME/bootkon/content/agenticdata/src/dataform", - "$HOME/bootkon/content/agenticdata/src/adk" + "$BK_DIR", + "$BK_DIR/content/agenticdata/src/dataform", + "$BK_DIR/content/agenticdata/src/adk" ], "permissions": { "allow": [ @@ -147,4 +181,11 @@ else fi echo -echo "Bootstrap complete." +if [ "$FAILURES" -eq 0 ]; then + echo "Bootstrap complete." +else + echo "Bootstrap finished with $FAILURES error(s) -- scroll up for the details," >&2 + echo "then run this script again (it is safe to re-run). If it keeps failing," >&2 + echo "ask the event staff." >&2 + exit 1 +fi From 42010a68be4828c85e85ad4b6cdcb7a22e88a5ec Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:47:07 +0200 Subject: [PATCH 27/28] tutorial: happy path = zero commands -- verify first, commands in the fallback Review items #21, #10 and the tutorial half of #22 (P1/P2). On the wget launch path, the intro's '. bk' and 'bk-start' steps were pure no-ops (both had just run), costing two commands, ~10 lines of output, and a tutorial that closed and reopened itself while being read. The intro now leads with verification -- 'your PROJECT_ID is X, your GCP_USERNAME is Y; both correct? press START' -- and moves the commands into the only-if-something-is-wrong branch. That branch also fixes the confirmed never-refreshes loop (#10): it says '. bk && bk-start' explicitly, because a plain '. bk' reload never re-renders the pane, so the sidebar kept showing None and beginners re-edited a value that was already correct. A one-line tip covers reopening a closed tutorial. Same change in all four streams. --- content/agenticdata/TUTORIAL.md | 37 +++++++++++++++------------------ content/agents/TUTORIAL.md | 37 +++++++++++++++------------------ content/data/TUTORIAL.md | 37 +++++++++++++++------------------ content/devex/TUTORIAL.md | 37 +++++++++++++++------------------ 4 files changed, 68 insertions(+), 80 deletions(-) diff --git a/content/agenticdata/TUTORIAL.md b/content/agenticdata/TUTORIAL.md index 448181e..6cc89ba 100644 --- a/content/agenticdata/TUTORIAL.md +++ b/content/agenticdata/TUTORIAL.md @@ -31,34 +31,31 @@ echo "Let's go, Cymbal!" Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Set up your environment +### Check your environment -Initialize bootkon. This sets your environment variables and auto-detects your -Google Cloud project and account from Cloud Shell: +The setup command you pasted has already configured everything. Let's verify it: -```bash -. bk -``` - -Then reload the tutorial window on the right-hand side of your screen (run this -again any time you accidentally close the tutorial or the editor): - -```bash -bk-start -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -New terminals load the environment automatically — you only re-run `. bk` after -changing your configuration. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -Now, your +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Only if a value is wrong or shows `None`: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. +Then reload the environment and refresh the values shown on this page: -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'agenticdata/labs/1_environment_setup.md' %} diff --git a/content/agents/TUTORIAL.md b/content/agents/TUTORIAL.md index 4570f51..4b48e71 100644 --- a/content/agents/TUTORIAL.md +++ b/content/agents/TUTORIAL.md @@ -30,34 +30,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Set up your environment +### Check your environment -Initialize bootkon. This sets your environment variables and auto-detects your -Google Cloud project and account from Cloud Shell: +The setup command you pasted has already configured everything. Let's verify it: -```bash -. bk -``` - -Then reload the tutorial window on the right-hand side of your screen (run this -again any time you accidentally close the tutorial or the editor): - -```bash -bk-start -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -New terminals load the environment automatically — you only re-run `. bk` after -changing your configuration. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -Now, your +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Only if a value is wrong or shows `None`: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -If either shows `None` or looks wrong, open `vars.local.sh` by clicking here, set the correct value (no spaces), save, and run `. bk` again. (Your name comes from the launch command; edit `MY_NAME` here to change it.) +Then reload the environment and refresh the values shown on this page: -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'agents/labs/1_environment_setup.md' %} {% include 'agents/labs/2_adk.md' %} diff --git a/content/data/TUTORIAL.md b/content/data/TUTORIAL.md index 390b47e..8af7255 100644 --- a/content/data/TUTORIAL.md +++ b/content/data/TUTORIAL.md @@ -31,34 +31,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Set up your environment +### Check your environment -Initialize bootkon. This sets your environment variables and auto-detects your -Google Cloud project and account from Cloud Shell: +The setup command you pasted has already configured everything. Let's verify it: -```bash -. bk -``` - -Then reload the tutorial window on the right-hand side of your screen (run this -again any time you accidentally close the tutorial or the editor): - -```bash -bk-start -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -New terminals load the environment automatically — you only re-run `. bk` after -changing your configuration. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -Now, your +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Only if a value is wrong or shows `None`: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. +Then reload the environment and refresh the values shown on this page: -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'data/labs/1_environment_setup.md' %} diff --git a/content/devex/TUTORIAL.md b/content/devex/TUTORIAL.md index f1aca95..fd877a1 100644 --- a/content/devex/TUTORIAL.md +++ b/content/devex/TUTORIAL.md @@ -30,34 +30,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Set up your environment +### Check your environment -Initialize bootkon. This sets your environment variables and auto-detects your -Google Cloud project and account from Cloud Shell: +The setup command you pasted has already configured everything. Let's verify it: -```bash -. bk -``` - -Then reload the tutorial window on the right-hand side of your screen (run this -again any time you accidentally close the tutorial or the editor): - -```bash -bk-start -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -New terminals load the environment automatically — you only re-run `. bk` after -changing your configuration. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -Now, your +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Only if a value is wrong or shows `None`: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -If `PROJECT_ID` is wrong, switch to your event project in the Cloud Shell project selector at the top of the window, then run `. bk` again. If `GCP_USERNAME` shows `None`, or your name is missing, open `vars.local.sh` by clicking here, fix it (no spaces), save, and run `. bk` again. +Then reload the environment and refresh the values shown on this page: -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'devex/labs/2_devex_agy_cli.md' %} From f24b245bbd81e3e2503e0ca86239b6e1504063ca Mon Sep 17 00:00:00 2001 From: Fabian Hirschmann Date: Thu, 9 Jul 2026 22:47:42 +0200 Subject: [PATCH 28/28] docs: document the fork caveats Review item #25's follow-up note. Forking is supported by design (BK_REPO and the rendered image links derive from the checkout's git origin), but three caveats were undocumented: the launch line must name the fork, the fork must be public for the tutorial images to load, and a few hardcoded upstream references (bk-legacy-download's dataset repo) do not follow the fork. --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b16bd94..c27c780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,3 +31,18 @@ All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. + +## Running bootkon from a fork + +Forks work out of the box -- `bk` derives the repo identity (`BK_REPO`) from +the checkout's own git origin, so rendered image links automatically follow +the fork. Three caveats: + +1. Set `BK_REPO=/` in the launch command you hand out; the launch + fetches `.scripts/bk` from whatever repo that line names (only after the + clone does bk self-correct from the checkout's origin). +2. The fork must be **public**: the tutorial pane fetches images through + unauthenticated `?raw=true` blob URLs, which 404 on a private fork. +3. A few upstream references are hardcoded and are NOT redirected by forking, + e.g. `.scripts/bk-legacy-download` always fetches the data-stream dataset + from `fhirschmann/bootkon-data`.