@slamb2k/mad-skills 2.0.37 → 2.0.38
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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -5
- package/package.json +1 -1
- package/skills/manifest.json +5 -5
- package/skills/ship/SKILL.md +37 -47
- package/skills/ship/references/stage-prompts.md +46 -252
- package/skills/ship/scripts/ci-watch.sh +204 -0
- package/skills/ship/scripts/merge.sh +146 -0
- package/skills/ship/tests/evals.json +3 -3
- package/skills/sync/SKILL.md +22 -102
- package/skills/sync/scripts/sync.sh +158 -0
- package/agents/ship-analyzer.md +0 -106
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ci-watch.sh — Poll CI/pipeline checks until complete with fail-fast
|
|
3
|
+
# Usage: ci-watch.sh <PLATFORM> <PR_NUMBER> <BRANCH> [azdo options]
|
|
4
|
+
# AzDO options: --azdo-mode=cli|rest --azdo-org-url=URL --azdo-project=NAME --azdo-project-url-safe=NAME
|
|
5
|
+
# Env: AZURE_DEVOPS_EXT_PAT or AZDO_PAT (required for azdo rest mode)
|
|
6
|
+
# Exit codes: 0=all_passed/no_checks, 1=some_failed, 2=pending (timeout), 3=tool error
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
PLATFORM="${1:?Usage: ci-watch.sh <PLATFORM> <PR_NUMBER> <BRANCH>}"
|
|
10
|
+
PR_NUMBER="${2:?Usage: ci-watch.sh <PLATFORM> <PR_NUMBER> <BRANCH>}"
|
|
11
|
+
BRANCH="${3:?Usage: ci-watch.sh <PLATFORM> <PR_NUMBER> <BRANCH>}"
|
|
12
|
+
shift 3
|
|
13
|
+
|
|
14
|
+
AZDO_MODE="" AZDO_ORG_URL="" AZDO_PROJECT="" AZDO_PROJECT_URL_SAFE=""
|
|
15
|
+
for arg in "$@"; do
|
|
16
|
+
case "$arg" in
|
|
17
|
+
--azdo-mode=*) AZDO_MODE="${arg#*=}" ;;
|
|
18
|
+
--azdo-org-url=*) AZDO_ORG_URL="${arg#*=}" ;;
|
|
19
|
+
--azdo-project=*) AZDO_PROJECT="${arg#*=}" ;;
|
|
20
|
+
--azdo-project-url-safe=*) AZDO_PROJECT_URL_SAFE="${arg#*=}" ;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
STATUS="" CHECKS="" FAILING=""
|
|
25
|
+
|
|
26
|
+
emit_report() {
|
|
27
|
+
echo "CHECKS_REPORT_BEGIN"
|
|
28
|
+
echo "status=$STATUS"
|
|
29
|
+
echo "checks=$CHECKS"
|
|
30
|
+
echo "failing_checks=${FAILING:-none}"
|
|
31
|
+
echo "CHECKS_REPORT_END"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ── GitHub ──────────────────────────────────────────────
|
|
35
|
+
if [ "$PLATFORM" = "github" ]; then
|
|
36
|
+
if ! command -v gh &>/dev/null; then
|
|
37
|
+
STATUS="no_checks"; FAILING="none"
|
|
38
|
+
emit_report; exit 3
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# gh pr checks --watch blocks until done; --fail-fast stops on first failure
|
|
42
|
+
gh pr checks "$PR_NUMBER" --watch --fail-fast 2>/dev/null || true
|
|
43
|
+
|
|
44
|
+
# Parse final status
|
|
45
|
+
CHECKS_JSON=$(gh pr checks "$PR_NUMBER" --json name,state 2>/dev/null || echo "[]")
|
|
46
|
+
if [ "$CHECKS_JSON" = "[]" ]; then
|
|
47
|
+
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
48
|
+
emit_report; exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
FAIL_COUNT=$(echo "$CHECKS_JSON" | jq '[.[] | select(.state=="FAILURE")] | length')
|
|
52
|
+
CHECKS=$(echo "$CHECKS_JSON" | jq -r '.[] | "\(.name):\(.state | ascii_downcase)"' | paste -sd, -)
|
|
53
|
+
FAILING=$(echo "$CHECKS_JSON" | jq -r '.[] | select(.state=="FAILURE") | .name' | paste -sd, -)
|
|
54
|
+
|
|
55
|
+
if [ "${FAIL_COUNT:-0}" -gt 0 ]; then
|
|
56
|
+
STATUS="some_failed"
|
|
57
|
+
emit_report; exit 1
|
|
58
|
+
else
|
|
59
|
+
STATUS="all_passed"; FAILING="none"
|
|
60
|
+
emit_report; exit 0
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ── Azure DevOps ────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
# Resolve PAT for REST mode
|
|
67
|
+
PAT="${AZURE_DEVOPS_EXT_PAT:-${AZDO_PAT:-}}"
|
|
68
|
+
AUTH=""
|
|
69
|
+
if [ "$AZDO_MODE" = "rest" ]; then
|
|
70
|
+
if [ -z "$PAT" ]; then
|
|
71
|
+
STATUS="no_checks"; FAILING="none"
|
|
72
|
+
CHECKS="error:no PAT configured"
|
|
73
|
+
emit_report; exit 3
|
|
74
|
+
fi
|
|
75
|
+
AUTH="Authorization: Basic $(echo -n ":$PAT" | base64)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── AzDO CLI mode ──────────────────────────────────────
|
|
79
|
+
if [ "$AZDO_MODE" = "cli" ]; then
|
|
80
|
+
# Wait for CI to start (max 2 min)
|
|
81
|
+
RUNS_FOUND=false
|
|
82
|
+
for _ in $(seq 1 8); do
|
|
83
|
+
RUN_COUNT=$(az pipelines runs list --branch "$BRANCH" --top 5 \
|
|
84
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
85
|
+
--query "length(@)" -o tsv 2>/dev/null)
|
|
86
|
+
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
87
|
+
RUNS_FOUND=true; break
|
|
88
|
+
fi
|
|
89
|
+
sleep 15
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
if [ "$RUNS_FOUND" = false ]; then
|
|
93
|
+
# Check PR policies
|
|
94
|
+
POLICY_COUNT=$(az repos pr policy list --id "$PR_NUMBER" \
|
|
95
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
96
|
+
--query "length(@)" -o tsv 2>/dev/null || echo "0")
|
|
97
|
+
if [ "$POLICY_COUNT" = "0" ] || [ -z "$POLICY_COUNT" ]; then
|
|
98
|
+
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
99
|
+
emit_report; exit 0
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Wait for runs to complete with fail-fast (max 30 min)
|
|
104
|
+
for _ in $(seq 1 120); do
|
|
105
|
+
FAILED=$(az pipelines runs list --branch "$BRANCH" --top 5 \
|
|
106
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
107
|
+
--query "[?result=='failed'] | length(@)" -o tsv 2>/dev/null)
|
|
108
|
+
if [ -n "$FAILED" ] && [ "$FAILED" != "0" ]; then
|
|
109
|
+
break
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
IN_PROGRESS=$(az pipelines runs list --branch "$BRANCH" --top 5 \
|
|
113
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
114
|
+
--query "[?status=='inProgress'] | length(@)" -o tsv 2>/dev/null)
|
|
115
|
+
if [ "$IN_PROGRESS" = "0" ] || [ -z "$IN_PROGRESS" ]; then break; fi
|
|
116
|
+
sleep 15
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
# Determine final status
|
|
120
|
+
RUNS_TABLE=$(az pipelines runs list --branch "$BRANCH" --top 5 \
|
|
121
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
122
|
+
--query "[].{name:definition.name, result:result}" -o json 2>/dev/null || echo "[]")
|
|
123
|
+
CHECKS=$(echo "$RUNS_TABLE" | jq -r '.[] | "\(.name):\(.result // "pending")"' | paste -sd, -)
|
|
124
|
+
FAILING=$(echo "$RUNS_TABLE" | jq -r '.[] | select(.result=="failed") | .name' | paste -sd, -)
|
|
125
|
+
|
|
126
|
+
# Also check PR policies
|
|
127
|
+
REJECTED=$(az repos pr policy list --id "$PR_NUMBER" \
|
|
128
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
129
|
+
--query "[?status=='rejected'] | length(@)" -o tsv 2>/dev/null || echo "0")
|
|
130
|
+
|
|
131
|
+
FAIL_COUNT=$(echo "$RUNS_TABLE" | jq '[.[] | select(.result=="failed")] | length')
|
|
132
|
+
STILL_RUNNING=$(echo "$RUNS_TABLE" | jq '[.[] | select(.result==null)] | length')
|
|
133
|
+
|
|
134
|
+
if [ "${FAIL_COUNT:-0}" -gt 0 ] || [ "${REJECTED:-0}" -gt 0 ]; then
|
|
135
|
+
STATUS="some_failed"
|
|
136
|
+
emit_report; exit 1
|
|
137
|
+
elif [ "${STILL_RUNNING:-0}" -gt 0 ]; then
|
|
138
|
+
STATUS="pending"; FAILING="none"
|
|
139
|
+
emit_report; exit 2
|
|
140
|
+
else
|
|
141
|
+
STATUS="all_passed"; FAILING="none"
|
|
142
|
+
emit_report; exit 0
|
|
143
|
+
fi
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# ── AzDO REST mode ─────────────────────────────────────
|
|
147
|
+
if [ "$AZDO_MODE" = "rest" ]; then
|
|
148
|
+
BUILDS_URL="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/build/builds?branchName=refs/heads/$BRANCH&\$top=5&api-version=7.0"
|
|
149
|
+
|
|
150
|
+
# Wait for CI to start (max 2 min)
|
|
151
|
+
RUNS_FOUND=false
|
|
152
|
+
for _ in $(seq 1 8); do
|
|
153
|
+
RUN_COUNT=$(curl -s -H "$AUTH" "$BUILDS_URL" | jq '.value | length')
|
|
154
|
+
if [ -n "$RUN_COUNT" ] && [ "$RUN_COUNT" != "0" ]; then
|
|
155
|
+
RUNS_FOUND=true; break
|
|
156
|
+
fi
|
|
157
|
+
sleep 15
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
if [ "$RUNS_FOUND" = false ]; then
|
|
161
|
+
EVALS=$(curl -s -H "$AUTH" \
|
|
162
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
|
|
163
|
+
EVAL_COUNT=$(echo "$EVALS" | jq '.value | length')
|
|
164
|
+
if [ "$EVAL_COUNT" = "0" ] || [ -z "$EVAL_COUNT" ]; then
|
|
165
|
+
STATUS="no_checks"; CHECKS=""; FAILING="none"
|
|
166
|
+
emit_report; exit 0
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# Wait for runs with fail-fast (max 30 min)
|
|
171
|
+
for _ in $(seq 1 120); do
|
|
172
|
+
BUILDS_JSON=$(curl -s -H "$AUTH" "$BUILDS_URL")
|
|
173
|
+
|
|
174
|
+
FAIL_COUNT=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.result=="failed")] | length')
|
|
175
|
+
if [ "${FAIL_COUNT:-0}" -gt 0 ]; then break; fi
|
|
176
|
+
|
|
177
|
+
IN_PROGRESS=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.status=="inProgress")] | length')
|
|
178
|
+
if [ "$IN_PROGRESS" = "0" ]; then break; fi
|
|
179
|
+
sleep 15
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
# Final status
|
|
183
|
+
BUILDS_JSON=$(curl -s -H "$AUTH" "$BUILDS_URL")
|
|
184
|
+
CHECKS=$(echo "$BUILDS_JSON" | jq -r '.value[] | "\(.definition.name):\(.result // "pending")"' | paste -sd, -)
|
|
185
|
+
FAILING=$(echo "$BUILDS_JSON" | jq -r '.value[] | select(.result=="failed") | .definition.name' | paste -sd, -)
|
|
186
|
+
FAIL_COUNT=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.result=="failed")] | length')
|
|
187
|
+
STILL_RUNNING=$(echo "$BUILDS_JSON" | jq '[.value[] | select(.status=="inProgress")] | length')
|
|
188
|
+
|
|
189
|
+
# Check policy evaluations
|
|
190
|
+
EVALS=$(curl -s -H "$AUTH" \
|
|
191
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
|
|
192
|
+
REJECTED=$(echo "$EVALS" | jq '[.value[] | select(.status=="rejected")] | length')
|
|
193
|
+
|
|
194
|
+
if [ "${FAIL_COUNT:-0}" -gt 0 ] || [ "${REJECTED:-0}" -gt 0 ]; then
|
|
195
|
+
STATUS="some_failed"
|
|
196
|
+
emit_report; exit 1
|
|
197
|
+
elif [ "${STILL_RUNNING:-0}" -gt 0 ]; then
|
|
198
|
+
STATUS="pending"; FAILING="none"
|
|
199
|
+
emit_report; exit 2
|
|
200
|
+
else
|
|
201
|
+
STATUS="all_passed"; FAILING="none"
|
|
202
|
+
emit_report; exit 0
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# merge.sh — Merge a PR on GitHub or Azure DevOps
|
|
3
|
+
# Usage: merge.sh <PLATFORM> <PR_NUMBER> [--squash|--merge] [--delete-branch|--keep-branch] [azdo options]
|
|
4
|
+
# AzDO options: --azdo-mode=cli|rest --azdo-org-url=URL --azdo-project=NAME --azdo-project-url-safe=NAME
|
|
5
|
+
# Env: AZURE_DEVOPS_EXT_PAT or AZDO_PAT (required for azdo rest mode)
|
|
6
|
+
# Exit codes: 0=merged, 1=failed, 2=failed after retry
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
PLATFORM="${1:?Usage: merge.sh <PLATFORM> <PR_NUMBER> [flags]}"
|
|
10
|
+
PR_NUMBER="${2:?Usage: merge.sh <PLATFORM> <PR_NUMBER> [flags]}"
|
|
11
|
+
shift 2
|
|
12
|
+
|
|
13
|
+
SQUASH=true DELETE_BRANCH=true
|
|
14
|
+
AZDO_MODE="" AZDO_ORG_URL="" AZDO_PROJECT="" AZDO_PROJECT_URL_SAFE=""
|
|
15
|
+
|
|
16
|
+
for arg in "$@"; do
|
|
17
|
+
case "$arg" in
|
|
18
|
+
--merge) SQUASH=false ;;
|
|
19
|
+
--squash) SQUASH=true ;;
|
|
20
|
+
--keep-branch) DELETE_BRANCH=false ;;
|
|
21
|
+
--delete-branch) DELETE_BRANCH=true ;;
|
|
22
|
+
--azdo-mode=*) AZDO_MODE="${arg#*=}" ;;
|
|
23
|
+
--azdo-org-url=*) AZDO_ORG_URL="${arg#*=}" ;;
|
|
24
|
+
--azdo-project=*) AZDO_PROJECT="${arg#*=}" ;;
|
|
25
|
+
--azdo-project-url-safe=*) AZDO_PROJECT_URL_SAFE="${arg#*=}" ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
STATUS="" MERGE_COMMIT="" MERGE_TYPE="" BRANCH_DELETED="" ERRORS="none"
|
|
30
|
+
|
|
31
|
+
emit_report() {
|
|
32
|
+
echo "LAND_REPORT_BEGIN"
|
|
33
|
+
echo "status=$STATUS"
|
|
34
|
+
echo "merge_commit=$MERGE_COMMIT"
|
|
35
|
+
echo "merge_type=$MERGE_TYPE"
|
|
36
|
+
echo "branch_deleted=$BRANCH_DELETED"
|
|
37
|
+
echo "errors=$ERRORS"
|
|
38
|
+
echo "LAND_REPORT_END"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
MERGE_TYPE=$( [ "$SQUASH" = true ] && echo "squash" || echo "merge" )
|
|
42
|
+
|
|
43
|
+
# ── GitHub ──────────────────────────────────────────────
|
|
44
|
+
if [ "$PLATFORM" = "github" ]; then
|
|
45
|
+
GH_MERGE_FLAG=$( [ "$SQUASH" = true ] && echo "--squash" || echo "--merge" )
|
|
46
|
+
GH_BRANCH_FLAG=$( [ "$DELETE_BRANCH" = true ] && echo "--delete-branch" || echo "" )
|
|
47
|
+
|
|
48
|
+
if gh pr merge "$PR_NUMBER" $GH_MERGE_FLAG $GH_BRANCH_FLAG 2>/dev/null; then
|
|
49
|
+
STATUS="success"
|
|
50
|
+
MERGE_COMMIT=$(gh pr view "$PR_NUMBER" --json mergeCommit -q '.mergeCommit.oid' 2>/dev/null | head -c 7)
|
|
51
|
+
BRANCH_DELETED=$DELETE_BRANCH
|
|
52
|
+
else
|
|
53
|
+
STATUS="failed"
|
|
54
|
+
ERRORS="gh pr merge failed"
|
|
55
|
+
BRANCH_DELETED=false
|
|
56
|
+
fi
|
|
57
|
+
emit_report
|
|
58
|
+
[ "$STATUS" = "success" ] && exit 0 || exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# ── Azure DevOps ────────────────────────────────────────
|
|
62
|
+
PAT="${AZURE_DEVOPS_EXT_PAT:-${AZDO_PAT:-}}"
|
|
63
|
+
SQUASH_FLAG=$( [ "$SQUASH" = true ] && echo "true" || echo "false" )
|
|
64
|
+
DELETE_FLAG=$( [ "$DELETE_BRANCH" = true ] && echo "true" || echo "false" )
|
|
65
|
+
|
|
66
|
+
# ── AzDO CLI mode ──────────────────────────────────────
|
|
67
|
+
if [ "$AZDO_MODE" = "cli" ]; then
|
|
68
|
+
# Check for rejected policies
|
|
69
|
+
REJECTED=$(az repos pr policy list --id "$PR_NUMBER" \
|
|
70
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
71
|
+
--query "[?status=='rejected'] | length(@)" -o tsv 2>/dev/null)
|
|
72
|
+
if [ -n "$REJECTED" ] && [ "$REJECTED" != "0" ]; then
|
|
73
|
+
STATUS="failed"
|
|
74
|
+
ERRORS="$REJECTED PR policies are rejected"
|
|
75
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
76
|
+
emit_report; exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Complete the PR
|
|
80
|
+
if az repos pr update --id "$PR_NUMBER" --status completed \
|
|
81
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
82
|
+
--squash "$SQUASH_FLAG" \
|
|
83
|
+
--delete-source-branch "$DELETE_FLAG" 2>/dev/null; then
|
|
84
|
+
STATUS="success"
|
|
85
|
+
MERGE_COMMIT=$(git rev-parse --short HEAD 2>/dev/null)
|
|
86
|
+
BRANCH_DELETED=$DELETE_BRANCH
|
|
87
|
+
else
|
|
88
|
+
# Retry once after 30s (policies may still be evaluating)
|
|
89
|
+
sleep 30
|
|
90
|
+
if az repos pr update --id "$PR_NUMBER" --status completed \
|
|
91
|
+
--org "$AZDO_ORG_URL" --project "$AZDO_PROJECT" \
|
|
92
|
+
--squash "$SQUASH_FLAG" \
|
|
93
|
+
--delete-source-branch "$DELETE_FLAG" 2>/dev/null; then
|
|
94
|
+
STATUS="success"
|
|
95
|
+
MERGE_COMMIT=$(git rev-parse --short HEAD 2>/dev/null)
|
|
96
|
+
BRANCH_DELETED=$DELETE_BRANCH
|
|
97
|
+
else
|
|
98
|
+
STATUS="failed"
|
|
99
|
+
ERRORS="Merge failed after retry"
|
|
100
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
101
|
+
emit_report; exit 2
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
emit_report
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# ── AzDO REST mode ─────────────────────────────────────
|
|
109
|
+
if [ "$AZDO_MODE" = "rest" ]; then
|
|
110
|
+
AUTH="Authorization: Basic $(echo -n ":$PAT" | base64)"
|
|
111
|
+
REPO_NAME=$(basename -s .git "$(git remote get-url origin)")
|
|
112
|
+
PR_API="$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/git/repositories/$REPO_NAME/pullrequests/$PR_NUMBER"
|
|
113
|
+
|
|
114
|
+
# Check for rejected policies
|
|
115
|
+
EVALS=$(curl -s -H "$AUTH" \
|
|
116
|
+
"$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apis/policy/evaluations?artifactId=vstfs:///CodeReview/CodeReviewId/$AZDO_PROJECT_URL_SAFE/$PR_NUMBER&api-version=7.0")
|
|
117
|
+
REJECTED=$(echo "$EVALS" | jq '[.value[] | select(.status=="rejected")] | length')
|
|
118
|
+
if [ "$REJECTED" != "0" ]; then
|
|
119
|
+
STATUS="failed"
|
|
120
|
+
ERRORS="PR has rejected policy evaluations"
|
|
121
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
122
|
+
emit_report; exit 1
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Resolve merge strategy
|
|
126
|
+
MERGE_STRATEGY=$( [ "$SQUASH" = true ] && echo "squash" || echo "noFastForward" )
|
|
127
|
+
|
|
128
|
+
# Complete the PR
|
|
129
|
+
RESPONSE=$(curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
|
|
130
|
+
"$PR_API?api-version=7.0" \
|
|
131
|
+
-d "{\"status\": \"completed\", \"completionOptions\": {\"mergeStrategy\": \"$MERGE_STRATEGY\", \"deleteSourceBranch\": $DELETE_FLAG}}")
|
|
132
|
+
|
|
133
|
+
PR_STATUS=$(echo "$RESPONSE" | jq -r '.status // empty')
|
|
134
|
+
if [ "$PR_STATUS" = "completed" ]; then
|
|
135
|
+
STATUS="success"
|
|
136
|
+
MERGE_COMMIT=$(echo "$RESPONSE" | jq -r '.lastMergeCommit.commitId // empty' | head -c 7)
|
|
137
|
+
BRANCH_DELETED=$DELETE_BRANCH
|
|
138
|
+
else
|
|
139
|
+
STATUS="failed"
|
|
140
|
+
ERRORS="REST merge returned status: ${PR_STATUS:-unknown}"
|
|
141
|
+
MERGE_COMMIT=""; BRANCH_DELETED=false
|
|
142
|
+
emit_report; exit 1
|
|
143
|
+
fi
|
|
144
|
+
emit_report
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
"name": "
|
|
22
|
+
"name": "script-based-architecture",
|
|
23
23
|
"prompt": "Ship everything to production",
|
|
24
24
|
"assertions": [
|
|
25
25
|
{ "type": "contains", "value": "██" },
|
|
26
|
-
{ "type": "regex", "value": "
|
|
27
|
-
{ "type": "semantic", "value": "
|
|
26
|
+
{ "type": "regex", "value": "(script|sync\\.sh|ci-watch\\.sh|merge\\.sh|general.purpose)", "flags": "i" },
|
|
27
|
+
{ "type": "semantic", "value": "Uses deterministic scripts for sync, CI monitoring, and merge stages, with LLM subagents only for commit/PR authoring and CI fix analysis" }
|
|
28
28
|
]
|
|
29
29
|
}
|
|
30
30
|
]
|
package/skills/sync/SKILL.md
CHANGED
|
@@ -59,8 +59,9 @@ Status icons: ✅ done · ❌ failed · ⚠️ degraded · ⏳ working · ⏭️
|
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
62
|
-
Synchronize local repository with the remote default branch using a
|
|
63
|
-
|
|
62
|
+
Synchronize local repository with the remote default branch using a
|
|
63
|
+
deterministic bash script — no LLM subagent needed since all steps are
|
|
64
|
+
pure git commands.
|
|
64
65
|
|
|
65
66
|
## Flags
|
|
66
67
|
|
|
@@ -112,110 +113,29 @@ Pass `{REMOTE}` and `{DEFAULT_BRANCH}` into the subagent prompt.
|
|
|
112
113
|
|
|
113
114
|
## Execution
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
Run the sync script directly — no LLM subagent needed since all steps are
|
|
117
|
+
deterministic git commands:
|
|
116
118
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
```bash
|
|
120
|
+
# Resolve skill root (plugin install or direct install)
|
|
121
|
+
for SKILL_ROOT in \
|
|
122
|
+
"${CLAUDE_PLUGIN_ROOT:-}" \
|
|
123
|
+
"$HOME/.claude/plugins/marketplaces/slamb2k" \
|
|
124
|
+
"$(dirname "$(readlink -f "$0")")/.."
|
|
125
|
+
do
|
|
126
|
+
[ -f "$SKILL_ROOT/skills/sync/scripts/sync.sh" ] && break
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
bash "$SKILL_ROOT/skills/sync/scripts/sync.sh" \
|
|
130
|
+
"{REMOTE}" "{DEFAULT_BRANCH}" {FLAGS}
|
|
124
131
|
```
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
Parse the output between `SYNC_REPORT_BEGIN` and `SYNC_REPORT_END` markers.
|
|
134
|
+
Extract `key=value` pairs for the report fields: `status`, `remote`,
|
|
135
|
+
`default_branch`, `main_updated_to`, `current_branch`, `stash`, `rebase`,
|
|
136
|
+
`branches_cleaned`, `errors`.
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
Synchronize this git repository with {REMOTE}/{DEFAULT_BRANCH}. Execute the
|
|
130
|
-
following steps in order and report results.
|
|
131
|
-
|
|
132
|
-
Limit SYNC_REPORT to 15 lines maximum.
|
|
133
|
-
|
|
134
|
-
FLAGS: {flags from request, or "none"}
|
|
135
|
-
|
|
136
|
-
## Steps
|
|
137
|
-
|
|
138
|
-
1. **Check state**
|
|
139
|
-
BRANCH=$(git branch --show-current 2>/dev/null || echo "DETACHED")
|
|
140
|
-
CHANGES=$(git status --porcelain | head -20)
|
|
141
|
-
Record: current_branch=$BRANCH, has_changes=(non-empty CHANGES)
|
|
142
|
-
|
|
143
|
-
If BRANCH == "DETACHED":
|
|
144
|
-
Record error: "Detached HEAD — cannot sync. Checkout a branch first."
|
|
145
|
-
Skip to Output.
|
|
146
|
-
|
|
147
|
-
2. **Stash changes** (skip if --no-stash or no changes)
|
|
148
|
-
If has_changes and not --no-stash:
|
|
149
|
-
git stash push -m "sync-auto-stash-$(date +%Y%m%d-%H%M%S)"
|
|
150
|
-
Record: stash_created=true
|
|
151
|
-
|
|
152
|
-
3. **Sync {DEFAULT_BRANCH}**
|
|
153
|
-
git fetch {REMOTE}
|
|
154
|
-
If BRANCH != "{DEFAULT_BRANCH}":
|
|
155
|
-
git checkout {DEFAULT_BRANCH}
|
|
156
|
-
git pull {REMOTE} {DEFAULT_BRANCH} --ff-only
|
|
157
|
-
If pull fails (diverged):
|
|
158
|
-
git pull {REMOTE} {DEFAULT_BRANCH} --rebase
|
|
159
|
-
Record: main_commit=$(git rev-parse --short HEAD)
|
|
160
|
-
Record: main_message=$(git log -1 --format=%s)
|
|
161
|
-
|
|
162
|
-
4. **Return to branch and update** (skip if already on {DEFAULT_BRANCH})
|
|
163
|
-
If current_branch != "{DEFAULT_BRANCH}":
|
|
164
|
-
git checkout $BRANCH
|
|
165
|
-
If --no-rebase:
|
|
166
|
-
git merge {DEFAULT_BRANCH} --no-edit
|
|
167
|
-
Else:
|
|
168
|
-
git rebase {DEFAULT_BRANCH}
|
|
169
|
-
If rebase fails:
|
|
170
|
-
git rebase --abort
|
|
171
|
-
Record: rebase_status="conflict — aborted, branch unchanged"
|
|
172
|
-
|
|
173
|
-
5. **Restore stash** (if created in step 2)
|
|
174
|
-
If stash_created:
|
|
175
|
-
git stash pop
|
|
176
|
-
If pop fails (conflict):
|
|
177
|
-
Record: stash="conflict — run 'git stash show' to inspect"
|
|
178
|
-
Else:
|
|
179
|
-
Record: stash="restored"
|
|
180
|
-
|
|
181
|
-
6. **Cleanup branches** (skip if --no-cleanup)
|
|
182
|
-
git fetch --prune
|
|
183
|
-
|
|
184
|
-
# Delete branches whose remote is gone
|
|
185
|
-
for branch in $(git branch -vv | grep ': gone]' | awk '{print $1}'); do
|
|
186
|
-
if [ "$branch" != "$BRANCH" ]; then
|
|
187
|
-
git branch -d "$branch" 2>/dev/null && echo "Deleted: $branch"
|
|
188
|
-
fi
|
|
189
|
-
done
|
|
190
|
-
|
|
191
|
-
# Delete branches fully merged into {DEFAULT_BRANCH} (except current)
|
|
192
|
-
for branch in $(git branch --merged {DEFAULT_BRANCH} | grep -v '^\*' | grep -v '{DEFAULT_BRANCH}'); do
|
|
193
|
-
branch=$(echo "$branch" | xargs)
|
|
194
|
-
if [ "$branch" != "$BRANCH" ] && [ -n "$branch" ]; then
|
|
195
|
-
git branch -d "$branch" 2>/dev/null && echo "Deleted: $branch"
|
|
196
|
-
fi
|
|
197
|
-
done
|
|
198
|
-
|
|
199
|
-
Record: branches_cleaned={list of deleted branches, or "none"}
|
|
200
|
-
|
|
201
|
-
7. **Final state**
|
|
202
|
-
echo "Branch: $(git branch --show-current)"
|
|
203
|
-
echo "HEAD: $(git log -1 --format='%h %s')"
|
|
204
|
-
echo "Status: $(git status --short | wc -l) modified files"
|
|
205
|
-
|
|
206
|
-
## Output Format
|
|
207
|
-
|
|
208
|
-
SYNC_REPORT:
|
|
209
|
-
- status: success|failed
|
|
210
|
-
- remote: {REMOTE}
|
|
211
|
-
- default_branch: {DEFAULT_BRANCH}
|
|
212
|
-
- main_updated_to: {commit} - {message}
|
|
213
|
-
- current_branch: {branch}
|
|
214
|
-
- stash: restored|none|conflict
|
|
215
|
-
- rebase: success|conflict|skipped
|
|
216
|
-
- branches_cleaned: {list or "none"}
|
|
217
|
-
- errors: {any errors encountered}
|
|
218
|
-
```
|
|
138
|
+
Exit codes: 0=success, 1=fatal error, 2=partial success (conflict warnings).
|
|
219
139
|
|
|
220
140
|
---
|
|
221
141
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sync.sh — Deterministic repo sync with origin/default-branch
|
|
3
|
+
# Usage: sync.sh <REMOTE> <DEFAULT_BRANCH> [--no-stash] [--no-cleanup] [--no-rebase]
|
|
4
|
+
# Output: Key-value SYNC_REPORT between BEGIN/END markers on stdout
|
|
5
|
+
# Exit codes: 0=success, 1=fatal, 2=partial (conflict warnings)
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
REMOTE="${1:?Usage: sync.sh <REMOTE> <DEFAULT_BRANCH> [flags]}"
|
|
9
|
+
DEFAULT_BRANCH="${2:?Usage: sync.sh <REMOTE> <DEFAULT_BRANCH> [flags]}"
|
|
10
|
+
shift 2
|
|
11
|
+
|
|
12
|
+
NO_STASH=false; NO_CLEANUP=false; NO_REBASE=false
|
|
13
|
+
for arg in "$@"; do
|
|
14
|
+
case "$arg" in
|
|
15
|
+
--no-stash) NO_STASH=true ;;
|
|
16
|
+
--no-cleanup) NO_CLEANUP=true ;;
|
|
17
|
+
--no-rebase) NO_REBASE=true ;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Report fields
|
|
22
|
+
STATUS="success"
|
|
23
|
+
MAIN_UPDATED_TO=""
|
|
24
|
+
CURRENT_BRANCH=""
|
|
25
|
+
STASH_STATUS="none"
|
|
26
|
+
REBASE_STATUS="skipped"
|
|
27
|
+
BRANCHES_CLEANED="none"
|
|
28
|
+
ERRORS="none"
|
|
29
|
+
STASH_CREATED=false
|
|
30
|
+
EXIT_CODE=0
|
|
31
|
+
|
|
32
|
+
emit_report() {
|
|
33
|
+
echo "SYNC_REPORT_BEGIN"
|
|
34
|
+
echo "status=$STATUS"
|
|
35
|
+
echo "remote=$REMOTE"
|
|
36
|
+
echo "default_branch=$DEFAULT_BRANCH"
|
|
37
|
+
echo "main_updated_to=$MAIN_UPDATED_TO"
|
|
38
|
+
echo "current_branch=$CURRENT_BRANCH"
|
|
39
|
+
echo "stash=$STASH_STATUS"
|
|
40
|
+
echo "rebase=$REBASE_STATUS"
|
|
41
|
+
echo "branches_cleaned=$BRANCHES_CLEANED"
|
|
42
|
+
echo "errors=$ERRORS"
|
|
43
|
+
echo "SYNC_REPORT_END"
|
|
44
|
+
exit "$EXIT_CODE"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Step 1: Check state
|
|
48
|
+
BRANCH=$(git branch --show-current 2>/dev/null || echo "DETACHED")
|
|
49
|
+
CURRENT_BRANCH="$BRANCH"
|
|
50
|
+
|
|
51
|
+
if [ "$BRANCH" = "DETACHED" ]; then
|
|
52
|
+
STATUS="failed"
|
|
53
|
+
ERRORS="Detached HEAD — cannot sync. Checkout a branch first."
|
|
54
|
+
EXIT_CODE=1
|
|
55
|
+
emit_report
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
HAS_CHANGES=false
|
|
59
|
+
if [ -n "$(git status --porcelain 2>/dev/null | head -1)" ]; then
|
|
60
|
+
HAS_CHANGES=true
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Step 2: Stash changes
|
|
64
|
+
if [ "$HAS_CHANGES" = true ] && [ "$NO_STASH" = false ]; then
|
|
65
|
+
if git stash push -m "sync-auto-stash-$(date +%Y%m%d-%H%M%S)" 2>/dev/null; then
|
|
66
|
+
STASH_CREATED=true
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Step 3: Sync default branch
|
|
71
|
+
git fetch "$REMOTE" 2>/dev/null
|
|
72
|
+
|
|
73
|
+
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
|
|
74
|
+
git checkout "$DEFAULT_BRANCH" 2>/dev/null
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if ! git pull "$REMOTE" "$DEFAULT_BRANCH" --ff-only 2>/dev/null; then
|
|
78
|
+
if ! git pull "$REMOTE" "$DEFAULT_BRANCH" --rebase 2>/dev/null; then
|
|
79
|
+
STATUS="failed"
|
|
80
|
+
ERRORS="Failed to pull $REMOTE/$DEFAULT_BRANCH"
|
|
81
|
+
EXIT_CODE=1
|
|
82
|
+
# Try to get back to original branch
|
|
83
|
+
[ "$BRANCH" != "$DEFAULT_BRANCH" ] && git checkout "$BRANCH" 2>/dev/null
|
|
84
|
+
# Restore stash if we created one
|
|
85
|
+
[ "$STASH_CREATED" = true ] && git stash pop 2>/dev/null
|
|
86
|
+
emit_report
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
MAIN_COMMIT=$(git rev-parse --short HEAD 2>/dev/null)
|
|
91
|
+
MAIN_MESSAGE=$(git log -1 --format=%s 2>/dev/null)
|
|
92
|
+
MAIN_UPDATED_TO="$MAIN_COMMIT - $MAIN_MESSAGE"
|
|
93
|
+
|
|
94
|
+
# Step 4: Return to branch and update
|
|
95
|
+
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
|
|
96
|
+
git checkout "$BRANCH" 2>/dev/null
|
|
97
|
+
|
|
98
|
+
if [ "$NO_REBASE" = true ]; then
|
|
99
|
+
if ! git merge "$DEFAULT_BRANCH" --no-edit 2>/dev/null; then
|
|
100
|
+
REBASE_STATUS="conflict — merge aborted"
|
|
101
|
+
EXIT_CODE=2
|
|
102
|
+
else
|
|
103
|
+
REBASE_STATUS="success"
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
if ! git rebase "$DEFAULT_BRANCH" 2>/dev/null; then
|
|
107
|
+
git rebase --abort 2>/dev/null
|
|
108
|
+
REBASE_STATUS="conflict — aborted, branch unchanged"
|
|
109
|
+
EXIT_CODE=2
|
|
110
|
+
else
|
|
111
|
+
REBASE_STATUS="success"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
117
|
+
|
|
118
|
+
# Step 5: Restore stash
|
|
119
|
+
if [ "$STASH_CREATED" = true ]; then
|
|
120
|
+
if git stash pop 2>/dev/null; then
|
|
121
|
+
STASH_STATUS="restored"
|
|
122
|
+
else
|
|
123
|
+
STASH_STATUS="conflict — run 'git stash show' to inspect"
|
|
124
|
+
EXIT_CODE=2
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Step 6: Cleanup branches
|
|
129
|
+
if [ "$NO_CLEANUP" = false ]; then
|
|
130
|
+
git fetch --prune 2>/dev/null
|
|
131
|
+
|
|
132
|
+
CLEANED=()
|
|
133
|
+
|
|
134
|
+
# Delete branches whose remote is gone
|
|
135
|
+
while IFS= read -r b; do
|
|
136
|
+
[ -z "$b" ] && continue
|
|
137
|
+
[ "$b" = "$CURRENT_BRANCH" ] && continue
|
|
138
|
+
if git branch -d "$b" 2>/dev/null; then
|
|
139
|
+
CLEANED+=("$b")
|
|
140
|
+
fi
|
|
141
|
+
done < <(git branch -vv 2>/dev/null | grep ': gone]' | awk '{print $1}')
|
|
142
|
+
|
|
143
|
+
# Delete branches fully merged into default branch
|
|
144
|
+
while IFS= read -r b; do
|
|
145
|
+
b=$(echo "$b" | xargs)
|
|
146
|
+
[ -z "$b" ] && continue
|
|
147
|
+
[ "$b" = "$CURRENT_BRANCH" ] && continue
|
|
148
|
+
if git branch -d "$b" 2>/dev/null; then
|
|
149
|
+
CLEANED+=("$b")
|
|
150
|
+
fi
|
|
151
|
+
done < <(git branch --merged "$DEFAULT_BRANCH" 2>/dev/null | grep -v '^\*' | grep -v "$DEFAULT_BRANCH")
|
|
152
|
+
|
|
153
|
+
if [ ${#CLEANED[@]} -gt 0 ]; then
|
|
154
|
+
BRANCHES_CLEANED=$(IFS=,; echo "${CLEANED[*]}")
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
emit_report
|