#!/bin/sh # Latchpoint — Perforce change-commit trigger (POSIX sh; Linux/macOS Helix Core servers) # # Install: # 1. Copy to the Perforce server, e.g. /p4/latchpoint-trigger.sh ; chmod +x # 2. Set for the p4d service account: # LATCHPOINT_URL= # LATCHPOINT_SECRET= # LATCHPOINT_SWARM_URL= # 3. p4 triggers # latchpoint change-commit //... "/p4/latchpoint-trigger.sh %change% %user%" # # Design rule: this script MUST NOT be able to block a submit. Every failure path # exits 0. A version-control server is not a place to be clever. CHANGE="$1" USER_ARG="$2" P4BIN="${P4_EXE:-p4}" DEPOT_NAME="${LATCHPOINT_DEPOT:-depot}" [ -z "$CHANGE" ] && exit 0 [ -z "$LATCHPOINT_URL" ] && { echo "Latchpoint: LATCHPOINT_URL not set; skipping."; exit 0; } [ -z "$LATCHPOINT_SECRET" ] && { echo "Latchpoint: LATCHPOINT_SECRET not set; skipping."; exit 0; } DESC_RAW=$("$P4BIN" -ztag describe -s "$CHANGE" 2>/dev/null) || { echo "Latchpoint: p4 describe failed; skipping."; exit 0; } field() { printf '%s\n' "$DESC_RAW" | sed -n "s/^\.\.\. $1 \(.*\)$/\1/p" | head -1; } DESC=$(field desc) P4USER=$(field user) EPOCH=$(field time) FILE_COUNT=$(printf '%s\n' "$DESC_RAW" | grep -c '^\.\.\. depotFile') [ -z "$DESC" ] && { echo "Latchpoint: empty description; skipping."; exit 0; } # Cheap local guard: no Jira key, no network call. printf '%s' "$DESC" | grep -Eq '\b[A-Z][A-Z0-9]+-[0-9]+\b' || { echo "Latchpoint: no Jira issue key in description; skipping."; exit 0; } [ -n "$USER_ARG" ] && P4USER="$USER_ARG" # ISO-8601 from epoch; fall back to current time if date(1) lacks -d/-r. ISO=$(date -u -d "@$EPOCH" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u -r "$EPOCH" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u +%Y-%m-%dT%H:%M:%SZ) # JSON-escape: backslash, quote, then control chars -> \n esc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | awk 'BEGIN{ORS=""} {print (NR>1 ? "\\n" : "") $0}' } # With a Swarm base URL, changelist links in Jira resolve to the real Swarm page # instead of our docs. Optional — the payload is valid either way. SERVER_JSON='' if [ -n "$LATCHPOINT_SWARM_URL" ]; then SERVER_JSON=$(printf ',"serverUrl":"%s"' "$(esc "${LATCHPOINT_SWARM_URL%/}")") fi PAYLOAD=$(printf '{"depot":"%s"%s,"changelists":[{"change":"%s","description":"%s","user":"%s","time":"%s","fileCount":%s}]}' \ "$(esc "$DEPOT_NAME")" "$SERVER_JSON" "$(esc "$CHANGE")" "$(esc "$DESC")" "$(esc "$P4USER")" "$ISO" "${FILE_COUNT:-0}") # curl exits 0 on HTTP errors, so check the status code explicitly — otherwise a # rejected request would be reported to the user as a success. TMP="${TMPDIR:-/tmp}/latchpoint.$$" CODE=$(curl -sS -m 20 -o "$TMP" -w '%{http_code}' -X POST "$LATCHPOINT_URL" \ -H 'Content-Type: application/json' \ -H "X-Latchpoint-Secret: $LATCHPOINT_SECRET" \ --data "$PAYLOAD" 2>/dev/null) || CODE=000 if [ "$CODE" = "200" ]; then echo "Latchpoint: published changelist $CHANGE." else echo "Latchpoint: publish failed (HTTP $CODE: $(head -c 200 "$TMP" 2>/dev/null)); submit unaffected." fi rm -f "$TMP" exit 0