@cxi-lmai/ci-agent-platform 3.0.1 → 3.1.1
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/README.md +12 -4
- package/package.json +2 -2
- package/payload/INSTALL.md +8 -2
- package/payload/agents/agent-architect.md +1 -1
- package/payload/agents/code-reviewer.md +1 -1
- package/payload/agents/codebase-auditor.md +1 -1
- package/payload/agents/coder.md +1 -1
- package/payload/agents/decomposer.md +1 -1
- package/payload/agents/docs-sync.md +1 -1
- package/payload/agents/e2e-test-writer.md +1 -1
- package/payload/agents/performance-reviewer.md +1 -1
- package/payload/agents/release-mr.md +1 -1
- package/payload/agents/security-reviewer.md +1 -1
- package/payload/agents/test-fix.md +1 -1
- package/payload/agents/test-writer.md +1 -1
- package/payload/agents-omp/agent-architect.md +1 -1
- package/payload/agents-omp/code-reviewer.md +1 -1
- package/payload/agents-omp/codebase-auditor.md +1 -1
- package/payload/agents-omp/coder.md +1 -1
- package/payload/agents-omp/decomposer.md +1 -1
- package/payload/agents-omp/docs-sync.md +1 -1
- package/payload/agents-omp/e2e-test-writer.md +1 -1
- package/payload/agents-omp/migration-reviewer.md +1 -1
- package/payload/agents-omp/orchestrator.md +1 -1
- package/payload/agents-omp/performance-reviewer.md +1 -1
- package/payload/agents-omp/postmortem.md +1 -1
- package/payload/agents-omp/release-mr.md +1 -1
- package/payload/agents-omp/security-reviewer.md +1 -1
- package/payload/agents-omp/test-fix.md +1 -1
- package/payload/agents-omp/test-writer.md +1 -1
- package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +210 -0
- package/payload/ci-templates/github/claude-issue-pipeline.yml +1 -1
- package/payload/ci-templates/github/claude-pipeline.yml +2 -2
- package/payload/ci-templates/github/claude-test-fix.yml +1 -1
- package/payload/ci-templates/scripts/agent-architect.sh +233 -0
- package/payload/ci-templates/scripts/code.sh +26 -27
- package/payload/ci-templates/scripts/codebase-audit.sh +252 -0
- package/payload/ci-templates/scripts/coverage-ratchet.sh +77 -0
- package/payload/ci-templates/scripts/cve-fix.sh +246 -0
- package/payload/ci-templates/scripts/docs-sync.sh +225 -0
- package/payload/ci-templates/scripts/e2e-test-gen.sh +201 -0
- package/payload/ci-templates/scripts/lib/failure-notice.sh +104 -0
- package/payload/ci-templates/scripts/lib/issue-loop.sh +155 -40
- package/payload/ci-templates/scripts/lib/pipeline-common.sh +100 -26
- package/payload/ci-templates/scripts/lib/platform.sh +270 -13
- package/payload/ci-templates/scripts/lib/usage-capture-omp.sh +7 -1
- package/payload/ci-templates/scripts/lib/usage-capture.sh +7 -1
- package/payload/ci-templates/scripts/metrics-snapshot.sh +377 -0
- package/payload/ci-templates/scripts/orchestrate.sh +55 -37
- package/payload/ci-templates/scripts/postmortem.sh +10 -0
- package/payload/ci-templates/scripts/review-fix.sh +22 -11
- package/payload/ci-templates/scripts/review.sh +11 -7
- package/payload/ci-templates/scripts/test-fix.sh +7 -0
- package/payload/skills/agent-architect/SKILL.md +45 -0
- package/payload/skills/codebase-audit/SKILL.md +84 -0
- package/payload/skills/cve-fix/SKILL.md +98 -0
- package/payload/skills/docs-sync/SKILL.md +74 -0
- package/payload/skills/e2e-test-gen/SKILL.md +65 -0
- package/payload/skills/fix-review-findings/SKILL.md +2 -2
- package/payload/skills/fix-tests/SKILL.md +2 -2
- package/payload/templates/memory-index.template.md +34 -0
- package/payload/templates/pipeline-config.template.md +11 -1
- package/payload/templates/review_suppressions.template.md +55 -0
- package/payload/templates/spec-issue.template.md +39 -7
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Opt-in coverage gate. Reads the configured coverage report, compares it with
|
|
3
|
+
# the target branch's latest successful pipeline, and sends coverage drops on
|
|
4
|
+
# wip merge requests into the existing test-fix loop.
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
# shellcheck source=lib/pipeline-common.sh
|
|
9
|
+
source "$SCRIPT_DIR/lib/pipeline-common.sh"
|
|
10
|
+
pipe_defaults
|
|
11
|
+
|
|
12
|
+
coverage_from_jacoco() {
|
|
13
|
+
local covered missed
|
|
14
|
+
if ! covered=$(xmllint --xpath 'string(//report/counter[@type="INSTRUCTION"]/@covered)' \
|
|
15
|
+
"$PIPE_COVERAGE_REPORT" 2>/dev/null) ||
|
|
16
|
+
! missed=$(xmllint --xpath 'string(//report/counter[@type="INSTRUCTION"]/@missed)' \
|
|
17
|
+
"$PIPE_COVERAGE_REPORT" 2>/dev/null); then
|
|
18
|
+
echo "ERROR: unable to parse JaCoCo instruction coverage from: $PIPE_COVERAGE_REPORT" >&2
|
|
19
|
+
return 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [[ "$covered" =~ ^[0-9]+$ ]] && [[ "$missed" =~ ^[0-9]+$ ]] && \
|
|
23
|
+
[ "$((covered + missed))" -gt 0 ]; then
|
|
24
|
+
printf '%s\n' "$((covered * 100 / (covered + missed)))"
|
|
25
|
+
else
|
|
26
|
+
echo "ERROR: unable to parse JaCoCo instruction coverage from: $PIPE_COVERAGE_REPORT" >&2
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case "$PIPE_COVERAGE_REPORT_KIND" in
|
|
32
|
+
jacoco) ;;
|
|
33
|
+
*) echo "ERROR: unsupported coverage report kind: $PIPE_COVERAGE_REPORT_KIND" >&2; exit 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
[ -z "${PIPE_MR_IID:-}" ] && exit 0
|
|
37
|
+
|
|
38
|
+
if [ -z "$PIPE_COVERAGE_REPORT" ] || [ ! -f "$PIPE_COVERAGE_REPORT" ]; then
|
|
39
|
+
echo "Coverage ratchet inactive: coverage report is not available: ${PIPE_COVERAGE_REPORT:-<unset>}"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
command -v xmllint >/dev/null 2>&1 || {
|
|
44
|
+
echo "ERROR: xmllint is required to parse PIPE_COVERAGE_REPORT" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
CURRENT_COV=$(coverage_from_jacoco)
|
|
49
|
+
|
|
50
|
+
if [ "$PIPE_COVERAGE_RATCHET" = "1" ] && [ -z "$PIPE_COVERAGE_SIGNAL" ]; then
|
|
51
|
+
echo "ERROR: PIPE_COVERAGE_SIGNAL must be set when PIPE_COVERAGE_RATCHET=1" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
BASELINE=$(platform_latest_success_coverage "$PIPE_TARGET_BRANCH" 2>/dev/null || true)
|
|
56
|
+
BASELINE=${BASELINE%%.*}
|
|
57
|
+
[[ "$BASELINE" =~ ^[0-9]+$ ]] || BASELINE=0
|
|
58
|
+
|
|
59
|
+
echo "Total coverage: ${CURRENT_COV}%"
|
|
60
|
+
echo "Coverage: current=${CURRENT_COV}% baseline=${BASELINE}%"
|
|
61
|
+
|
|
62
|
+
if [ "$CURRENT_COV" -lt "$BASELINE" ]; then
|
|
63
|
+
echo "WARNING: Coverage dropped from ${BASELINE}% to ${CURRENT_COV}%"
|
|
64
|
+
COMMENT_FILE="$PIPE_CONTEXT_DIR/coverage-ratchet.md"
|
|
65
|
+
printf '**Coverage dropped** from %s%% to %s%%. Please check whether new code is missing tests.\n' \
|
|
66
|
+
"$BASELINE" "$CURRENT_COV" > "$COMMENT_FILE"
|
|
67
|
+
platform_post_comment_file "$COMMENT_FILE" || true
|
|
68
|
+
|
|
69
|
+
LABELS=$(platform_mr_labels)
|
|
70
|
+
case ",$LABELS," in
|
|
71
|
+
*",$PIPE_LABEL_WIP,"*)
|
|
72
|
+
printf 'COVERAGE_DROP:%s:%s\n' "$BASELINE" "$CURRENT_COV" > "$PIPE_COVERAGE_SIGNAL"
|
|
73
|
+
echo "Coverage dropped on $PIPE_LABEL_WIP MR/PR — exiting 1 to trigger test-fix"
|
|
74
|
+
exit 1
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
fi
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Runner for the opt-in CVE remediation job. A project-local scanner supplies
|
|
3
|
+
# HIGH/CRITICAL identifiers in PIPE_CVE_LIST; this runner scopes the coder,
|
|
4
|
+
# verifies its commit and compile result, and alone owns rebase/push/comments.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
# shellcheck source=lib/pipeline-common.sh
|
|
9
|
+
source "$SCRIPT_DIR/lib/pipeline-common.sh"
|
|
10
|
+
pipe_defaults
|
|
11
|
+
|
|
12
|
+
CONTEXT_FILE="$PIPE_CONTEXT_DIR/cve-fix-context.md"
|
|
13
|
+
CVE_LIST_FILE="$PIPE_CONTEXT_DIR/cve-fix-list.txt"
|
|
14
|
+
MANIFEST_FILE="$PIPE_CONTEXT_DIR/cve-fix-manifest.txt"
|
|
15
|
+
CVE_ENV="$PIPE_CONTEXT_DIR/cve-fix.env"
|
|
16
|
+
COMMENT_FILE="$PIPE_CONTEXT_DIR/cve-fix-comment.md"
|
|
17
|
+
|
|
18
|
+
# The scanner controls both selection and severity filtering. Bound its output
|
|
19
|
+
# before it reaches a context or comment, and never forward it in argv/env.
|
|
20
|
+
head -c 8000 <<< "${PIPE_CVE_LIST:-}" > "$CVE_LIST_FILE"
|
|
21
|
+
# A cap can cut the final list entry mid-line. Add only a record terminator
|
|
22
|
+
# before filtering/counting; the caller-provided content remains bounded.
|
|
23
|
+
printf '\n' >> "$CVE_LIST_FILE"
|
|
24
|
+
sed -i '/^[[:space:]]*$/d' "$CVE_LIST_FILE"
|
|
25
|
+
CVE_COUNT=$(awk 'NF { count++ } END { print count + 0 }' "$CVE_LIST_FILE")
|
|
26
|
+
|
|
27
|
+
write_marker() {
|
|
28
|
+
# Marker values are canonical runner output. Never source model-written data.
|
|
29
|
+
printf 'CVE_FIX_APPLIED=%s\nCVE_COMPILED=%s\nCVE_COUNT=%s\nCVE_MANUAL_REQUIRED=%s\n' \
|
|
30
|
+
"$1" "$2" "$CVE_COUNT" "$3" > "$CVE_ENV"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
post_comment() {
|
|
34
|
+
# Posts the complete, bounded body already written to $COMMENT_FILE.
|
|
35
|
+
platform_post_comment_file "$COMMENT_FILE" || pipe_log "Could not post CVE remediation comment"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
post_manual_comment() {
|
|
39
|
+
# $1 is the short manual-action explanation. The list is bounded above.
|
|
40
|
+
{
|
|
41
|
+
printf '**Automated CVE remediation requires manual action.**\n\n%s\n' "$1"
|
|
42
|
+
if [ -s "$CVE_LIST_FILE" ]; then
|
|
43
|
+
printf '\nAffected HIGH/CRITICAL vulnerability identifiers:\n'
|
|
44
|
+
sed 's/^/- /' "$CVE_LIST_FILE"
|
|
45
|
+
fi
|
|
46
|
+
} > "$COMMENT_FILE"
|
|
47
|
+
post_comment
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
count_matching_commits() {
|
|
51
|
+
local count=0 subject
|
|
52
|
+
while IFS= read -r subject; do
|
|
53
|
+
[ "$subject" = "$PIPE_COMMIT_CVEFIX" ] && count=$((count + 1))
|
|
54
|
+
done < <(git log "origin/$PIPE_TARGET_BRANCH..HEAD" --format='%s' 2>/dev/null)
|
|
55
|
+
printf '%s\n' "$count"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
marker_is_valid() {
|
|
59
|
+
local line applied=0 compiled=0 count=0 manual=0
|
|
60
|
+
[ -f "$CVE_ENV" ] || return 1
|
|
61
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
62
|
+
case "$line" in
|
|
63
|
+
CVE_FIX_APPLIED=0|CVE_FIX_APPLIED=1) applied=$((applied + 1)) ;;
|
|
64
|
+
CVE_COMPILED=0|CVE_COMPILED=1) compiled=$((compiled + 1)) ;;
|
|
65
|
+
CVE_MANUAL_REQUIRED=0|CVE_MANUAL_REQUIRED=1) manual=$((manual + 1)) ;;
|
|
66
|
+
CVE_COUNT=*)
|
|
67
|
+
[[ "$line" =~ ^CVE_COUNT=[0-9]+$ ]] || return 1
|
|
68
|
+
count=$((count + 1))
|
|
69
|
+
;;
|
|
70
|
+
*) return 1 ;;
|
|
71
|
+
esac
|
|
72
|
+
done < "$CVE_ENV"
|
|
73
|
+
[ "$applied" -eq 1 ] && [ "$compiled" -eq 1 ] && [ "$count" -eq 1 ] && [ "$manual" -eq 1 ]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
marker_value() {
|
|
77
|
+
# marker_is_valid has already limited this to a literal 0, 1, or decimal.
|
|
78
|
+
pipe_get_env "$CVE_ENV" "$1"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# This job has no purpose outside a merge/pull-request pipeline. Do not attempt
|
|
82
|
+
# a forge lookup that can accidentally turn a branch pipeline into a comment.
|
|
83
|
+
if [ -z "${PIPE_MR_IID:-}" ]; then
|
|
84
|
+
pipe_log "No merge request/pull request context; CVE remediation is inactive"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Empty scanner output is normal and must remain silent. Check it before any
|
|
89
|
+
# metadata/WIP gate so enabling the optional job does not spam ordinary MRs.
|
|
90
|
+
if [ "$CVE_COUNT" = "0" ]; then
|
|
91
|
+
pipe_log "No vulnerability identifiers supplied; CVE remediation is inactive"
|
|
92
|
+
write_marker 0 0 1
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# The list was supplied by the caller and can be arbitrarily large. The only
|
|
97
|
+
# bounded copy is the context file above; do not let the dispatcher inherit it.
|
|
98
|
+
unset PIPE_CVE_LIST
|
|
99
|
+
|
|
100
|
+
META=$(platform_mr_meta 2>/dev/null || true)
|
|
101
|
+
MR_LABELS=$(jq -r '.labels // ""' <<< "$META" 2>/dev/null || true)
|
|
102
|
+
META_SOURCE_BRANCH=$(jq -r '.source_branch // ""' <<< "$META" 2>/dev/null || true)
|
|
103
|
+
if [ -n "$META_SOURCE_BRANCH" ]; then
|
|
104
|
+
PIPE_SOURCE_BRANCH="$META_SOURCE_BRANCH"
|
|
105
|
+
export PIPE_SOURCE_BRANCH
|
|
106
|
+
fi
|
|
107
|
+
if [ -z "${PIPE_SOURCE_BRANCH:-}" ]; then
|
|
108
|
+
pipe_log "MR/PR metadata has no source branch"
|
|
109
|
+
post_manual_comment "The source branch metadata is unavailable; update the dependencies manually."
|
|
110
|
+
write_marker 0 0 1
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if ! grep -qF "$PIPE_LABEL_WIP" <<< "$MR_LABELS"; then
|
|
115
|
+
pipe_log "MR/PR is not labeled $PIPE_LABEL_WIP; requesting a manual dependency update"
|
|
116
|
+
post_manual_comment "This MR/PR is not marked for autonomous changes; update the dependencies manually."
|
|
117
|
+
write_marker 0 0 1
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# A dependency manifest is project-relative by contract. Reject paths that
|
|
122
|
+
# could point outside this checkout before using them in git or file commands.
|
|
123
|
+
case "$PIPE_DEPENDENCY_MANIFEST" in
|
|
124
|
+
''|/*|..|../*|*/..|*/../*)
|
|
125
|
+
pipe_log "Invalid project-relative dependency manifest path: $PIPE_DEPENDENCY_MANIFEST"
|
|
126
|
+
post_manual_comment "The configured dependency manifest path is invalid; update the dependencies manually."
|
|
127
|
+
write_marker 0 0 1
|
|
128
|
+
exit 0
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
pipe_git_identity
|
|
133
|
+
pipe_checkout_source
|
|
134
|
+
if ! git rev-parse --verify "origin/$PIPE_SOURCE_BRANCH" >/dev/null 2>&1 \
|
|
135
|
+
|| [ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" != "$PIPE_SOURCE_BRANCH" ]; then
|
|
136
|
+
pipe_log "Unable to check out source branch safely: $PIPE_SOURCE_BRANCH"
|
|
137
|
+
post_manual_comment "The source branch could not be checked out safely; update the dependencies manually."
|
|
138
|
+
write_marker 0 0 1
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
EXISTING_FIXES=$(count_matching_commits)
|
|
143
|
+
if [ "$EXISTING_FIXES" -ge "$PIPE_FIX_LOOP_CAP" ]; then
|
|
144
|
+
pipe_log "CVE remediation reached its commit budget ($EXISTING_FIXES/$PIPE_FIX_LOOP_CAP)"
|
|
145
|
+
post_manual_comment "The CVE remediation commit budget is exhausted; update the dependencies manually."
|
|
146
|
+
write_marker 0 0 1
|
|
147
|
+
exit 0
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if [ ! -f "$PIPE_DEPENDENCY_MANIFEST" ]; then
|
|
151
|
+
pipe_log "Dependency manifest is missing: $PIPE_DEPENDENCY_MANIFEST"
|
|
152
|
+
post_manual_comment "Dependency manifest is missing: \`$PIPE_DEPENDENCY_MANIFEST\`. Update the dependencies manually."
|
|
153
|
+
write_marker 0 0 1
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
head -c 8000 "$PIPE_DEPENDENCY_MANIFEST" > "$MANIFEST_FILE"
|
|
157
|
+
REMAINING_BUDGET=$((PIPE_FIX_LOOP_CAP - EXISTING_FIXES))
|
|
158
|
+
rm -f "$CVE_ENV"
|
|
159
|
+
{
|
|
160
|
+
printf '# CVE remediation context\n\n'
|
|
161
|
+
printf 'Mode: apply\n'
|
|
162
|
+
printf 'Vulnerability List File: %s\n' "$CVE_LIST_FILE"
|
|
163
|
+
printf 'Dependency Manifest Path: %s\n' "$PIPE_DEPENDENCY_MANIFEST"
|
|
164
|
+
printf 'Dependency Manifest Content File: %s\n' "$MANIFEST_FILE"
|
|
165
|
+
printf 'Compile Command: %s\n' "$PIPE_COMPILE_CMD"
|
|
166
|
+
printf 'Commit Subject: %s\n' "$PIPE_COMMIT_CVEFIX"
|
|
167
|
+
printf 'Source Branch: %s\n' "$PIPE_SOURCE_BRANCH"
|
|
168
|
+
printf 'Target Branch: %s\n' "$PIPE_TARGET_BRANCH"
|
|
169
|
+
printf 'Suppression Path: %s\n' "$PIPE_CVE_SUPPRESSIONS"
|
|
170
|
+
printf 'Remaining Commit Budget: %s\n' "$REMAINING_BUDGET"
|
|
171
|
+
printf 'Marker File: %s\n' "$CVE_ENV"
|
|
172
|
+
} > "$CONTEXT_FILE"
|
|
173
|
+
|
|
174
|
+
# The skill decides dependency versions. This is the same code-writing policy
|
|
175
|
+
# as other runners; it receives no shell-built remediation prompt.
|
|
176
|
+
PRE_AGENT_SHA=$(git rev-parse HEAD)
|
|
177
|
+
pipe_run_agent cve-fix "/cve-fix" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_CODE" < /dev/null
|
|
178
|
+
POST_AGENT_SHA=$(git rev-parse HEAD)
|
|
179
|
+
NEW_FIXES=$(count_matching_commits)
|
|
180
|
+
RANGE_COMMITS=$(git rev-list --count "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null || echo 0)
|
|
181
|
+
RANGE_HAS_DIFF=false
|
|
182
|
+
if ! git diff --quiet "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null; then
|
|
183
|
+
RANGE_HAS_DIFF=true
|
|
184
|
+
fi
|
|
185
|
+
RANGE_MANIFEST_ONLY=true
|
|
186
|
+
while IFS= read -r path; do
|
|
187
|
+
[ "$path" = "$PIPE_DEPENDENCY_MANIFEST" ] || { RANGE_MANIFEST_ONLY=false; break; }
|
|
188
|
+
done < <(git diff --name-only "$PRE_AGENT_SHA..$POST_AGENT_SHA" 2>/dev/null)
|
|
189
|
+
|
|
190
|
+
if ! marker_is_valid || [ "$(marker_value CVE_FIX_APPLIED)" != "1" ] \
|
|
191
|
+
|| [ "$(marker_value CVE_COMPILED)" != "1" ] || [ "$(marker_value CVE_MANUAL_REQUIRED)" != "0" ] \
|
|
192
|
+
|| [ "$NEW_FIXES" -ne $((EXISTING_FIXES + 1)) ] || [ "$RANGE_COMMITS" != "1" ] \
|
|
193
|
+
|| [ "$(git log -1 --format='%s' 2>/dev/null)" != "$PIPE_COMMIT_CVEFIX" ] \
|
|
194
|
+
|| [ "$RANGE_HAS_DIFF" != true ] || [ "$RANGE_MANIFEST_ONLY" != true ]; then
|
|
195
|
+
# This range and its dirty tracked files belong to the agent. Restore the
|
|
196
|
+
# checked-out remote tip; do not carry unverified work into a later rebase.
|
|
197
|
+
git reset --hard "$PRE_AGENT_SHA" >/dev/null 2>&1 || git checkout -- . || true
|
|
198
|
+
pipe_failure_log_notice_files "CVE remediation" "update the listed dependencies manually" "$PIPE_AGENT_STDERR" >&2
|
|
199
|
+
{
|
|
200
|
+
pipe_failure_notice_files "CVE remediation" "update the listed dependencies manually" "$PIPE_AGENT_STDERR"
|
|
201
|
+
printf '\n'
|
|
202
|
+
if [ -s "$CVE_LIST_FILE" ]; then
|
|
203
|
+
printf 'Affected HIGH/CRITICAL vulnerability identifiers:\n'
|
|
204
|
+
sed 's/^/- /' "$CVE_LIST_FILE"
|
|
205
|
+
fi
|
|
206
|
+
printf '\nThe coder made no verified remediation commit, so manual dependency update is required.\n'
|
|
207
|
+
} > "$COMMENT_FILE"
|
|
208
|
+
post_comment
|
|
209
|
+
write_marker 0 0 1
|
|
210
|
+
exit 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# The accepted commit is authoritative. Reset index and worktree to it so
|
|
214
|
+
# compile/rebase see exactly what the runner will push; context artifacts stay
|
|
215
|
+
# untracked and are therefore preserved.
|
|
216
|
+
git reset --hard HEAD
|
|
217
|
+
|
|
218
|
+
if [ -z "$PIPE_COMPILE_CMD" ]; then
|
|
219
|
+
pipe_log "Configured compile command is empty; refusing to push an unverified CVE remediation"
|
|
220
|
+
post_manual_comment "The configured compile command is empty, so the automated dependency update cannot be verified."
|
|
221
|
+
write_marker 1 0 1
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
if ! bash -c "$PIPE_COMPILE_CMD"; then
|
|
225
|
+
pipe_log "CVE remediation compile command failed; refusing to push"
|
|
226
|
+
post_manual_comment "The automated dependency update was committed locally but the compile command failed; manual review is required."
|
|
227
|
+
write_marker 1 0 1
|
|
228
|
+
exit 1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
if ! pipe_rebase_and_push; then
|
|
232
|
+
pipe_log "CVE remediation rebase/push failed; human work was not overwritten"
|
|
233
|
+
post_manual_comment "The automated dependency update could not be rebased and pushed safely; resolve the branch changes manually."
|
|
234
|
+
write_marker 1 1 1
|
|
235
|
+
exit 1
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
printf '**Automated CVE remediation succeeded.**\n\n'
|
|
240
|
+
printf 'The configured dependency manifest was updated, compiled, rebased, and pushed.\n\n'
|
|
241
|
+
printf 'Remediated HIGH/CRITICAL vulnerability identifiers:\n'
|
|
242
|
+
sed 's/^/- /' "$CVE_LIST_FILE"
|
|
243
|
+
} > "$COMMENT_FILE"
|
|
244
|
+
post_comment
|
|
245
|
+
write_marker 1 1 0
|
|
246
|
+
pipe_log "Pushed verified CVE remediation commit"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Runner for the opt-in `docs-sync` job. Finds documentation gaps AND acts on
|
|
3
|
+
# them in a single pass: on a wip MR/PR the skill applies the documentation
|
|
4
|
+
# edits and this script commits and pushes them, on any other MR/PR the skill
|
|
5
|
+
# reports the gaps and this script posts them as an advisory comment.
|
|
6
|
+
#
|
|
7
|
+
# The runner is mechanical: it classifies the MR/PR, computes the changed files
|
|
8
|
+
# and the full diff, runs the /docs-sync skill, posts the verdict, and commits.
|
|
9
|
+
# Every judgement about what needs documenting lives in the skill and in the
|
|
10
|
+
# docs-sync agent it spawns.
|
|
11
|
+
#
|
|
12
|
+
# The diff is always base..head, never an incremental slice: a gap introduced by
|
|
13
|
+
# an earlier push must still be found on a later one.
|
|
14
|
+
set -e
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
+
# shellcheck source=lib/pipeline-common.sh
|
|
17
|
+
source "$SCRIPT_DIR/lib/pipeline-common.sh"
|
|
18
|
+
pipe_defaults
|
|
19
|
+
|
|
20
|
+
DOCS_MARKER="**Automated Documentation Sync**"
|
|
21
|
+
CONTEXT_FILE="$PIPE_CONTEXT_DIR/docs-sync-context.md"
|
|
22
|
+
FILES_FILE="$PIPE_CONTEXT_DIR/docs-sync-files.txt"
|
|
23
|
+
DIFF_FILE="$PIPE_CONTEXT_DIR/docs-sync-diff.patch"
|
|
24
|
+
DOCS_ENV="$PIPE_CONTEXT_DIR/docs-sync.env"
|
|
25
|
+
COMMENT_FILE="$PIPE_CONTEXT_DIR/docs-sync-comment.md"
|
|
26
|
+
|
|
27
|
+
# Documentation roots the agent may edit and this script may commit. Comma
|
|
28
|
+
# separated in PIPE_DOCS_ROOTS; a root is a directory or a single file.
|
|
29
|
+
# The split is a bash field split, not `tr ',' '\n'` piped into `read`: that
|
|
30
|
+
# pipeline emits no trailing newline, so `read` silently drops the LAST root.
|
|
31
|
+
# Surrounding whitespace is trimmed per element so "docs, CLAUDE.md" works.
|
|
32
|
+
DOCS_ROOTS=()
|
|
33
|
+
IFS=',' read -ra DOCS_ROOTS_RAW <<< "$PIPE_DOCS_ROOTS"
|
|
34
|
+
for root in "${DOCS_ROOTS_RAW[@]}"; do
|
|
35
|
+
root="${root#"${root%%[![:space:]]*}"}"
|
|
36
|
+
root="${root%"${root##*[![:space:]]}"}"
|
|
37
|
+
if [ -n "$root" ]; then
|
|
38
|
+
DOCS_ROOTS+=("$root")
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
if [ "${#DOCS_ROOTS[@]}" -eq 0 ]; then
|
|
42
|
+
pipe_log "PIPE_DOCS_ROOTS is empty, nothing to sync"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# True when the path is one of the documentation roots or sits under one.
|
|
47
|
+
docs_root_path() {
|
|
48
|
+
local root
|
|
49
|
+
for root in "${DOCS_ROOTS[@]}"; do
|
|
50
|
+
[ "$1" = "$root" ] && return 0
|
|
51
|
+
case "$1" in "$root"/*) return 0 ;; esac
|
|
52
|
+
done
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# 1. Fetch MR/PR meta (token work). The source branch comes from the same read:
|
|
57
|
+
# it is what an applied documentation commit is pushed to, and the CI runtime
|
|
58
|
+
# does not always provide it.
|
|
59
|
+
META=$(platform_mr_meta)
|
|
60
|
+
MR_TITLE=$(echo "$META" | jq -r '.title // ""')
|
|
61
|
+
MR_LABELS=$(echo "$META" | jq -r '.labels // ""')
|
|
62
|
+
META_SOURCE_BRANCH=$(echo "$META" | jq -r '.source_branch // ""')
|
|
63
|
+
if [ -z "${PIPE_SOURCE_BRANCH:-}" ] && [ -n "$META_SOURCE_BRANCH" ]; then
|
|
64
|
+
PIPE_SOURCE_BRANCH="$META_SOURCE_BRANCH"
|
|
65
|
+
export PIPE_SOURCE_BRANCH
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# 2. Classify. Only an autonomous (wip) MR/PR gets its documentation edited;
|
|
69
|
+
# a human-authored one gets a report.
|
|
70
|
+
DOCS_MODE=report
|
|
71
|
+
if echo "$MR_LABELS" | grep -q "$PIPE_LABEL_WIP"; then
|
|
72
|
+
DOCS_MODE=apply
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# 3. Full-MR diff base. platform_compute_diff_base would hand back the previous
|
|
76
|
+
# push head on a re-run, which is the wrong scope for this job, so the base
|
|
77
|
+
# is always the merge base with the target branch.
|
|
78
|
+
git fetch origin "$PIPE_TARGET_BRANCH" 2>/dev/null || true
|
|
79
|
+
DIFF_BASE=$(git merge-base "origin/$PIPE_TARGET_BRANCH" "$PIPE_HEAD_SHA" 2>/dev/null || echo "origin/$PIPE_TARGET_BRANCH")
|
|
80
|
+
pipe_log "mode=$DOCS_MODE diff base=$DIFF_BASE head=$PIPE_HEAD_SHA"
|
|
81
|
+
|
|
82
|
+
git diff --name-only "$DIFF_BASE" "$PIPE_HEAD_SHA" > "$FILES_FILE" 2>/dev/null || : > "$FILES_FILE"
|
|
83
|
+
if [ ! -s "$FILES_FILE" ]; then
|
|
84
|
+
pipe_log "No changed files in $DIFF_BASE..$PIPE_HEAD_SHA, skipping docs-sync"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# 4. Skip when the MR/PR only touches documentation: there is no new behaviour
|
|
89
|
+
# to document, and the author is already editing the docs.
|
|
90
|
+
CODE_CHANGED=false
|
|
91
|
+
while IFS= read -r file; do
|
|
92
|
+
[ -n "$file" ] || continue
|
|
93
|
+
if ! docs_root_path "$file"; then
|
|
94
|
+
CODE_CHANGED=true
|
|
95
|
+
break
|
|
96
|
+
fi
|
|
97
|
+
done < "$FILES_FILE"
|
|
98
|
+
if [ "$CODE_CHANGED" = "false" ]; then
|
|
99
|
+
pipe_log "Only documentation paths changed, skipping docs-sync"
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
git diff "$DIFF_BASE" "$PIPE_HEAD_SHA" > "$DIFF_FILE" 2>/dev/null || : > "$DIFF_FILE"
|
|
104
|
+
|
|
105
|
+
# 5. In apply mode, check out the real source branch so the edits and the commit
|
|
106
|
+
# land on its tip. Loop guard: at most one consecutive automated docs commit
|
|
107
|
+
# per branch streak, otherwise apply -> pipeline -> apply would never settle.
|
|
108
|
+
# The guard degrades this run to report mode instead of skipping it, so the
|
|
109
|
+
# MR/PR still gets a verdict.
|
|
110
|
+
if [ "$DOCS_MODE" = "apply" ]; then
|
|
111
|
+
pipe_git_identity
|
|
112
|
+
pipe_checkout_source
|
|
113
|
+
if [ "$(pipe_count_fix_commits "$PIPE_COMMIT_DOCSSYNC")" -gt 0 ]; then
|
|
114
|
+
pipe_log "Tip is already an automated docs commit, degrading to report mode"
|
|
115
|
+
DOCS_MODE=report
|
|
116
|
+
fi
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 6. Write the context file the skill reads. Unbounded text (the diff, the file
|
|
120
|
+
# list) stays in its own file, never in argv or the environment.
|
|
121
|
+
{
|
|
122
|
+
echo "# Documentation sync context"
|
|
123
|
+
echo
|
|
124
|
+
echo "Mode: $DOCS_MODE"
|
|
125
|
+
echo "Title: $MR_TITLE"
|
|
126
|
+
echo "Documentation roots: $PIPE_DOCS_ROOTS"
|
|
127
|
+
echo "Changed files: $FILES_FILE"
|
|
128
|
+
echo "Full diff: $DIFF_FILE"
|
|
129
|
+
echo "Diff range: $DIFF_BASE..$PIPE_HEAD_SHA"
|
|
130
|
+
} > "$CONTEXT_FILE"
|
|
131
|
+
|
|
132
|
+
# 7. Run the skill. It reads the context, spawns the docs-sync agent in the
|
|
133
|
+
# requested mode, writes $PIPE_RESULT_DOCSSYNC, the comment body, and the
|
|
134
|
+
# docs-sync.env marker this script reads back.
|
|
135
|
+
pipe_run_agent docs-sync "/docs-sync" "Agent,Read,Write,Edit,Glob,Grep,Bash" "$PIPE_MODEL_REVIEW" < /dev/null
|
|
136
|
+
|
|
137
|
+
# 8. Read the marker. DOCS_COMMENT_PATH carries the body because a comment is
|
|
138
|
+
# unbounded text. A result file written by a model can be malformed JSON, so
|
|
139
|
+
# the fallback extraction is defensive: an unparseable or comment-less result
|
|
140
|
+
# is treated exactly like a missing one.
|
|
141
|
+
DOCS_HAS_GAPS=$(pipe_get_env "$DOCS_ENV" DOCS_HAS_GAPS); DOCS_HAS_GAPS=${DOCS_HAS_GAPS:-false}
|
|
142
|
+
DOCS_CHANGED=$(pipe_get_env "$DOCS_ENV" DOCS_CHANGED); DOCS_CHANGED=${DOCS_CHANGED:-false}
|
|
143
|
+
DOCS_BODY=$(pipe_get_env "$DOCS_ENV" DOCS_COMMENT_PATH); DOCS_BODY=${DOCS_BODY:-$PIPE_CONTEXT_DIR/docs-sync-body.md}
|
|
144
|
+
if [ ! -s "$DOCS_BODY" ] && [ -f "$PIPE_RESULT_DOCSSYNC" ]; then
|
|
145
|
+
RESULT_BODY="$PIPE_CONTEXT_DIR/docs-sync-result-body.md"
|
|
146
|
+
jq -re '.comment // empty' "$PIPE_RESULT_DOCSSYNC" > "$RESULT_BODY" 2>/dev/null || true
|
|
147
|
+
if [ -s "$RESULT_BODY" ]; then
|
|
148
|
+
DOCS_BODY="$RESULT_BODY"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
DOCS_FAILED=false
|
|
153
|
+
if [ -s "$DOCS_BODY" ]; then
|
|
154
|
+
{
|
|
155
|
+
printf '%s\n\n' "$DOCS_MARKER"
|
|
156
|
+
cat "$DOCS_BODY"
|
|
157
|
+
} > "$COMMENT_FILE"
|
|
158
|
+
else
|
|
159
|
+
DOCS_FAILED=true
|
|
160
|
+
{
|
|
161
|
+
printf '%s\n\n⚠️ ' "$DOCS_MARKER"
|
|
162
|
+
pipe_failure_notice_files "documentation sync" \
|
|
163
|
+
"check by hand whether these changes need updates under $PIPE_DOCS_ROOTS" \
|
|
164
|
+
"$PIPE_AGENT_STDERR" "$PIPE_RESULT_DOCSSYNC"
|
|
165
|
+
} > "$COMMENT_FILE"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# 9. Always post the verdict. Metrics reflect the runner's actual commit
|
|
169
|
+
# decision, not the skill's informational DOCS_CHANGED marker.
|
|
170
|
+
DOCS_COMMITTED=false
|
|
171
|
+
emit_docs_metric() {
|
|
172
|
+
pipe_metric_event docs-sync docs_sync_gaps \
|
|
173
|
+
"$(jq -n --arg m "$DOCS_MODE" \
|
|
174
|
+
--argjson g "$([ "$DOCS_HAS_GAPS" = "true" ] && echo true || echo false)" \
|
|
175
|
+
--argjson c "$([ "$DOCS_COMMITTED" = "true" ] && echo true || echo false)" \
|
|
176
|
+
--argjson f "$([ "$DOCS_FAILED" = "true" ] && echo true || echo false)" \
|
|
177
|
+
'{mode: $m, has_gaps: $g, changed: $c, failed: $f}')"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
platform_post_comment_file "$COMMENT_FILE"
|
|
181
|
+
|
|
182
|
+
# 10. Commit and push what the agent applied. A failed agent applied nothing, so
|
|
183
|
+
# the notice posted above is the whole output and the exit is 0: an
|
|
184
|
+
# automation outage is not a documentation gap.
|
|
185
|
+
if [ "$DOCS_FAILED" = "true" ]; then
|
|
186
|
+
emit_docs_metric
|
|
187
|
+
pipe_log "Agent did not complete, manual-review notice posted, nothing to commit"
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
190
|
+
if [ "$DOCS_MODE" != "apply" ]; then
|
|
191
|
+
emit_docs_metric
|
|
192
|
+
exit 0
|
|
193
|
+
fi
|
|
194
|
+
if [ "$DOCS_CHANGED" != "true" ]; then
|
|
195
|
+
pipe_log "Skill reported no documentation edits, checking the staged diff anyway"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Stage each root on its own: one missing root must not stop the others from
|
|
199
|
+
# being staged, which a single `git add` of the whole set would do.
|
|
200
|
+
for root in "${DOCS_ROOTS[@]}"; do
|
|
201
|
+
[ -e "$root" ] || continue
|
|
202
|
+
git add -- "$root" || true
|
|
203
|
+
done
|
|
204
|
+
if git diff --cached --quiet; then
|
|
205
|
+
emit_docs_metric
|
|
206
|
+
pipe_log "No documentation changes staged, nothing to commit"
|
|
207
|
+
exit 0
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
git commit -m "$PIPE_COMMIT_DOCSSYNC"
|
|
211
|
+
DOCS_COMMITTED=true
|
|
212
|
+
emit_docs_metric
|
|
213
|
+
|
|
214
|
+
# The agent may only touch the documentation roots, but an edit outside them
|
|
215
|
+
# would leave the working tree dirty and abort the rebase below before it
|
|
216
|
+
# starts, after which `git rebase --abort` fails too. Discard out-of-scope
|
|
217
|
+
# edits so the tree is clean, enforcing the documented boundary.
|
|
218
|
+
if ! git diff --quiet; then
|
|
219
|
+
pipe_log "Discarding out-of-scope working-tree changes left outside $PIPE_DOCS_ROOTS:"
|
|
220
|
+
git diff --name-only
|
|
221
|
+
git checkout -- .
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
pipe_rebase_and_push
|
|
225
|
+
pipe_log "Pushed the docs-sync commit, a new pipeline will run"
|