Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
987db96
bk: fail fast on invalid BK_STREAM, persist env for new terminals
fhirschmann Jul 5, 2026
68f7e8f
bk: consolidate per-participant env into vars.local.sh
fhirschmann Jul 8, 2026
b6e1a02
docs: use `. bk` reload in agenticdata, document branch testing
fhirschmann Jul 8, 2026
bfc815b
bk: describe every var in vars.local.sh; set MY_NAME at launch; two-l…
fhirschmann Jul 8, 2026
902d8ed
agenticdata: use PROJECT_ID as the single project var; stop pinning G…
fhirschmann Jul 8, 2026
d17b072
bk: derive PROJECT_ID live from Cloud Shell; cache PROJECT_NUMBER
fhirschmann Jul 9, 2026
4c3f0a5
bk: fail fast when no Cloud Shell project is selected
fhirschmann Jul 9, 2026
7fef4b0
bk: optional per-stream init hook (bk-init); generate secrets before …
fhirschmann Jul 9, 2026
34a74d0
readme: make a failed bootkon download loud instead of silent
fhirschmann Jul 9, 2026
1ac3ec4
bk-start: fail with one clear message when the checkout is missing
fhirschmann Jul 9, 2026
a28ea9a
bk-tutorial: guard the reload memory and stop depending on the caller…
fhirschmann Jul 9, 2026
167017f
bk-render-jinja2: render unset scalars as empty and warn on empty sec…
fhirschmann Jul 9, 2026
036f95a
bk-set-var: harden the comment flag, appends, and file creation
fhirschmann Jul 9, 2026
fbf5b01
bk-remove-block: resolve symlinks before the atomic replace
fhirschmann Jul 9, 2026
dec05e3
bk: drop BK_INITIALIZED -- a non-empty PROJECT_NUMBER is the ready si…
fhirschmann Jul 9, 2026
69be9cc
bk: replace the dead apt/git install block with a hard error
fhirschmann Jul 9, 2026
a57b1bf
bk: one clear instruction per failure, for people who never used a te…
fhirschmann Jul 9, 2026
0725ffd
bk: probe repo/branch/stream before cloning; never trust a broken che…
fhirschmann Jul 9, 2026
501fee5
bk: validate vars.local.sh before sourcing it -- everywhere
fhirschmann Jul 9, 2026
caf51ec
bk: sync a stale GOOGLE_CLOUD_PROJECT with the validated project
fhirschmann Jul 9, 2026
fce580b
bk: stop persisting the three derived repo variables
fhirschmann Jul 9, 2026
0431617
bk: stop teleporting the caller's shell to the repo root
fhirschmann Jul 9, 2026
d66cfcb
bk: quiet output -- narration behind BK_DEBUG, one summary line, 1-li…
fhirschmann Jul 9, 2026
aa7c224
bk: interactive first-run onboarding -- the robot greets and asks the…
fhirschmann Jul 9, 2026
720c34a
bk: wrap the whole body in _bk_main -- a truncated download runs nothing
fhirschmann Jul 9, 2026
5a244b0
agenticdata: bk-bootstrap fails loudly instead of succeeding silently
fhirschmann Jul 9, 2026
42010a6
tutorial: happy path = zero commands -- verify first, commands in the…
fhirschmann Jul 9, 2026
f24b245
docs: document the fork caveats
fhirschmann Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
514 changes: 388 additions & 126 deletions .scripts/bk

Large diffs are not rendered by default.

28 changes: 20 additions & 8 deletions .scripts/bk-deactivate
Original file line number Diff line number Diff line change
@@ -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
"$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 PROJECT_NUMBER' or start a new shell)."
33 changes: 21 additions & 12 deletions .scripts/bk-info
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,32 @@ 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_TUTORIAL: ${GREEN}$BK_TUTORIAL${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_INIT_SCRIPT: ${GREEN}$BK_INIT_SCRIPT${NC}"
echo -e "BK_INITIALIZED: ${GREEN}$BK_INITIALIZED${NC}"
echo -e "BK_STREAM: ${GREEN}$BK_STREAM${NC}"
echo -e "BK_BRANCH: ${GREEN}$BK_BRANCH${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
echo -e "data: ${RED}does not exist${NC}"
else
echo -e "data: ${GREEN}exists${NC}"
du -sh 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 config --get remote.origin.url)${NC}"
echo -e "git origin: ${GREEN}$(git -C "${BK_DIR}" config --get remote.origin.url 2>/dev/null)${NC}"
65 changes: 65 additions & 0 deletions .scripts/bk-remove-block
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/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

# 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 <<<'

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"
21 changes: 16 additions & 5 deletions .scripts/bk-render-jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
140 changes: 140 additions & 0 deletions .scripts/bk-set-var
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/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] [--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
# 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
comment=""
while [ $# -gt 0 ]; do
case "$1" in
--if-empty) if_empty=1; shift ;;
--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-}"

# 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

# 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}="

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"
# 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
mv "$tmp" "$file"
12 changes: 8 additions & 4 deletions .scripts/bk-start
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#!/bin/sh

cd $BK_DIR

if [ -z "$BK_INITIALIZED" ]; then
if [ -z "$PROJECT_NUMBER" ]; then
echo "Bootkon has not been initialized."
echo "Please execute: "
echo " . bk (including the dot)"
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
43 changes: 33 additions & 10 deletions .scripts/bk-tutorial
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,44 @@
#
# Usage: bk-tutorial <path/to/tutorial.md>

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"
Loading