#!/bin/bash
set -u
#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://workshop.perforce.com/projects/p4sudo/view/main/LICENSE
#------------------------------------------------------------------------------
# mkblackbelt.sh — P4Sudo command script: Bootstrap Black Belt customer on PPN
#
# Site-defined command script called by p4sudo.sh when an authorized user runs:
# p4 sudo mkblackbelt <CustomerTag> [options]
#
# Performs a 9-step workflow: validates inputs, provisions the customer's P4
# depot and stream on PPN, submits an INFO.md instantiated from a template, and
# adds a row to the Black Belt Customer Index.
#
# IMPORTANT: stdout is the broker protocol channel (inherited from p4sudo.sh).
# Write only the final result message to stdout. All intermediate progress is
# written to a per-invocation log file. (See broker-rewrite-reference/README.md.)
#
# On any step failure, the ephemeral workspace (if created) is left in place for
# debugging. Detailed failure context is in the log.
#
# Called by: p4sudo.sh (type=script dispatch)
# Deployment: /p4/common/site/p4sudo/commands/mkblackbelt.sh
# Config: ${P4SUDO_CFG:-/p4/common/site/config/p4sudo.cfg}
#==============================================================================
# Declarations and Environment
declare ThisScript=${0##*/}
declare Version=1.0.0
declare -i ErrorCount=0
declare -i WarningCount=0
declare Log=
declare SDPInstance=${SDP_INSTANCE:-1}
declare SDPRoot=${SDP_ROOT:-/p4}
declare SDPCommon="$SDPRoot/common"
declare SDPCommonLib="$SDPCommon/lib"
declare LOGS="${LOGS:-$SDPRoot/$SDPInstance/logs}"
# Broker context — injected into the environment by p4sudo.sh.
declare P4Port="${P4SUDO_P4PORT:-}"
declare P4SudoUser="${P4SUDO_USER:-}"
declare RequestingUser="${P4SUDO_REQUESTING_USER:-}"
# Parsed inputs — required.
declare CustomerTag=
declare SalesforceLink=
declare Tier=
declare CommsChannel=
declare ProgramStartDate=
declare ProgramEndDate=
declare LicensedUsers=
declare BackgroundUsers=
declare Contact1Name='' Contact1Email='' Contact1Role=''
declare P4PrimaryName='' P4PrimaryEmail='' P4PrimaryRole=''
# Parsed inputs — optional.
declare IndexNotes=
declare Contact2Name='' Contact2Email='' Contact2Role=''
declare P4SecondaryName='' P4SecondaryEmail='' P4SecondaryRole=''
declare P4Extra1Name='' P4Extra1Email='' P4Extra1Role=''
declare P4Extra2Name='' P4Extra2Email='' P4Extra2Role=''
# Auto-populated at runtime.
declare ActivationDate= # YYYY/MM/DD — today
# Ephemeral workspace — populated after successful creation; left for debugging
# on any error, deleted only on full success (step 8).
declare EphemeralWs=
declare EphemeralRoot=
# P4 paths and known workspace names.
declare BlackBeltWs="p4sudo-svc.blackbelt"
declare TemplateDepotPath="//BlackBelt/main/docs/INFO.md.template"
declare CustomerIndexDepotPath="//BlackBelt/main/Customers/CustomerIndex.md"
declare H1="=============================================================================="
#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
function errmsg () { msg "Error: ${1:-Unknown Error}"; ErrorCount+=1; }
function warnmsg () { msg "Warning: ${1:-Unknown Warning}"; WarningCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "${2:-1}"; }
#------------------------------------------------------------------------------
# Function: log_write
function log_write ()
{
local level="${1:-INFO}"; shift
[[ -n "$Log" && "$Log" != off ]] && \
echo "$(date '+%Y-%m-%dT%H:%M:%S') [$level] $*" >> "$Log"
}
#------------------------------------------------------------------------------
# Function: p4cmd
# Run a p4 command as the service account against the configured P4 port.
function p4cmd ()
{
p4 -p "$P4Port" -u "$P4SudoUser" "$@"
}
#------------------------------------------------------------------------------
# Function: do_subst
# Replace a token with a value in a file (in-place, sed -i).
# Escapes sed replacement metacharacters (backslash, pipe, ampersand) in value.
function do_subst ()
{
local token="$1" value="$2" file="$3"
local esc
esc=$(printf '%s' "$value" | sed 's/[\\|&]/\\&/g')
sed -i "s|${token}|${esc}|g" "$file"
}
#------------------------------------------------------------------------------
# Function: insert_customer_row
# Insert newRow after the last table row in the Active Customers section of file.
# Uses awk to locate the section and find the last line starting with '|'.
function insert_customer_row ()
{
local file="$1" row="$2"
awk -v newRow="$row" '
{
if (/^## Active Customers/) { inSection=1 }
else if (/^## /) { inSection=0 }
if (inSection && /^\|/) { lastTableLine=NR }
lines[NR]=$0
}
END {
for (i=1; i<=NR; i++) {
print lines[i]
if (i==lastTableLine) print newRow
}
}' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
}
#------------------------------------------------------------------------------
# Function: validate_inputs
# Checks all required inputs and format constraints.
# Increments ErrorCount for each failure; caller bails if non-zero.
function validate_inputs ()
{
# Required fields.
[[ -z "$CustomerTag" ]] && errmsg "CustomerTag is required (first positional argument)."
[[ -z "$SalesforceLink" ]] && errmsg "--salesforce-link is required."
[[ -z "$Tier" ]] && errmsg "--tier is required."
[[ -z "$CommsChannel" ]] && errmsg "--comms is required."
[[ -z "$ProgramStartDate" ]] && errmsg "--start-date is required."
[[ -z "$ProgramEndDate" ]] && errmsg "--end-date is required."
[[ -z "$LicensedUsers" ]] && errmsg "--licensed-users is required."
[[ -z "$BackgroundUsers" ]] && errmsg "--background-users is required."
[[ -z "$Contact1Name" ]] && errmsg "--contact1-name is required."
[[ -z "$Contact1Email" ]] && errmsg "--contact1-email is required."
[[ -z "$Contact1Role" ]] && errmsg "--contact1-role is required."
[[ -z "$P4PrimaryName" ]] && errmsg "--p4primary-name is required."
[[ -z "$P4PrimaryEmail" ]] && errmsg "--p4primary-email is required."
[[ -z "$P4PrimaryRole" ]] && errmsg "--p4primary-role is required."
[[ "$ErrorCount" -gt 0 ]] && return 1
# CustomerTag: alphanumeric, any case, must not start with a digit.
if [[ ! "$CustomerTag" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
errmsg "CustomerTag must be alphanumeric (no spaces or special characters) and must not start with a digit: '$CustomerTag'"
fi
# Tier must be one of the three valid values.
case "$Tier" in
Standard|Enterprise|Essential) ;;
*) errmsg "Tier must be Standard, Enterprise, or Essential: '$Tier'";;
esac
# Dates: YYYY/MM/DD.
local datePattern='^[0-9]{4}/[0-9]{2}/[0-9]{2}$'
[[ "$ProgramStartDate" =~ $datePattern ]] || errmsg "--start-date must be YYYY/MM/DD: '$ProgramStartDate'"
[[ "$ProgramEndDate" =~ $datePattern ]] || errmsg "--end-date must be YYYY/MM/DD: '$ProgramEndDate'"
# Integers.
[[ "$LicensedUsers" =~ ^[0-9]+$ ]] || errmsg "--licensed-users must be a non-negative integer: '$LicensedUsers'"
[[ "$BackgroundUsers" =~ ^[0-9]+$ ]] || errmsg "--background-users must be a non-negative integer: '$BackgroundUsers'"
[[ "$ErrorCount" -gt 0 ]] && return 1
return 0
}
#==============================================================================
# Load SDP Library Functions.
if [[ -d "$SDPCommonLib" ]]; then
# shellcheck disable=SC1090 disable=SC1091
source "$SDPCommonLib/logging.lib" ||\
bail "Failed to load bash lib [$SDPCommonLib/logging.lib]. Aborting."
# shellcheck disable=SC1090 disable=SC1091
source "$SDPCommonLib/run.lib" ||\
bail "Failed to load bash lib [$SDPCommonLib/run.lib]. Aborting."
fi
# run.lib declares its own Version; restore this script's version.
declare Version=1.0.0
# Override terminate() from logging.lib: stdout is the broker protocol channel.
# The library's terminate() prints "Log is: ..." to stdout, which would corrupt
# the broker protocol response. Write that information to the log instead.
# shellcheck disable=SC2317
function terminate ()
{
trap - EXIT SIGINT SIGTERM
log_write "INFO" "Exiting. ErrorCount=$ErrorCount Log=$Log"
exit "$ErrorCount"
}
#------------------------------------------------------------------------------
# Function: usage (required function)
function usage ()
{
local style=${1:--h}
local usageErrorMessage=${2:-Unset}
if [[ "$usageErrorMessage" != Unset ]]; then
msg "\n\nUsage Error:\n\n$usageErrorMessage\n\n"
fi
msg "USAGE for $ThisScript v$Version:
$ThisScript <CustomerTag> \\
--salesforce-link=<URL> \\
--tier=<Standard|Enterprise|Essential> \\
--comms=<channel> \\
--start-date=<YYYY/MM/DD> --end-date=<YYYY/MM/DD> \\
--licensed-users=<N> --background-users=<N> \\
--contact1-name=<name> --contact1-email=<email> --contact1-role=<role> \\
--p4primary-name=<name> --p4primary-email=<email> --p4primary-role=<role> \\
[--notes=<text>] \\
[--contact2-name=<name> --contact2-email=<email> --contact2-role=<role>] \\
[--p4secondary-name=<name> --p4secondary-email=<email> --p4secondary-role=<role>] \\
[--p4extra1-name=<name> --p4extra1-email=<email> --p4extra1-role=<role>] \\
[--p4extra2-name=<name> --p4extra2-email=<email> --p4extra2-role=<role>]
or
$ThisScript [-h|-man|-V]
"
if [[ "$style" == -man ]]; then
msg "
DESCRIPTION:
$ThisScript bootstraps a new Black Belt consulting customer on PPN.
Steps performed:
1. Validate inputs
2. Fetch INFO.md template (//$TemplateDepotPath)
3. Provision //<CustomerTag> stream depot (idempotent)
4. Provision //<CustomerTag>/main stream (idempotent)
5. Create ephemeral workspace for //<CustomerTag>/main
6. Instantiate INFO.md from template (token substitution)
7. Submit INFO.md to //<CustomerTag>/main/BlackBelt/INFO.md
8. Delete ephemeral workspace (only on success)
9. Update CustomerIndex.md via the '$BlackBeltWs' workspace
On any step failure, the ephemeral workspace is left in place for debugging.
A detailed per-invocation log is written to \$LOGS.
All P4 operations run as the service account (P4SUDO_USER).
REQUIRED ARGUMENTS:
<CustomerTag>
Short identifier for the customer P4 depot. Alphanumeric, any case, no
spaces, must not start with a digit. Becomes the depot name on PPN.
--salesforce-link=<URL> Salesforce opportunity or account URL.
--tier=<tier> Program tier: Standard, Enterprise, or Essential.
--comms=<channel> Comms channel (e.g. Slack #ext-acme-perforce-bb).
--start-date=<YYYY/MM/DD> Program start date.
--end-date=<YYYY/MM/DD> Program end date.
--licensed-users=<N> Licensed user count (non-negative integer).
--background-users=<N> Background user count (non-negative integer).
--contact1-name=<name> Customer primary contact name.
--contact1-email=<email> Customer primary contact email.
--contact1-role=<role> Customer primary contact role/notes.
--p4primary-name=<name> P4 primary staff contact name.
--p4primary-email=<email> P4 primary staff contact email.
--p4primary-role=<role> P4 primary staff contact role/notes.
OPTIONAL ARGUMENTS:
--notes=<text> Free-text notes for the Customer Index entry.
--contact2-name/email/role Second customer contact.
--p4secondary-name/email/role Second P4 staff contact.
--p4extra1-name/email/role Additional P4 staff contact (1).
--p4extra2-name/email/role Additional P4 staff contact (2).
HELP OPTIONS:
-h Display short help message.
-man Display man-style help message.
-V Display version info for this script.
ENVIRONMENT:
P4SUDO_P4PORT P4 port (set by p4sudo.sh dispatcher).
P4SUDO_USER Service account username (set by p4sudo.sh).
P4SUDO_REQUESTING_USER User who invoked 'p4 sudo mkblackbelt' (informational).
SEE ALSO:
p4sudo.sh(1), doc/use-cases.md, doc/mkblackbelt.ui.yaml
"
fi
exit 2
}
#==============================================================================
# Command Line Processing
set +u
[[ $# -gt 0 && "$1" == -h ]] && usage -h
[[ $# -gt 0 && "$1" == -man ]] && usage -man
[[ $# -gt 0 && "$1" == --help ]] && usage -man
[[ $# -gt 0 && ("$1" == -V || "$1" == --version) ]] && { msg "$ThisScript version $Version"; exit 0; }
# First positional argument is CustomerTag (not a flag).
[[ $# -gt 0 && "${1:0:1}" != "-" ]] && { CustomerTag="$1"; shift; }
while [[ $# -gt 0 ]]; do
case "$1" in
(--salesforce-link=*) SalesforceLink="${1#*=}";;
(--tier=*) Tier="${1#*=}";;
(--comms=*) CommsChannel="${1#*=}";;
(--notes=*) IndexNotes="${1#*=}";;
(--start-date=*) ProgramStartDate="${1#*=}";;
(--end-date=*) ProgramEndDate="${1#*=}";;
(--licensed-users=*) LicensedUsers="${1#*=}";;
(--background-users=*) BackgroundUsers="${1#*=}";;
(--contact1-name=*) Contact1Name="${1#*=}";;
(--contact1-email=*) Contact1Email="${1#*=}";;
(--contact1-role=*) Contact1Role="${1#*=}";;
(--contact2-name=*) Contact2Name="${1#*=}";;
(--contact2-email=*) Contact2Email="${1#*=}";;
(--contact2-role=*) Contact2Role="${1#*=}";;
(--p4primary-name=*) P4PrimaryName="${1#*=}";;
(--p4primary-email=*) P4PrimaryEmail="${1#*=}";;
(--p4primary-role=*) P4PrimaryRole="${1#*=}";;
(--p4secondary-name=*) P4SecondaryName="${1#*=}";;
(--p4secondary-email=*) P4SecondaryEmail="${1#*=}";;
(--p4secondary-role=*) P4SecondaryRole="${1#*=}";;
(--p4extra1-name=*) P4Extra1Name="${1#*=}";;
(--p4extra1-email=*) P4Extra1Email="${1#*=}";;
(--p4extra1-role=*) P4Extra1Role="${1#*=}";;
(--p4extra2-name=*) P4Extra2Name="${1#*=}";;
(--p4extra2-email=*) P4Extra2Email="${1#*=}";;
(--p4extra2-role=*) P4Extra2Role="${1#*=}";;
(*) usage -h "Unknown argument: $1";;
esac
shift
done
set -u
#==============================================================================
# Command Line Verification
[[ -z "$CustomerTag" ]] && usage -h "CustomerTag is required as the first positional argument."
# These env vars are set by p4sudo.sh; guard against direct invocation without them.
[[ -z "$P4Port" ]] && bail "P4SUDO_P4PORT is not set. This script must be called by p4sudo.sh."
[[ -z "$P4SudoUser" ]] && bail "P4SUDO_USER is not set. This script must be called by p4sudo.sh."
#==============================================================================
# Main Program
trap terminate EXIT SIGINT SIGTERM
# Per-invocation log setup.
if [[ -d "${LOGS:-}" ]]; then
Log="$LOGS/${ThisScript%.sh}.${CustomerTag}.$(date +'%Y-%m-%d-%H%M%S').log"
fi
log_write "INFO" "$H1"
log_write "INFO" "Starting $ThisScript v$Version"
log_write "INFO" "CustomerTag=$CustomerTag RequestingUser=${RequestingUser:-<unknown>} P4Port=$P4Port"
# Auto-populate dates and ephemeral workspace name.
ActivationDate=$(date '+%Y/%m/%d')
declare Timestamp
Timestamp=$(date '+%Y%m%d-%H%M%S')
EphemeralWs="p4sudo-mkblackbelt-${CustomerTag}-${Timestamp}"
EphemeralRoot="/tmp/${EphemeralWs}"
log_write "INFO" "ActivationDate=$ActivationDate EphemeralWs=$EphemeralWs"
#------------------------------------------------------------------------------
# Step 1 — Validate inputs.
log_write "INFO" "Step 1: Validating inputs."
validate_inputs || bail "Input validation failed. Correct the errors above and retry."
log_write "INFO" "Step 1: Inputs validated OK."
#------------------------------------------------------------------------------
# Step 2 — Fetch INFO.md template from PPN.
declare templateFile
templateFile=$(mktemp "/tmp/p4sudo-mkblackbelt-template.XXXXXX") || \
bail "Step 2: Could not create temp file for template."
log_write "INFO" "Step 2: Fetching template $TemplateDepotPath -> $templateFile"
declare p4out
p4out=$(p4cmd print -q -o "$templateFile" "$TemplateDepotPath" 2>&1)
declare -i p4rc=$?
if (( p4rc != 0 )); then
rm -f "$templateFile"
bail "Step 2: Failed to fetch template from $TemplateDepotPath: $p4out"
fi
log_write "INFO" "Step 2: Template fetched OK."
#------------------------------------------------------------------------------
# Step 3 — Provision //<CustomerTag> stream depot (idempotent).
log_write "INFO" "Step 3: Checking depot //$CustomerTag."
declare existingDepot
existingDepot=$(p4cmd depots -e "$CustomerTag" 2>&1)
if [[ -n "$existingDepot" ]]; then
log_write "INFO" "Step 3: Depot //$CustomerTag already exists; skipping creation."
else
log_write "INFO" "Step 3: Creating depot //$CustomerTag."
p4out=$(printf 'Depot: %s\nOwner: %s\nDescription:\n\tBlack Belt consulting depot for %s.\nType: stream\nMap: %s/...\n' \
"$CustomerTag" "$P4SudoUser" "$CustomerTag" "$CustomerTag" | \
p4cmd depot -i 2>&1)
p4rc=$?
log_write "INFO" "Step 3: p4 depot -i output: $p4out"
if (( p4rc != 0 )); then
rm -f "$templateFile"
bail "Step 3: Failed to create depot //$CustomerTag: $p4out"
fi
log_write "INFO" "Step 3: Depot //$CustomerTag created OK."
fi
#------------------------------------------------------------------------------
# Step 4 — Provision //<CustomerTag>/main stream (idempotent).
log_write "INFO" "Step 4: Checking stream //$CustomerTag/main."
declare existingStream
existingStream=$(p4cmd streams -e "//${CustomerTag}/main" 2>&1)
if [[ -n "$existingStream" ]]; then
log_write "INFO" "Step 4: Stream //$CustomerTag/main already exists; skipping creation."
else
log_write "INFO" "Step 4: Creating stream //$CustomerTag/main."
p4out=$(printf 'Stream: //%s/main\nOwner: %s\nName: main\nParent: none\nType: mainline\nDescription:\n\tBlack Belt main stream for %s.\nPaths:\n\tshare ...\n' \
"$CustomerTag" "$P4SudoUser" "$CustomerTag" | \
p4cmd stream -i 2>&1)
p4rc=$?
log_write "INFO" "Step 4: p4 stream -i output: $p4out"
if (( p4rc != 0 )); then
rm -f "$templateFile"
bail "Step 4: Failed to create stream //$CustomerTag/main: $p4out"
fi
log_write "INFO" "Step 4: Stream //$CustomerTag/main created OK."
fi
#------------------------------------------------------------------------------
# Step 5 — Create ephemeral workspace.
log_write "INFO" "Step 5: Creating ephemeral workspace $EphemeralWs rooted at $EphemeralRoot."
if ! mkdir -p "$EphemeralRoot"; then
rm -f "$templateFile"
bail "Step 5: Could not create workspace root directory: $EphemeralRoot"
fi
p4out=$(printf 'Client: %s\nOwner: %s\nDescription:\n\tEphemeral P4Sudo workspace for mkblackbelt %s.\nRoot: %s\nOptions: noallwrite noclobber nocompress unlocked nomodtime normdir\nSubmitOptions: submitunchanged\nLineEnd: local\nStream: //%s/main\n' \
"$EphemeralWs" "$P4SudoUser" "$CustomerTag" "$EphemeralRoot" "$CustomerTag" | \
p4cmd client -i 2>&1)
p4rc=$?
log_write "INFO" "Step 5: p4 client -i output: $p4out"
if (( p4rc != 0 )); then
rm -f "$templateFile"
rm -rf "$EphemeralRoot"
bail "Step 5: Failed to create ephemeral workspace $EphemeralWs: $p4out"
fi
log_write "INFO" "Ephemeral workspace is live." # left for debugging if subsequent steps fail
log_write "INFO" "Step 5: Ephemeral workspace $EphemeralWs created OK."
#------------------------------------------------------------------------------
# Step 6 — Instantiate INFO.md from template (token substitution).
declare infoMdDir="${EphemeralRoot}/BlackBelt"
declare infoMdFile="${infoMdDir}/INFO.md"
if ! mkdir -p "$infoMdDir"; then
rm -f "$templateFile"
bail "Step 6: Could not create directory: $infoMdDir"
fi
if ! cp "$templateFile" "$infoMdFile"; then
rm -f "$templateFile"
bail "Step 6: Could not copy template to $infoMdFile"
fi
rm -f "$templateFile"
log_write "INFO" "Step 6: Performing token substitution on $infoMdFile."
do_subst "__EDITME_CUSTOMER_TAG__" "$CustomerTag" "$infoMdFile"
do_subst "__EDITME_SALESFORCE_LINK__" "$SalesforceLink" "$infoMdFile"
do_subst "__EDITME_YYYY_MM_DD__" "$ActivationDate" "$infoMdFile"
do_subst "__EDITME_LICENSED_USERS__" "$LicensedUsers" "$infoMdFile"
do_subst "__EDITME_BACKGROUND_USERS__" "$BackgroundUsers" "$infoMdFile"
do_subst "__EDITME_PROGRAM_START_DATE__" "$ProgramStartDate" "$infoMdFile"
do_subst "__EDITME_PROGRAM_END_DATE__" "$ProgramEndDate" "$infoMdFile"
do_subst "__EDITME_CONTACT_1_NAME__" "$Contact1Name" "$infoMdFile"
do_subst "__EDITME_CONTACT_1_EMAIL__" "$Contact1Email" "$infoMdFile"
do_subst "__EDITME_CONTACT_1_ROLE_AND_NOTES__" "$Contact1Role" "$infoMdFile"
do_subst "__EDITME_CONTACT_2_NAME__" "$Contact2Name" "$infoMdFile"
do_subst "__EDITME_CONTACT_2_EMAIL__" "$Contact2Email" "$infoMdFile"
do_subst "__EDITME_CONTACT_2_ROLE_AND_NOTES__" "$Contact2Role" "$infoMdFile"
do_subst "__EDITME_P4_PRIMARY_NAME__" "$P4PrimaryName" "$infoMdFile"
do_subst "__EDITME_P4_PRIMARY_EMAIL__" "$P4PrimaryEmail" "$infoMdFile"
do_subst "__EDITME_P4_PRIMARY_ROLE_AND_NOTES__" "$P4PrimaryRole" "$infoMdFile"
do_subst "__EDITME_P4_SECONDARY_NAME__" "$P4SecondaryName" "$infoMdFile"
do_subst "__EDITME_P4_SECONDARY_EMAIL__" "$P4SecondaryEmail" "$infoMdFile"
do_subst "__EDITME_P4_SECONDARY_ROLE_AND_NOTES__" "$P4SecondaryRole" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_1_NAME__" "$P4Extra1Name" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_1_EMAIL__" "$P4Extra1Email" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_1_ROLE_AND_NOTES__" "$P4Extra1Role" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_2_NAME__" "$P4Extra2Name" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_2_EMAIL__" "$P4Extra2Email" "$infoMdFile"
do_subst "__EDITME_P4_EXTRA_2_ROLE_AND_NOTES__" "$P4Extra2Role" "$infoMdFile"
log_write "INFO" "Step 6: Token substitution complete."
#------------------------------------------------------------------------------
# Step 7 — Submit INFO.md to //<CustomerTag>/main/BlackBelt/INFO.md.
log_write "INFO" "Step 7: Adding and submitting $infoMdFile."
p4out=$(p4cmd -c "$EphemeralWs" add "$infoMdFile" 2>&1)
p4rc=$?
log_write "INFO" "Step 7: p4 add: $p4out"
if (( p4rc != 0 )); then
bail "Step 7: 'p4 add' failed: $p4out"
fi
p4out=$(p4cmd -c "$EphemeralWs" submit \
-d "Bootstrap Black Belt INFO.md for $CustomerTag" 2>&1)
p4rc=$?
log_write "INFO" "Step 7: p4 submit: $p4out"
if (( p4rc != 0 )); then
bail "Step 7: 'p4 submit' failed: $p4out"
fi
log_write "INFO" "Step 7: INFO.md submitted OK."
#------------------------------------------------------------------------------
# Step 8 — Delete ephemeral workspace (only on success).
log_write "INFO" "Step 8: Deleting ephemeral workspace $EphemeralWs."
p4out=$(p4cmd client -d "$EphemeralWs" 2>&1)
p4rc=$?
log_write "INFO" "Step 8: p4 client -d: $p4out"
if (( p4rc != 0 )); then
warnmsg "Step 8: Could not delete ephemeral workspace $EphemeralWs: $p4out"
log_write "WARN" "Step 8: Manual cleanup may be required for workspace $EphemeralWs and $EphemeralRoot"
else
rm -rf "$EphemeralRoot"
log_write "INFO" "Step 8: Ephemeral workspace and temp directory deleted."
fi
#------------------------------------------------------------------------------
# Step 9 — Update CustomerIndex.md via the persistent BlackBelt workspace.
log_write "INFO" "Step 9: Updating CustomerIndex.md."
declare swarmUrl="https://ppn.perforce.com/files/${CustomerTag}/main/BlackBelt/INFO.md"
declare newRow="| [${CustomerTag}](${swarmUrl}) | Active | ${ProgramStartDate} | ${ProgramEndDate} | ${Tier} | ${CommsChannel} | ${IndexNotes} |"
# Sync to latest revision.
p4out=$(p4cmd -c "$BlackBeltWs" sync "$CustomerIndexDepotPath" 2>&1)
log_write "INFO" "Step 9: sync: $p4out"
# Open for edit.
p4out=$(p4cmd -c "$BlackBeltWs" edit "$CustomerIndexDepotPath" 2>&1)
p4rc=$?
log_write "INFO" "Step 9: edit: $p4out"
if (( p4rc != 0 )); then
bail "Step 9: Could not open CustomerIndex.md for edit: $p4out"
fi
# Determine local path.
declare localIndexFile
localIndexFile=$(p4cmd -c "$BlackBeltWs" where "$CustomerIndexDepotPath" 2>/dev/null | awk '{print $3}')
if [[ -z "$localIndexFile" || ! -f "$localIndexFile" ]]; then
bail "Step 9: Could not determine local path for $CustomerIndexDepotPath"
fi
insert_customer_row "$localIndexFile" "$newRow" || \
bail "Step 9: Failed to insert new row into CustomerIndex.md."
# Submit — retry once on conflict.
p4out=$(p4cmd -c "$BlackBeltWs" submit \
-d "Add $CustomerTag to Black Belt Customer Index" 2>&1)
p4rc=$?
log_write "INFO" "Step 9: submit: $p4out"
if (( p4rc != 0 )); then
if echo "$p4out" | grep -q "out of date"; then
log_write "WARN" "Step 9: Submit conflict on CustomerIndex.md; reverting and retrying."
p4out=$(p4cmd -c "$BlackBeltWs" revert "$CustomerIndexDepotPath" 2>&1)
log_write "INFO" "Step 9: revert: $p4out"
p4out=$(p4cmd -c "$BlackBeltWs" sync "$CustomerIndexDepotPath" 2>&1)
log_write "INFO" "Step 9: retry sync: $p4out"
p4out=$(p4cmd -c "$BlackBeltWs" edit "$CustomerIndexDepotPath" 2>&1)
log_write "INFO" "Step 9: retry edit: $p4out"
insert_customer_row "$localIndexFile" "$newRow" || \
bail "Step 9 (retry): Failed to insert new row into CustomerIndex.md."
p4out=$(p4cmd -c "$BlackBeltWs" submit \
-d "Add $CustomerTag to Black Belt Customer Index" 2>&1)
p4rc=$?
log_write "INFO" "Step 9: retry submit: $p4out"
if (( p4rc != 0 )); then
bail "Step 9: CustomerIndex.md submit failed after retry: $p4out"
fi
log_write "INFO" "Step 9: CustomerIndex.md updated (after retry)."
else
bail "Step 9: CustomerIndex.md submit failed: $p4out"
fi
else
log_write "INFO" "Step 9: CustomerIndex.md updated OK."
fi
#------------------------------------------------------------------------------
# Done.
log_write "INFO" "$H1"
log_write "INFO" "mkblackbelt completed successfully for $CustomerTag."
msg "mkblackbelt: SUCCESS"
msg ""
msg "Customer: $CustomerTag"
msg "Depot: //$CustomerTag"
msg "Stream: //$CustomerTag/main"
msg "INFO.md: //$CustomerTag/main/BlackBelt/INFO.md"
msg "Index: $swarmUrl"
msg "Log: ${Log:-<logging unavailable>}"
exit "$ErrorCount"
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 32599 | bot_Claude_Anthropic |
Add bin/mkblackbelt.sh - P4Sudo site command to bootstrap Black Belt customer on PPN. Implements 9-step workflow from doc/use-cases.md. ShellCheck 0.10.0 clean. #review-32600 @robert_cowham @tom_tyler |