@salesforce/afv-skills 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +239 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +21 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +90 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_excerpt.md +31 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_to_config.json +50 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +76 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/sop_parsing_patterns.md +157 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/source_format_handling.md +230 -0
- package/skills/dx-apexguru-scan/SKILL.md +403 -0
- package/skills/dx-apexguru-scan/examples/README.md +54 -0
- package/skills/dx-apexguru-scan/examples/sample-decoded-summary.json +176 -0
- package/skills/dx-apexguru-scan/examples/sample-full-no-runtime-response.json +26 -0
- package/skills/dx-apexguru-scan/examples/sample-succeeded-response.json +15 -0
- package/skills/dx-apexguru-scan/references/api-reference.md +81 -0
- package/skills/dx-apexguru-scan/references/authentication.md +134 -0
- package/skills/dx-apexguru-scan/references/error-handling.md +56 -0
- package/skills/dx-apexguru-scan/references/violation-catalog.md +28 -0
- package/skills/dx-apexguru-scan/scripts/build-zip.sh +87 -0
- package/skills/dx-apexguru-scan/scripts/decode-report.js +389 -0
- package/skills/dx-apexguru-scan/scripts/resolve-token.sh +151 -0
- package/skills/dx-apexguru-scan/scripts/run-scan.sh +153 -0
- package/skills/dx-apexguru-scan/scripts/scan.sh +96 -0
- package/skills/dx-apexguru-scan/scripts/validate-token.js +121 -0
- package/skills/dx-devops-pipeline-manage/SKILL.md +263 -0
- package/skills/dx-devops-pipeline-manage/examples/common-workflows.md +177 -0
- package/skills/dx-devops-pipeline-manage/references/cli-commands.md +298 -0
- package/skills/dx-devops-pipeline-manage/references/parsing-patterns.md +134 -0
- package/skills/dx-devops-pipeline-manage/scripts/check-activation-ready.sh +34 -0
- package/skills/dx-devops-pipeline-manage/scripts/validate-org-type.sh +17 -0
- package/skills/dx-devops-pipeline-manage/scripts/verify-operation.sh +82 -0
- package/skills/dx-devops-promote/SKILL.md +214 -0
- package/skills/dx-devops-promote/examples/promotion-workflows.md +212 -0
- package/skills/dx-devops-promote/references/cli-commands.md +303 -0
- package/skills/experience-lwc-base-components-integrate/SKILL.md +176 -0
- package/skills/experience-lwc-base-components-integrate/references/lbc-expert-guidance.md +127 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-component-index.md +179 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-components.md +5429 -0
- package/skills/experience-lwc-base-components-integrate/scripts/extract-component-docs.sh +61 -0
- package/skills/experience-lwc-rtl-validate/SKILL.md +149 -0
- package/skills/experience-lwc-rtl-validate/references/rtl-expert.md +892 -0
- package/skills/experience-lwc-rtl-validate/scripts/scan-rtl-css.sh +206 -0
- package/skills/experience-lwc-typescript-migrate/SKILL.md +207 -0
- package/skills/experience-lwc-typescript-migrate/assets/dts-template.ts +15 -0
- package/skills/experience-lwc-typescript-migrate/assets/type-patterns.ts +44 -0
- package/skills/experience-lwc-typescript-migrate/scripts/find-consumers.sh +128 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +323 -0
- package/skills/experience-ui-bundle-localize/references/gotchas.md +249 -0
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +169 -0
- package/skills/experience-ui-bundle-localize/references/interpolation.md +311 -0
- package/skills/experience-ui-bundle-localize/references/label-xml.md +282 -0
- package/skills/experience-ui-bundle-localize/references/verifying.md +219 -0
- package/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh +195 -0
- package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +100 -0
- package/skills/experience-ui-bundle-localize/scripts/check-org-api-version.sh +40 -0
- package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +57 -0
- package/skills/platform-custom-lightning-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md +1 -1
- package/skills/platform-mcp-tool-widget-coordinate/SKILL.md +250 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/action-name-source-prompt.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/apex-invocable-source-prompt.md +90 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/nested-object-source-prompt.md +191 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/pasted-tool-output-prompt.md +85 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/build-plan-format.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/mcp-tool-output-discovery.md +184 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/two-clt-modeling.md +128 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/validation-gates.md +181 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Submit a zip to the ApexGuru SFAP Scan API and poll to completion.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash run-scan.sh <zip-path> <raw-result-out.json> [--org <alias>] [--fast]
|
|
6
|
+
# [--max-polls N] [--interval SEC]
|
|
7
|
+
# The endpoint (prod/stage/dev host) is chosen by resolve-token.sh from the
|
|
8
|
+
# token's environment; customers on a prod org always hit api.salesforce.com.
|
|
9
|
+
#
|
|
10
|
+
# Steps:
|
|
11
|
+
# 1. resolve-token.sh → baseUrl + JWT
|
|
12
|
+
# 2. POST {baseUrl}/scan (multipart file upload) → 202 {scanId, status}
|
|
13
|
+
# 3. GET {baseUrl}/scan/{scanId} every ~15s until SUCCEEDED/FAILED
|
|
14
|
+
# 4. Write the final raw SUCCEEDED body verbatim to <raw-result-out.json>
|
|
15
|
+
# (report stays base64-encoded here; decode-report.js handles it).
|
|
16
|
+
#
|
|
17
|
+
# Progress lines go to STDERR so stdout stays a single clean JSON summary:
|
|
18
|
+
# {"scanId":"...","status":"SUCCEEDED","analysisMode":"static|full",
|
|
19
|
+
# "violationCount":N,"filesScanned":N,"rawResult":"<path>"}
|
|
20
|
+
# On failure: {"error":"...","status":"...","httpStatus":NNN,"hint":"..."} exit 1.
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
|
|
25
|
+
ZIP_PATH="${1:-}"
|
|
26
|
+
RAW_OUT="${2:-}"
|
|
27
|
+
shift 2 2>/dev/null || true
|
|
28
|
+
|
|
29
|
+
FAST=""
|
|
30
|
+
ORG_ALIAS=""
|
|
31
|
+
MAX_POLLS=40 # 40 * 15s = 10 min ceiling
|
|
32
|
+
INTERVAL=15
|
|
33
|
+
while [ $# -gt 0 ]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--org) ORG_ALIAS="${2:-}"; shift 2 ;;
|
|
36
|
+
--fast) FAST="true"; shift ;;
|
|
37
|
+
--max-polls) MAX_POLLS="${2:-40}"; shift 2 ;;
|
|
38
|
+
--interval) INTERVAL="${2:-15}"; shift 2 ;;
|
|
39
|
+
*) shift ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
err() { # msg httpStatus status hint
|
|
44
|
+
jq -cn --arg m "$1" --argjson h "${2:-0}" --arg s "${3:-}" --arg hint "${4:-}" \
|
|
45
|
+
'{error:$m, httpStatus:$h, status:$s, hint:$hint}'
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[ -n "$ZIP_PATH" ] && [ -f "$ZIP_PATH" ] || err "zip not found: $ZIP_PATH" 0 "" "run build-zip.sh first"
|
|
50
|
+
[ -n "$RAW_OUT" ] || err "missing raw output path" 0 "" "usage: run-scan.sh <zip> <raw-out.json>"
|
|
51
|
+
|
|
52
|
+
# --- 1. token ---
|
|
53
|
+
RESOLVE_ARGS=()
|
|
54
|
+
[ -n "$ORG_ALIAS" ] && RESOLVE_ARGS+=(--org "$ORG_ALIAS")
|
|
55
|
+
CREDS="$(bash "$SCRIPT_DIR/resolve-token.sh" ${RESOLVE_ARGS[@]+"${RESOLVE_ARGS[@]}"})" || {
|
|
56
|
+
echo "$CREDS" >&2; exit 1;
|
|
57
|
+
}
|
|
58
|
+
BASE_URL="$(echo "$CREDS" | jq -r '.baseUrl')"
|
|
59
|
+
# The JWT is a secret: resolve-token.sh returns a 0600 file PATH, never the token
|
|
60
|
+
# itself. Read it into memory and delete the file right away so it never lingers.
|
|
61
|
+
TOKEN_FILE="$(echo "$CREDS" | jq -r '.tokenFile')"
|
|
62
|
+
TOKEN=""
|
|
63
|
+
if [ -n "$TOKEN_FILE" ] && [ -f "$TOKEN_FILE" ]; then
|
|
64
|
+
TOKEN="$(cat "$TOKEN_FILE")"
|
|
65
|
+
rm -f "$TOKEN_FILE"
|
|
66
|
+
fi
|
|
67
|
+
[ -n "$TOKEN" ] || err "could not read resolved token" 0 "" "re-run; if it persists, resolve the token manually via APEXGURU_SFAP_TOKEN"
|
|
68
|
+
|
|
69
|
+
hint_for_status() { # maps HTTP status → actionable hint (see references/error-handling.md)
|
|
70
|
+
case "$1" in
|
|
71
|
+
401) echo "Token bad or expired. Re-authenticate the Salesforce org (or supply a fresh sfap_api JWT) and retry." ;;
|
|
72
|
+
403) echo "Scan owned by a different org. The token's tnk-claim org must match the scan owner." ;;
|
|
73
|
+
404) echo "Unknown scanId, or the scan was archived (~30-day GC). Re-submit." ;;
|
|
74
|
+
400) echo "Malformed zip, no Apex inside, or over size limit (200MB compressed / 1GB decompressed)." ;;
|
|
75
|
+
*) echo "See references/error-handling.md." ;;
|
|
76
|
+
esac
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Auto-enable fast mode if >10 Apex files (unless explicitly disabled)
|
|
80
|
+
if [ -z "$FAST" ]; then
|
|
81
|
+
# grep -c exits non-zero when there are no matches; capture the count without
|
|
82
|
+
# letting that non-zero status trip `set -e`, and default an empty result to 0.
|
|
83
|
+
FILE_COUNT=$(unzip -l "$ZIP_PATH" 2>/dev/null | grep -c '\.cls$' || true)
|
|
84
|
+
FILE_COUNT=${FILE_COUNT:-0}
|
|
85
|
+
if [ "$FILE_COUNT" -gt 10 ]; then
|
|
86
|
+
FAST="true"
|
|
87
|
+
echo "Auto-enabling fast mode (detected $FILE_COUNT Apex files)" >&2
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# --- 2. submit ---
|
|
92
|
+
echo "Submitting scan to $BASE_URL/scan ..." >&2
|
|
93
|
+
SUBMIT_BODY="$(mktemp)"
|
|
94
|
+
FAST_ARG=()
|
|
95
|
+
[ -n "$FAST" ] && FAST_ARG=(-F "fastMode=true")
|
|
96
|
+
|
|
97
|
+
HTTP=$(curl -sS -o "$SUBMIT_BODY" -w '%{http_code}' -X POST "$BASE_URL/scan" \
|
|
98
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
99
|
+
-H "x-apexguru-client: ApexGuru-Skill" \
|
|
100
|
+
-F "file=@${ZIP_PATH};type=application/zip" \
|
|
101
|
+
${FAST_ARG[@]+"${FAST_ARG[@]}"}) || err "network error submitting scan" 0 "" "check connectivity to the API host"
|
|
102
|
+
|
|
103
|
+
if [ "$HTTP" != "202" ] && [ "$HTTP" != "200" ]; then
|
|
104
|
+
MSG="$(jq -r '.message // .error // empty' "$SUBMIT_BODY" 2>/dev/null || true)"
|
|
105
|
+
rm -f "$SUBMIT_BODY"
|
|
106
|
+
err "submit failed: ${MSG:-HTTP $HTTP}" "$HTTP" "" "$(hint_for_status "$HTTP")"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
SCAN_ID="$(jq -r '.scanId // empty' "$SUBMIT_BODY")"
|
|
110
|
+
rm -f "$SUBMIT_BODY"
|
|
111
|
+
[ -n "$SCAN_ID" ] || err "no scanId in submit response" "$HTTP" "" "unexpected API response shape"
|
|
112
|
+
echo "Accepted. scanId=$SCAN_ID (polling every ${INTERVAL}s, up to $MAX_POLLS times)..." >&2
|
|
113
|
+
|
|
114
|
+
# --- 3. poll ---
|
|
115
|
+
POLL_BODY="$(mktemp)"
|
|
116
|
+
STATUS="QUEUED"
|
|
117
|
+
i=0
|
|
118
|
+
while [ "$i" -lt "$MAX_POLLS" ]; do
|
|
119
|
+
i=$((i + 1))
|
|
120
|
+
sleep "$INTERVAL"
|
|
121
|
+
HTTP=$(curl -sS -o "$POLL_BODY" -w '%{http_code}' -X GET "$BASE_URL/scan/$SCAN_ID" \
|
|
122
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
123
|
+
-H "x-apexguru-client: ApexGuru-Skill") || { echo "poll $i: network hiccup, retrying" >&2; continue; }
|
|
124
|
+
|
|
125
|
+
if [ "$HTTP" != "200" ]; then
|
|
126
|
+
MSG="$(jq -r '.message // .error // empty' "$POLL_BODY" 2>/dev/null || true)"
|
|
127
|
+
rm -f "$POLL_BODY"
|
|
128
|
+
err "poll failed: ${MSG:-HTTP $HTTP}" "$HTTP" "$STATUS" "$(hint_for_status "$HTTP")"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
STATUS="$(jq -r '.status // "UNKNOWN"' "$POLL_BODY")"
|
|
132
|
+
echo "poll $i/$MAX_POLLS: $STATUS" >&2
|
|
133
|
+
|
|
134
|
+
case "$STATUS" in
|
|
135
|
+
SUCCEEDED)
|
|
136
|
+
cp "$POLL_BODY" "$RAW_OUT"
|
|
137
|
+
rm -f "$POLL_BODY"
|
|
138
|
+
jq -c '{status, violationCount, filesScanned, rawResult: "'"$RAW_OUT"'"}' "$RAW_OUT"
|
|
139
|
+
exit 0
|
|
140
|
+
;;
|
|
141
|
+
FAILED)
|
|
142
|
+
MSG="$(jq -r '.message // "scan reported FAILED"' "$POLL_BODY")"
|
|
143
|
+
rm -f "$POLL_BODY"
|
|
144
|
+
err "$MSG" "$HTTP" "FAILED" "Inspect the message; re-submit after addressing it."
|
|
145
|
+
;;
|
|
146
|
+
QUEUED|RUNNING) : ;; # keep polling
|
|
147
|
+
*) echo "poll $i: unexpected status '$STATUS', continuing" >&2 ;;
|
|
148
|
+
esac
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
rm -f "$POLL_BODY"
|
|
152
|
+
err "scan did not finish within $((MAX_POLLS * INTERVAL))s (last status: $STATUS)" 0 "$STATUS" \
|
|
153
|
+
"Large projects can take longer — re-run with --max-polls higher, or use --fast."
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One-shot ApexGuru performance scan: package → submit+poll → decode+present.
|
|
3
|
+
#
|
|
4
|
+
# This wrapper runs all three steps of the workflow in a single command so the
|
|
5
|
+
# scan cannot be left half-finished (a bare run-scan.sh already prints a
|
|
6
|
+
# violation count, which can read as "done" and tempt an early stop before the
|
|
7
|
+
# report is decoded). The FINAL stdout of this script is the ready-to-present
|
|
8
|
+
# markdown report from decode-report.js --present — print that to the user
|
|
9
|
+
# verbatim.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# bash scan.sh <project-root> [--out <report.md>]
|
|
13
|
+
# [--org <alias>] [--fast] [--raw <raw-out.json>]
|
|
14
|
+
#
|
|
15
|
+
# <project-root> folder that contains Apex somewhere beneath it (defaults to
|
|
16
|
+
# CWD if omitted or given as "."). build-zip.sh collects every
|
|
17
|
+
# .cls/.trigger under it, any layout.
|
|
18
|
+
# --out FILE also write the presented markdown to FILE (e.g. report.md).
|
|
19
|
+
# --org ALIAS forwarded to run-scan.sh (which sf org to derive the JWT from).
|
|
20
|
+
# --fast forwarded to run-scan.sh (skip LLM-heavy fix generation).
|
|
21
|
+
# --raw FILE keep the raw SUCCEEDED JSON at FILE (default: a temp file).
|
|
22
|
+
#
|
|
23
|
+
# The endpoint (prod/stage/dev) is chosen from the token's env by resolve-token.sh;
|
|
24
|
+
# customers on a prod org always hit api.salesforce.com.
|
|
25
|
+
#
|
|
26
|
+
# Progress for each step streams to STDERR; only the final presented markdown
|
|
27
|
+
# goes to STDOUT. On any step failure, the failing step's JSON error/hint is
|
|
28
|
+
# surfaced to stderr and the script exits non-zero.
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
|
|
33
|
+
PROJECT_ROOT=""
|
|
34
|
+
OUT_FILE=""
|
|
35
|
+
RAW_OUT=""
|
|
36
|
+
ORG_ALIAS=""
|
|
37
|
+
FAST=""
|
|
38
|
+
|
|
39
|
+
# First non-flag arg is the project root.
|
|
40
|
+
while [ $# -gt 0 ]; do
|
|
41
|
+
case "$1" in
|
|
42
|
+
--out) OUT_FILE="${2:-}"; shift 2 ;;
|
|
43
|
+
--raw) RAW_OUT="${2:-}"; shift 2 ;;
|
|
44
|
+
--org) ORG_ALIAS="${2:-}"; shift 2 ;;
|
|
45
|
+
--fast) FAST="--fast"; shift ;;
|
|
46
|
+
*) [ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$1"; shift ;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
[ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$(pwd)"
|
|
51
|
+
[ "$PROJECT_ROOT" = "." ] && PROJECT_ROOT="$(pwd)"
|
|
52
|
+
|
|
53
|
+
TS="$(date +%Y%m%d-%H%M%S)"
|
|
54
|
+
WORK_DIR="$(mktemp -d)"
|
|
55
|
+
ZIP_PATH="$WORK_DIR/apexguru-${TS}.zip"
|
|
56
|
+
[ -n "$RAW_OUT" ] || RAW_OUT="$WORK_DIR/apexguru-raw-${TS}.json"
|
|
57
|
+
|
|
58
|
+
cleanup() { [ -n "${WORK_DIR:-}" ] && rm -rf "$WORK_DIR" 2>/dev/null || true; }
|
|
59
|
+
trap cleanup EXIT
|
|
60
|
+
|
|
61
|
+
# --- Step 1: package the project's Apex into a zip ---
|
|
62
|
+
echo "[scan] Step 1/3: packaging $PROJECT_ROOT ..." >&2
|
|
63
|
+
ZIP_JSON="$(bash "$SCRIPT_DIR/build-zip.sh" "$PROJECT_ROOT" "$ZIP_PATH")" || {
|
|
64
|
+
echo "$ZIP_JSON" >&2
|
|
65
|
+
echo "[scan] packaging failed — see the error/hint above." >&2
|
|
66
|
+
exit 1
|
|
67
|
+
}
|
|
68
|
+
echo "[scan] packaged: $(echo "$ZIP_JSON" | jq -r '.humanSize // "?"') → $(echo "$ZIP_JSON" | jq -r '.zip')" >&2
|
|
69
|
+
|
|
70
|
+
# --- Step 2: submit + poll to completion ---
|
|
71
|
+
echo "[scan] Step 2/3: submitting and polling the scan ..." >&2
|
|
72
|
+
RUN_ARGS=("$ZIP_PATH" "$RAW_OUT")
|
|
73
|
+
[ -n "$ORG_ALIAS" ] && RUN_ARGS+=(--org "$ORG_ALIAS")
|
|
74
|
+
[ -n "$FAST" ] && RUN_ARGS+=("$FAST")
|
|
75
|
+
RUN_JSON="$(bash "$SCRIPT_DIR/run-scan.sh" "${RUN_ARGS[@]}")" || {
|
|
76
|
+
echo "$RUN_JSON" >&2
|
|
77
|
+
echo "[scan] scan failed — see the error/hint above." >&2
|
|
78
|
+
exit 1
|
|
79
|
+
}
|
|
80
|
+
echo "[scan] scan complete: $(echo "$RUN_JSON" | jq -r '.violationCount // "?"') violations" >&2
|
|
81
|
+
|
|
82
|
+
# --- Step 3: decode + present (this is the deliverable) ---
|
|
83
|
+
echo "[scan] Step 3/3: decoding and presenting the report ..." >&2
|
|
84
|
+
PRESENTED="$(node "$SCRIPT_DIR/decode-report.js" "$RAW_OUT" --present)" || {
|
|
85
|
+
echo "[scan] decoding failed against $RAW_OUT" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Optionally persist the presented markdown (e.g. so it survives temp cleanup).
|
|
90
|
+
if [ -n "$OUT_FILE" ]; then
|
|
91
|
+
printf '%s\n' "$PRESENTED" > "$OUT_FILE"
|
|
92
|
+
echo "[scan] report also written to $OUT_FILE" >&2
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# FINAL stdout: the ready-to-present markdown. Print this to the user verbatim.
|
|
96
|
+
printf '%s\n' "$PRESENTED"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Locally sanity-check an SFAP JWT *before* any network call, so a bad token
|
|
3
|
+
// fails fast with a clear reason instead of a bare HTTP 401 deep in the poll
|
|
4
|
+
// loop. This does NOT verify the signature (only the server can) — it decodes
|
|
5
|
+
// the freely-readable header/body claims to catch the two things that cause
|
|
6
|
+
// almost every 401: missing sfap_api scope, or expired.
|
|
7
|
+
//
|
|
8
|
+
// The token's environment (prod/stage/dev) is detected from its tnk claim and
|
|
9
|
+
// reported as detectedEnv so the caller can route to the matching endpoint.
|
|
10
|
+
//
|
|
11
|
+
// The token is read from STDIN (never argv) so it does not leak into the
|
|
12
|
+
// process list. Usage:
|
|
13
|
+
// printf '%s' "$JWT" | node validate-token.js [--now <epoch>]
|
|
14
|
+
//
|
|
15
|
+
// Output (stdout, single-line JSON):
|
|
16
|
+
// pass: {"ok":true,"tnk":"...","detectedEnv":"prod|stage|dev|null","expiresInSeconds":N,"warnings":[...]}
|
|
17
|
+
// fail: {"ok":false,"error":"...","hint":"..."} (exit 1)
|
|
18
|
+
// A "fail" only happens for things we can PROVE wrong (expired / missing scope).
|
|
19
|
+
// Anything we cannot read is a warning, not a block.
|
|
20
|
+
|
|
21
|
+
function out(obj, code) {
|
|
22
|
+
console.log(JSON.stringify(obj));
|
|
23
|
+
process.exit(code);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
let now = Math.floor(Date.now() / 1000);
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
if (args[i] === "--now") now = parseInt(args[++i], 10) || now;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Read the token from stdin (keeps it out of argv / process list).
|
|
33
|
+
let token = "";
|
|
34
|
+
try {
|
|
35
|
+
token = require("fs").readFileSync(0, "utf8").trim();
|
|
36
|
+
} catch {
|
|
37
|
+
token = "";
|
|
38
|
+
}
|
|
39
|
+
if (!token) out({ ok: false, error: "no token on stdin", hint: "resolve the token before validating" }, 1);
|
|
40
|
+
|
|
41
|
+
// A JWT is header.body.signature, each a base64url-encoded chunk.
|
|
42
|
+
function decodeSegment(seg) {
|
|
43
|
+
const b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
|
|
44
|
+
const pad = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
|
45
|
+
return JSON.parse(Buffer.from(pad, "base64").toString("utf8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parts = token.split(".");
|
|
49
|
+
if (parts.length < 2) {
|
|
50
|
+
// Not a JWT we can read — don't block (token may be an opaque format).
|
|
51
|
+
out({ ok: true, tnk: null, detectedEnv: null, expiresInSeconds: null, warnings: ["token is not a readable JWT — skipping local checks"] }, 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let header, body;
|
|
55
|
+
try {
|
|
56
|
+
header = decodeSegment(parts[0]);
|
|
57
|
+
body = decodeSegment(parts[1]);
|
|
58
|
+
} catch {
|
|
59
|
+
out({ ok: true, tnk: null, detectedEnv: null, expiresInSeconds: null, warnings: ["could not decode JWT claims — skipping local checks"] }, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const warnings = [];
|
|
63
|
+
|
|
64
|
+
// --- 1. tnk / environment. Per the SFAP contract, tnk lives on the HEADER and
|
|
65
|
+
// looks like core/<instance>/<orgId> (e.g. core/prod/00D..., core/stagecomstg2/...,
|
|
66
|
+
// core/falcondeva/...). The environment marker lives ONLY in the <instance>
|
|
67
|
+
// segment — the trailing <orgId> (00D...) is an opaque id that must NOT be
|
|
68
|
+
// scanned for substrings, or a prod org whose id happens to contain "dev"/"stg"
|
|
69
|
+
// would be wrongly rejected. So we match markers against the instance segment
|
|
70
|
+
// and the iss host only, never the whole claim string. The detected env is
|
|
71
|
+
// reported (not rejected) so the caller can route to the matching endpoint. ---
|
|
72
|
+
const tnk = header.tnk || body.tnk || null;
|
|
73
|
+
const iss = body.iss || "";
|
|
74
|
+
// tnk instance segment = the middle of core/<instance>/<orgId>; fall back to the
|
|
75
|
+
// whole tnk only if it isn't in that 3-part shape.
|
|
76
|
+
const tnkParts = tnk ? String(tnk).split("/") : [];
|
|
77
|
+
const tnkInstance = tnkParts.length >= 3 ? tnkParts[1] : tnk || "";
|
|
78
|
+
// iss host (strip scheme/path) so we match the host label, not a full URL.
|
|
79
|
+
const issHost = String(iss).replace(/^https?:\/\//, "").split(/[/?#]/)[0];
|
|
80
|
+
const envHay = `${tnkInstance} ${issHost}`.toLowerCase();
|
|
81
|
+
const looksDev = /falcondeva|falcondev|falcontest|deva|(^|[^a-z])dev([^a-z]|$)/.test(envHay);
|
|
82
|
+
const looksStage = /stg|stage/.test(envHay);
|
|
83
|
+
// Env inferred from the token's own claims (null when we can't tell). Production
|
|
84
|
+
// instances carry no stg/dev marker, so a readable tnk with neither signal is prod.
|
|
85
|
+
const detectedEnv = looksStage && !looksDev
|
|
86
|
+
? "stage"
|
|
87
|
+
: looksDev && !looksStage
|
|
88
|
+
? "dev"
|
|
89
|
+
: tnk && !looksStage && !looksDev
|
|
90
|
+
? "prod"
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
if (!tnk) warnings.push("no tnk claim found — could not detect the token's environment; defaulting to prod endpoint");
|
|
94
|
+
|
|
95
|
+
// --- 2. scope must include sfap_api. Scope may be a space-delimited string
|
|
96
|
+
// (scp/scope) or an array. Only fail if we can read it AND sfap_api is absent. ---
|
|
97
|
+
const scopeRaw = body.scp ?? body.scope ?? null;
|
|
98
|
+
if (scopeRaw != null) {
|
|
99
|
+
const scopes = Array.isArray(scopeRaw) ? scopeRaw : String(scopeRaw).split(/\s+/);
|
|
100
|
+
if (!scopes.includes("sfap_api")) {
|
|
101
|
+
out({ ok: false, error: "token scope does not include sfap_api",
|
|
102
|
+
hint: "Re-mint the token with the sfap_api scope. See references/authentication.md." }, 1);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
warnings.push("no scope claim found — could not confirm sfap_api scope");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- 3. expiry. exp is epoch seconds. Fail if already past. ---
|
|
109
|
+
let expiresInSeconds = null;
|
|
110
|
+
if (typeof body.exp === "number") {
|
|
111
|
+
expiresInSeconds = body.exp - now;
|
|
112
|
+
if (expiresInSeconds <= 0) {
|
|
113
|
+
out({ ok: false, error: `token expired ${Math.abs(expiresInSeconds)}s ago`,
|
|
114
|
+
hint: "Re-mint a fresh SFAP token and retry." }, 1);
|
|
115
|
+
}
|
|
116
|
+
if (expiresInSeconds < 60) warnings.push(`token expires in ${expiresInSeconds}s — it may lapse mid-scan`);
|
|
117
|
+
} else {
|
|
118
|
+
warnings.push("no exp claim found — could not confirm the token is unexpired");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
out({ ok: true, tnk, detectedEnv, expiresInSeconds, warnings }, 0);
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dx-devops-pipeline-manage
|
|
3
|
+
description: "Use this skill to manage the full lifecycle of a DevOps Center pipeline — list all pipelines, get a single pipeline's details, create a new pipeline linked to a Git repository, add or remove stages, rename a stage, add or remove Salesforce environments on stages, attach or detach projects, and activate or deactivate the pipeline. Invoke when the user wants to set up a release pipeline, wire promotion stages across integration, UAT, staging, and production orgs, connect environments to stages, attach a project, or activate a continuous delivery pipeline. Uses sf devops pipeline and sf devops stage commands with --json output. DO NOT TRIGGER for work-item lifecycle, promotion or deployment execution, conflict detection, or standalone project creation (separate skills)."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0"
|
|
6
|
+
minApiVersion: "58.0"
|
|
7
|
+
relatedSkills:
|
|
8
|
+
- "dx-devops-work-item-manage"
|
|
9
|
+
accessCheck:
|
|
10
|
+
- type: "orgPref"
|
|
11
|
+
value: "ALMDevopsCorePref"
|
|
12
|
+
- type: "userPerm"
|
|
13
|
+
value: "UserHasDevOpsCore"
|
|
14
|
+
cliTools:
|
|
15
|
+
- tool: ["jq"]
|
|
16
|
+
semver: ">=1.6"
|
|
17
|
+
- tool: ["sf"]
|
|
18
|
+
semver: ">=2.0.0"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# DevOps Center Pipeline Management
|
|
22
|
+
|
|
23
|
+
Manages the complete pipeline lifecycle in DevOps Center — from creation against a repository, through stage and environment configuration and project attachment, to activation of a ready-to-promote release pipeline. Provides headless CLI-driven operations for autonomous release workflows.
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
- **In scope**: List pipelines, get pipeline details, create a pipeline (linked to an existing or new Git repo), add/delete/rename stages, add/delete Salesforce environments on stages, attach/detach projects, and activate/deactivate/rename the pipeline
|
|
28
|
+
- **Out of scope**: Work-item lifecycle, promotion/deployment execution, conflict detection, standalone project creation (separate skills)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Required Inputs
|
|
33
|
+
|
|
34
|
+
Gather or infer before proceeding:
|
|
35
|
+
|
|
36
|
+
- **Operation type**: list, get, create, add-stage, delete-stage, rename-stage, add-environment, delete-environment, attach-project, detach-project, activate, or deactivate
|
|
37
|
+
- **For get / any stage or environment op**: pipeline ID (required) — obtain via `sf devops pipeline list --json`
|
|
38
|
+
- **For create**: pipeline name (required) and a Git repo (`--repo`, required). Repo flags differ by scenario:
|
|
39
|
+
- **Existing repo (GitHub or Bitbucket)**: only `--repo <url>` — do **not** pass `--repo-type`/`--create-repo`
|
|
40
|
+
- **New GitHub repo**: `--repo <name> --create-repo --repo-type github --repo-owner <org-or-user>`
|
|
41
|
+
- **New Bitbucket repo**: `--repo <name> --create-repo --repo-type bitbucket --bitbucket-workspace <workspace>` (`--bitbucket-project-key <key>` optional)
|
|
42
|
+
- Description (`--description`) optional in all cases
|
|
43
|
+
- **For add-stage**: pipeline ID, new stage name, and `--next-stage-id` (the stage the new one precedes) — get stage IDs via `sf devops pipeline get`
|
|
44
|
+
- **For add-environment**: pipeline ID, stage ID, environment name, and `--org-type` (Production or Sandbox)
|
|
45
|
+
- **For attach/detach-project**: pipeline ID and project ID
|
|
46
|
+
- **For activate/deactivate/rename**: pipeline ID
|
|
47
|
+
|
|
48
|
+
Defaults unless specified:
|
|
49
|
+
- Output format: `--json` for headless consumption
|
|
50
|
+
- Target org: use `--target-org <alias>` if not relying on the default org
|
|
51
|
+
|
|
52
|
+
If the user gives a clear request ("create a pipeline on repo myorg/myrepo", "add a UAT stage before Production", "activate pipeline 0XB..."), proceed immediately without unnecessary questions.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Workflow
|
|
57
|
+
|
|
58
|
+
All operations use `sf devops pipeline` and `sf devops stage` CLI commands with `--json` output for structured consumption. Pipeline IDs and stage IDs are the primary identifiers — resolve them via `list` and `get` before mutating.
|
|
59
|
+
|
|
60
|
+
### Phase 1 — Identify Operation
|
|
61
|
+
|
|
62
|
+
1. **Determine the operation type** from user intent:
|
|
63
|
+
- "list", "show all pipelines" → list; "details of pipeline", "show stages" → get
|
|
64
|
+
- "create", "set up", "new pipeline" → create
|
|
65
|
+
- "add stage", "insert stage" → add-stage; "rename stage" → rename-stage; "remove/delete stage" → delete-stage
|
|
66
|
+
- "connect environment", "add org to stage" → add-environment; "remove environment" → delete-environment
|
|
67
|
+
- "attach project", "connect project" → attach-project; "detach project" → detach-project
|
|
68
|
+
- "activate", "turn on"; "deactivate", "turn off"; "rename pipeline" → lifecycle update
|
|
69
|
+
|
|
70
|
+
### Phase 2 — Execute Operation
|
|
71
|
+
|
|
72
|
+
2. **Verify org authentication** before any operation:
|
|
73
|
+
```bash
|
|
74
|
+
sf org display --json
|
|
75
|
+
```
|
|
76
|
+
- If no default org is set or auth has expired, instruct the user to run `sf org login web --set-default --alias <alias>`
|
|
77
|
+
- Confirm the org has DevOps Center enabled by running `sf devops pipeline list --json`
|
|
78
|
+
- Add `--target-org <alias>` to every command when targeting a specific org
|
|
79
|
+
|
|
80
|
+
3. **Inspect pipelines**:
|
|
81
|
+
```bash
|
|
82
|
+
sf devops pipeline list --json # all pipelines in the org
|
|
83
|
+
sf devops pipeline get --pipeline-id <pipeline-id> --json # one pipeline, with stages/repos/projects
|
|
84
|
+
```
|
|
85
|
+
- `list` returns SObject records under `.result.pipelines[]` with capitalized fields (`.Id`, `.Name`, `.IsActive`) — it does **not** include stages or connected projects
|
|
86
|
+
- `get` returns a single pipeline under `.result` with camelCase fields (`.id`, `.name`, `.stages[]`, `.connectedProjects[]`); each stage has `.id`, `.name`, `.nextStageId`, `.branchName`, and `.environment.{id,name}`. **Stages are a linked list** — order is defined by `nextStageId`, and the terminal stage has `nextStageId: null`. Use `get` to discover **stage IDs** before any stage or environment operation
|
|
87
|
+
|
|
88
|
+
4. **Create a pipeline** — the pipeline must be linked to a Git repository. `--name` and `--repo` are always required; the remaining flags depend on the repo scenario:
|
|
89
|
+
```bash
|
|
90
|
+
# Existing repo (GitHub or Bitbucket) — pass the full repo URL, nothing else
|
|
91
|
+
sf devops pipeline create --name "<pipeline-name>" --repo <repo-url> --json
|
|
92
|
+
|
|
93
|
+
# New GitHub repo — requires --repo-owner
|
|
94
|
+
sf devops pipeline create --name "<pipeline-name>" --repo <repo-name> \
|
|
95
|
+
--create-repo --repo-type github --repo-owner <org-or-user> --json
|
|
96
|
+
|
|
97
|
+
# New Bitbucket repo — requires --bitbucket-workspace (--bitbucket-project-key optional)
|
|
98
|
+
sf devops pipeline create --name "<pipeline-name>" --repo <repo-name> \
|
|
99
|
+
--create-repo --repo-type bitbucket --bitbucket-workspace <workspace> \
|
|
100
|
+
--bitbucket-project-key <key> --json
|
|
101
|
+
|
|
102
|
+
# Custom stage chain (any scenario) — repeat --stage in promotion order
|
|
103
|
+
sf devops pipeline create --name "<pipeline-name>" --repo <repo-url> \
|
|
104
|
+
--stage Dev --stage QA --stage Prod --json
|
|
105
|
+
```
|
|
106
|
+
- Provider-specific required flags: **GitHub new repo** → `--repo-owner`; **Bitbucket new repo** → `--bitbucket-workspace`. Omitting the provider's required flag fails the create
|
|
107
|
+
- Do **not** pass `--repo-type`/`--create-repo` for an existing repo — supply only the repo URL via `--repo`
|
|
108
|
+
- **Custom stages at create time**: a new pipeline seeds the default stage chain **Integration → UAT → Staging → Production**. To seed different stages, repeat `-s/--stage` once per stage **in promotion order** — e.g. `--stage Dev --stage QA --stage Prod`. This avoids adding/renaming stages afterward
|
|
109
|
+
- Add `--description "<text>"` optionally in any scenario
|
|
110
|
+
- Capture the returned pipeline ID for subsequent stage/environment/project/activation steps
|
|
111
|
+
- **Idempotency**: the CLI does not dedupe. Before creating, run `sf devops pipeline list --json` and check for a pipeline with the same name/repo; return the existing one if found. See `references/parsing-patterns.md` for the check-before-create snippet
|
|
112
|
+
|
|
113
|
+
5. **Configure stages** — a stage is added relative to an existing stage, then bound to an environment. **Read `references/cli-commands.md`** for full flag details before multi-stage work:
|
|
114
|
+
```bash
|
|
115
|
+
# Insert an empty stage BEFORE an existing stage (get the next-stage-id from `pipeline get`)
|
|
116
|
+
sf devops pipeline stage add --pipeline-id <id> --name "<stage-name>" --next-stage-id <stage-id> --json
|
|
117
|
+
# Rename a stage
|
|
118
|
+
sf devops pipeline stage update --pipeline-id <id> --stage-id <stage-id> --name "<new-name>" --json
|
|
119
|
+
# Delete a stage (predecessor auto-relinks to successor)
|
|
120
|
+
sf devops pipeline stage delete --pipeline-id <id> --stage-id <stage-id> --json
|
|
121
|
+
```
|
|
122
|
+
- `stage add` inserts an **empty** stage (no branch/environment) before `--next-stage-id`; configure its environment separately
|
|
123
|
+
- Build the promotion chain by inserting each new stage before the stage that should follow it
|
|
124
|
+
|
|
125
|
+
6. **Bind environments to stages** — attach a Salesforce org to a stage:
|
|
126
|
+
```bash
|
|
127
|
+
# Validate the org-type against the fixed enum BEFORE calling the CLI
|
|
128
|
+
bash scripts/validate-org-type.sh "<Production|Sandbox>" # exits non-zero on an invalid value
|
|
129
|
+
sf devops stage environment add --pipeline-id <id> --stage-id <stage-id> \
|
|
130
|
+
--environment-name "<env-name>" --org-type <Production|Sandbox> --json
|
|
131
|
+
# Remove an environment (pipeline must be inactive)
|
|
132
|
+
sf devops stage environment delete --pipeline-id <id> --environment-id <env-id> --json
|
|
133
|
+
```
|
|
134
|
+
- `--org-type` must be exactly `Production` or `Sandbox` — run `scripts/validate-org-type.sh <value>` first and only proceed on exit 0
|
|
135
|
+
- **Headless caveat**: `stage environment add` triggers an OAuth browser flow. In headless/CI runs pass `--no-browser` — the CLI prints a redirect URL for manual authentication
|
|
136
|
+
|
|
137
|
+
7. **Attach / detach a project** — a project can be attached to only one pipeline:
|
|
138
|
+
```bash
|
|
139
|
+
sf devops pipeline project add --pipeline-id <id> --project-id <project-id> --json
|
|
140
|
+
sf devops pipeline project delete --pipeline-id <id> --project-id <project-id> --json
|
|
141
|
+
```
|
|
142
|
+
- If the user names a project instead of providing its ID, resolve it via `sf devops project list --json` (see `references/parsing-patterns.md`)
|
|
143
|
+
|
|
144
|
+
8. **Activate / deactivate / rename the pipeline**:
|
|
145
|
+
```bash
|
|
146
|
+
# Before activating, confirm the deterministic ≥1-stage prerequisite
|
|
147
|
+
bash scripts/check-activation-ready.sh <id> [target-org] # exits non-zero if stage-less
|
|
148
|
+
sf devops pipeline update --pipeline-id <id> --activate --json # activate
|
|
149
|
+
sf devops pipeline update --pipeline-id <id> --deactivate --json # deactivate
|
|
150
|
+
sf devops pipeline update --pipeline-id <id> --name "<new-name>" --json # rename
|
|
151
|
+
```
|
|
152
|
+
- Before `--activate`, run `scripts/check-activation-ready.sh <id>` and only proceed on exit 0 — it fails with an actionable message when the pipeline has no stages
|
|
153
|
+
- **Stages cannot be modified after the pipeline is activated and changes are promoted through it** — finish stage/environment configuration before activating
|
|
154
|
+
- `--activate` and `--deactivate` are mutually exclusive; `--deactivate` and `--name` may be combined in one command
|
|
155
|
+
|
|
156
|
+
### Phase 3 — Verify and Report
|
|
157
|
+
|
|
158
|
+
9. **Verify operation success** — use `scripts/verify-operation.sh`, which performs the deterministic JSON-status and post-state field checks and exits non-zero with an actionable message on mismatch:
|
|
159
|
+
```bash
|
|
160
|
+
# Assert a captured command's JSON status is 0 (pipe the CLI output in)
|
|
161
|
+
sf devops pipeline update --pipeline-id <id> --activate --json | bash scripts/verify-operation.sh status -
|
|
162
|
+
# Assert post-state after activate / stage / project ops
|
|
163
|
+
bash scripts/verify-operation.sh active <id> true [target-org] # isActive == true
|
|
164
|
+
bash scripts/verify-operation.sh has-stage <id> "<stage>" [target-org] # stage present in chain
|
|
165
|
+
bash scripts/verify-operation.sh has-project <id> "<project>" [target-org] # project connected
|
|
166
|
+
```
|
|
167
|
+
- **Create**: confirm the pipeline appears in `sf devops pipeline list --json` by `.Name` and capture its `.Id`
|
|
168
|
+
- **Stage / environment / project changes**: verify with the `has-stage` / `has-project` modes above (they read `sf devops pipeline get` and check `.result.stages[]` / `.result.connectedProjects[]`)
|
|
169
|
+
- **Activate**: verify with the `active <id> true` mode
|
|
170
|
+
|
|
171
|
+
10. **Report results**:
|
|
172
|
+
- **List**: pipeline name, ID, and active state per pipeline (no stages — that's what `get` is for)
|
|
173
|
+
- **Get**: pipeline name, ID, active state, stage chain (each stage's name → environment → branch, ordered via `nextStageId`), connected projects
|
|
174
|
+
- **Create**: pipeline ID, name, and linked repo (or "existing pipeline returned" on idempotent match)
|
|
175
|
+
- **Stage / environment / project op**: the resulting stage chain with each stage's environment, in promotion order
|
|
176
|
+
- **Lifecycle**: the new active state and/or name
|
|
177
|
+
|
|
178
|
+
### Verification Checklist (gate before reporting success)
|
|
179
|
+
|
|
180
|
+
Confirm the items for the operation you performed. Do **not** report success until every applicable box holds:
|
|
181
|
+
|
|
182
|
+
- [ ] Every `sf devops` command was run with `--json` and returned `status: 0` (`scripts/verify-operation.sh status -`)
|
|
183
|
+
- [ ] **Create**: the new pipeline appears in `sf devops pipeline list --json` by name, and (for a new repo) the provider-specific flags were supplied (`--repo-owner` for GitHub, `--bitbucket-workspace` for Bitbucket)
|
|
184
|
+
- [ ] **Add-stage / add-environment**: the stage exists in the chain and `--org-type` passed `scripts/validate-org-type.sh` (`scripts/verify-operation.sh has-stage ...`)
|
|
185
|
+
- [ ] **Attach-project**: the project shows in `.result.connectedProjects[]` (`scripts/verify-operation.sh has-project ...`)
|
|
186
|
+
- [ ] **Activate**: `scripts/check-activation-ready.sh` passed beforehand and `.result.isActive` is now `true` (`scripts/verify-operation.sh active <id> true`)
|
|
187
|
+
- [ ] **Delete-environment**: the pipeline was inactive before the delete
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Rules / Constraints
|
|
192
|
+
|
|
193
|
+
| Constraint | Rationale |
|
|
194
|
+
|-----------|-----------|
|
|
195
|
+
| All sf devops commands must use `--json` flag | Structured output is required for headless consumption; human-readable output is unreliable for parsing |
|
|
196
|
+
| A pipeline requires a Git repo at create time | `sf devops pipeline create` requires `--name` and `--repo`; for an existing repo pass only the URL, for a new repo add `--create-repo` and `--repo-type` |
|
|
197
|
+
| New-repo create needs provider-specific flags | GitHub requires `--repo-owner`; Bitbucket requires `--bitbucket-workspace` (`--bitbucket-project-key` optional). The wrong provider's flags fail the command |
|
|
198
|
+
| Pipeline ID required for get, update, and all stage/environment/project ops | These commands identify the pipeline only by `--pipeline-id`; obtain it via `sf devops pipeline list` |
|
|
199
|
+
| Stage IDs come from `pipeline get` | `stage add` (`--next-stage-id`), `stage update`/`delete` (`--stage-id`), and `stage environment add` (`--stage-id`) all need stage IDs |
|
|
200
|
+
| `stage add` inserts an empty stage before `--next-stage-id` | Stages carry no environment until one is added; build the chain by anchoring to the following stage |
|
|
201
|
+
| `--org-type` must be exactly `Production` or `Sandbox` | The flag is a fixed enum; other values fail |
|
|
202
|
+
| Pipeline must have ≥1 stage before activation | `sf devops pipeline update --activate` rejects a stage-less pipeline |
|
|
203
|
+
| Do not modify stages after activate + promote | DevOps Center locks stage structure once changes have been promoted through an active pipeline |
|
|
204
|
+
| Environment delete requires an inactive pipeline | `stage environment delete` only succeeds while the pipeline is inactive |
|
|
205
|
+
| A project attaches to only one pipeline | `pipeline project add` fails if the project is already attached elsewhere; detach first |
|
|
206
|
+
| Idempotent create via check-before-create | The CLI does not dedupe; list existing pipelines and return the match instead of erroring |
|
|
207
|
+
| Prefer `--no-browser` in headless runs | `stage environment add` opens an OAuth browser flow; `--no-browser` prints a redirect URL for CI |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Gotchas
|
|
212
|
+
|
|
213
|
+
| Issue | Resolution |
|
|
214
|
+
|-------|------------|
|
|
215
|
+
| **No default org set** | Run `sf org display --json` first; if it fails, instruct user to run `sf org login web --set-default` |
|
|
216
|
+
| **Create fails — missing repo** | `--repo` is required; pass an existing repo URL, or `--create-repo` + `--repo-type` for a new repo |
|
|
217
|
+
| **New-repo create fails — missing provider flag** | GitHub new repo needs `--repo-owner`; Bitbucket new repo needs `--bitbucket-workspace`. Don't mix providers' flags (`--repo-owner` with `bitbucket`, or `--bitbucket-workspace` with `github`) |
|
|
218
|
+
| **`stage add` fails — no next-stage-id** | `--next-stage-id` is required; run `sf devops pipeline get --pipeline-id <id> --json` to find the stage IDs and pick the one the new stage should precede |
|
|
219
|
+
| **Environment add hangs in CI** | The OAuth browser flow blocks headless runs; add `--no-browser` and complete auth via the printed redirect URL |
|
|
220
|
+
| **Activation rejected** | The pipeline needs at least one stage; add a stage (and its environment) before `--activate` |
|
|
221
|
+
| **Cannot modify stages** | The pipeline is active and has promoted changes; stage structure is locked — configuration must complete before activation |
|
|
222
|
+
| **Environment delete fails** | The pipeline is active; deactivate with `pipeline update --deactivate` before deleting the environment |
|
|
223
|
+
| **Project already attached** | A project attaches to only one pipeline; detach from the other pipeline first via `pipeline project delete` |
|
|
224
|
+
| **Pipeline / stage / project not found** | The ID is invalid; run `sf devops pipeline list --json`, `sf devops pipeline get --json`, or `sf devops project list --json` to find valid IDs |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Output Expectations
|
|
229
|
+
|
|
230
|
+
Deliverables vary by operation:
|
|
231
|
+
|
|
232
|
+
- **List**: pipelines with ID, name, and active state (no stages/projects in the list view)
|
|
233
|
+
- **Get**: a pipeline with ID, name, active state, its stage chain (each with environment and branch, ordered via `nextStageId`), and connected projects
|
|
234
|
+
- **Create**: pipeline ID, name, and linked repository (or the pre-existing pipeline on idempotent match)
|
|
235
|
+
- **Stage op**: the updated ordered stage chain
|
|
236
|
+
- **Environment op**: the stage with its bound environment (name, org-type)
|
|
237
|
+
- **Project op**: confirmation of attach/detach
|
|
238
|
+
- **Lifecycle**: the new active state and/or pipeline name
|
|
239
|
+
|
|
240
|
+
Outputs are derived from `sf devops pipeline` and `sf devops stage` CLI commands.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Cross-Skill Integration
|
|
245
|
+
|
|
246
|
+
| Delegate to | When |
|
|
247
|
+
|-------------|------|
|
|
248
|
+
| `dx-devops-work-item-manage` | The user wants to create or advance work items once the pipeline is active |
|
|
249
|
+
|
|
250
|
+
If a project the user wants to attach can't be found, resolve or list existing projects with `sf devops project list --json` (see `references/parsing-patterns.md`) rather than delegating — project creation is out of scope for this skill.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Reference File Index
|
|
255
|
+
|
|
256
|
+
| File | When to read |
|
|
257
|
+
|------|-------------|
|
|
258
|
+
| `references/cli-commands.md` | When you need detailed CLI flag documentation and JSON output schemas for each `sf devops pipeline` / `sf devops stage` command |
|
|
259
|
+
| `references/parsing-patterns.md` | When you need jq snippets to parse the JSON (stage chains, pipeline/project ID resolution), error-handling reference, the check-before-create idempotent pattern, or auth requirements |
|
|
260
|
+
| `examples/common-workflows.md` | When the user's request matches a common pattern (end-to-end pipeline setup, inserting a stage, binding an environment, attaching a project, activation) |
|
|
261
|
+
| `scripts/validate-org-type.sh` | Run before `stage environment add` to validate `--org-type` against the `Production`/`Sandbox` enum |
|
|
262
|
+
| `scripts/check-activation-ready.sh` | Run before `pipeline update --activate` to confirm the pipeline has ≥1 stage |
|
|
263
|
+
| `scripts/verify-operation.sh` | Run in Phase 3 to assert a command's JSON status and post-state fields (`status` / `active` / `has-stage` / `has-project`) |
|