appsec 0.1.0__tar.gz
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.
- appsec-0.1.0/.claude/skills/cicd/SKILL.md +168 -0
- appsec-0.1.0/.claude/skills/cicd/scripts/_resolve-nick.sh +43 -0
- appsec-0.1.0/.claude/skills/cicd/scripts/portability-lint.sh +57 -0
- appsec-0.1.0/.claude/skills/cicd/scripts/pr-reply.sh +77 -0
- appsec-0.1.0/.claude/skills/cicd/scripts/pr-status.sh +165 -0
- appsec-0.1.0/.claude/skills/cicd/scripts/workflow.sh +157 -0
- appsec-0.1.0/.claude/skills/communicate/SKILL.md +323 -0
- appsec-0.1.0/.claude/skills/communicate/scripts/fetch-issues.sh +59 -0
- appsec-0.1.0/.claude/skills/communicate/scripts/mesh-message.sh +74 -0
- appsec-0.1.0/.claude/skills/communicate/scripts/post-comment.sh +65 -0
- appsec-0.1.0/.claude/skills/communicate/scripts/post-issue.sh +71 -0
- appsec-0.1.0/.claude/skills/communicate/scripts/templates/skill-update-brief.md +101 -0
- appsec-0.1.0/.claude/skills/run-tests/SKILL.md +50 -0
- appsec-0.1.0/.claude/skills/run-tests/scripts/test.sh +52 -0
- appsec-0.1.0/.claude/skills/sonarclaude/SKILL.md +84 -0
- appsec-0.1.0/.claude/skills/sonarclaude/scripts/sonar.sh +263 -0
- appsec-0.1.0/.claude/skills/version-bump/SKILL.md +66 -0
- appsec-0.1.0/.claude/skills/version-bump/scripts/bump.py +181 -0
- appsec-0.1.0/.claude/skills.local.yaml.example +19 -0
- appsec-0.1.0/.flake8 +17 -0
- appsec-0.1.0/.github/workflows/publish.yml +87 -0
- appsec-0.1.0/.github/workflows/security-checks.yml +41 -0
- appsec-0.1.0/.github/workflows/tests.yml +93 -0
- appsec-0.1.0/.gitignore +221 -0
- appsec-0.1.0/.markdownlint-cli2.yaml +22 -0
- appsec-0.1.0/.pre-commit-config.yaml +37 -0
- appsec-0.1.0/CHANGELOG.md +28 -0
- appsec-0.1.0/CLAUDE.md +71 -0
- appsec-0.1.0/LICENSE +21 -0
- appsec-0.1.0/PKG-INFO +20 -0
- appsec-0.1.0/README.md +3 -0
- appsec-0.1.0/appsec/__init__.py +11 -0
- appsec-0.1.0/appsec/__main__.py +8 -0
- appsec-0.1.0/appsec/cli/__init__.py +105 -0
- appsec-0.1.0/appsec/cli/_commands/__init__.py +0 -0
- appsec-0.1.0/appsec/cli/_commands/explain.py +40 -0
- appsec-0.1.0/appsec/cli/_commands/learn.py +44 -0
- appsec-0.1.0/appsec/cli/_commands/whoami.py +40 -0
- appsec-0.1.0/appsec/cli/_errors.py +39 -0
- appsec-0.1.0/appsec/cli/_output.py +45 -0
- appsec-0.1.0/culture.yaml +3 -0
- appsec-0.1.0/docs/skill-sources.md +26 -0
- appsec-0.1.0/docs/superpowers/plans/2026-05-14-appsec-onboarding.md +1926 -0
- appsec-0.1.0/docs/superpowers/specs/2026-05-14-appsec-onboarding-design.md +312 -0
- appsec-0.1.0/pyproject.toml +76 -0
- appsec-0.1.0/sonar-project.properties +15 -0
- appsec-0.1.0/tests/__init__.py +0 -0
- appsec-0.1.0/tests/test_cli_chassis.py +48 -0
- appsec-0.1.0/tests/test_cli_errors.py +36 -0
- appsec-0.1.0/tests/test_cli_output.py +57 -0
- appsec-0.1.0/tests/test_cli_stubs.py +69 -0
- appsec-0.1.0/tests/test_package.py +10 -0
- appsec-0.1.0/uv.lock +646 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cicd
|
|
3
|
+
description: >
|
|
4
|
+
appsec's CI/CD lane, layered on `agex pr`. Delegates lint / open /
|
|
5
|
+
read / reply / delta to agex; adds two steward extensions — `status`
|
|
6
|
+
(SonarCloud quality gate + hotspots + unresolved-thread tally) and
|
|
7
|
+
`await` (read --wait + status with non-zero exit on Sonar ERROR or
|
|
8
|
+
unresolved threads). Use when: creating PRs in appsec, handling
|
|
9
|
+
review feedback, polling CI status, or the user says "create PR",
|
|
10
|
+
"review comments", "address feedback", "resolve threads". Renamed
|
|
11
|
+
from `pr-review` in steward 0.7.0; rebased on agex in 0.12.0.
|
|
12
|
+
Vendored from steward; see docs/skill-sources.md for divergence notes.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# CI/CD — Steward edition
|
|
16
|
+
|
|
17
|
+
`agex pr` (in `agentculture/agex-cli`) is the upstream for the
|
|
18
|
+
five core PR-lifecycle verbs — `lint`, `open`, `read`, `reply`,
|
|
19
|
+
`delta`. Steward used to vendor parallel scripts for each; in 0.12.0
|
|
20
|
+
those vendored copies were dropped in favor of delegating to `agex`.
|
|
21
|
+
What's left in this skill is **the steward-specific gating layer**:
|
|
22
|
+
|
|
23
|
+
- `status` — SonarCloud quality gate, OPEN issues, hotspots, deploy
|
|
24
|
+
preview URL, unresolved-inline-thread tally.
|
|
25
|
+
- `await` — composes `agex pr read --wait` with `status` and gates on
|
|
26
|
+
Sonar `ERROR` / unresolved threads. The single command to run after
|
|
27
|
+
pushing a fix when you want "wake me when this PR is triage-able."
|
|
28
|
+
|
|
29
|
+
Those two are the steward unique surface today. They're filed as a
|
|
30
|
+
feature ask upstream
|
|
31
|
+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41));
|
|
32
|
+
once they land they migrate out of this skill.
|
|
33
|
+
|
|
34
|
+
The workflow is encapsulated in `scripts/workflow.sh` — follow that
|
|
35
|
+
(or call `agex pr` directly).
|
|
36
|
+
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
Hard requirements: `agex` (>=0.1), `gh` (GitHub CLI), `jq`, `bash`,
|
|
40
|
+
`python3` (stdlib only), `curl` (used by `pr-status.sh`).
|
|
41
|
+
|
|
42
|
+
Install agex once:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install agex-cli # or: pip install --user agex-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Soft requirement: `PyYAML` is needed **only for suffix mode** of the
|
|
49
|
+
sibling `agent-config` skill, where it parses Culture's server
|
|
50
|
+
manifest. Every `cicd` script works without it; suffix mode prints a
|
|
51
|
+
clear install hint when invoked without it.
|
|
52
|
+
|
|
53
|
+
Per-machine paths (sibling-project layout) live in
|
|
54
|
+
`.claude/skills.local.yaml`; see the committed `.example` for the
|
|
55
|
+
schema. `agex pr delta` reads the same file.
|
|
56
|
+
|
|
57
|
+
## How to run
|
|
58
|
+
|
|
59
|
+
`scripts/workflow.sh` is the entry point. Subcommands:
|
|
60
|
+
|
|
61
|
+
| Command | What it does |
|
|
62
|
+
|---------|--------------|
|
|
63
|
+
| `workflow.sh lint` | `agex pr lint --exit-on-violation` — portability + alignment-trigger check. |
|
|
64
|
+
| `workflow.sh open [gh-flags]` | `agex pr open --delayed-read`. Creates the PR, then polls 180s for an initial briefing. `--title TITLE` required; body via `--body-file PATH` or stdin. |
|
|
65
|
+
| `workflow.sh read [PR] [--wait N]` | `agex pr read`. One-shot briefing (CI checks, SonarCloud gate + new issues, all comments, next-step footer). Pass `--wait N` to poll up to N seconds for required reviewers. |
|
|
66
|
+
| `workflow.sh reply <PR>` | `agex pr reply <PR>` — batch JSONL replies (stdin) + thread resolve. agex auto-signs from `culture.yaml`. |
|
|
67
|
+
| `workflow.sh delta` | `agex pr delta` — sibling alignment dump. |
|
|
68
|
+
| `workflow.sh status <PR>` | **Steward extension.** `pr-status.sh` — Sonar gate, OPEN issues, hotspots, unresolved-thread breakdown, deploy preview URL. Authoritative gate for `await`. |
|
|
69
|
+
| `workflow.sh await <PR>` | **Steward extension.** `agex pr read --wait` then `status`. Exits non-zero on Sonar ERROR or unresolved threads. Tunables: `STEWARD_PR_AWAIT_WAIT` (default 1800s passed to `--wait`), `STEWARD_PR_AWAIT_SECONDS` (legacy fixed pre-sleep, deprecated). |
|
|
70
|
+
| `workflow.sh help` | Print the list. |
|
|
71
|
+
|
|
72
|
+
You can also call `agex pr <verb>` directly — `workflow.sh` is a
|
|
73
|
+
typing-saver around the same verbs. The steward `status` and `await`
|
|
74
|
+
extensions only have shell entry points.
|
|
75
|
+
|
|
76
|
+
The vendored single-comment helper `pr-reply.sh` (plus its
|
|
77
|
+
`_resolve-nick.sh` dependency) is still shipped — pinned by
|
|
78
|
+
`tests/test_pr_reply_signature.py` and `tests/test_resolve_nick.py`,
|
|
79
|
+
and useful when a one-off reply doesn't merit batch JSONL. It is not
|
|
80
|
+
called by `workflow.sh` anymore. The vendored `portability-lint.sh`
|
|
81
|
+
is also still shipped — `steward doctor`'s portability check runs it
|
|
82
|
+
directly against target repos. Both are scheduled for follow-up
|
|
83
|
+
migration to agex.
|
|
84
|
+
|
|
85
|
+
## Long waits (background polling)
|
|
86
|
+
|
|
87
|
+
`agex pr read --wait N` polls in-session for up to N seconds. The
|
|
88
|
+
Anthropic prompt cache has a 5-minute TTL; sleeping past it burns
|
|
89
|
+
context every cache miss. Two ways to drive the wait:
|
|
90
|
+
|
|
91
|
+
- **Synchronous** — `workflow.sh await <PR>` after `gh pr create` /
|
|
92
|
+
`workflow.sh open`. Fine when readiness is expected within ~5
|
|
93
|
+
minutes.
|
|
94
|
+
- **Asynchronous** — for longer waits, run `agex pr read --wait NNN`
|
|
95
|
+
inside a background subagent (Agent tool, `run_in_background: true`)
|
|
96
|
+
so the main session only pays the cache cost when readiness fires.
|
|
97
|
+
The subagent's only job is to invoke `agex pr read --wait` and echo
|
|
98
|
+
its headline back. The parent triages with `workflow.sh await`
|
|
99
|
+
when the notification arrives. The user can interrupt with
|
|
100
|
+
TaskStop.
|
|
101
|
+
|
|
102
|
+
This pattern was originally borrowed from sibling repo
|
|
103
|
+
[`agentculture/cfafi`](https://github.com/agentculture/cfafi)'s `poll`
|
|
104
|
+
skill. The async guidance is also filed upstream
|
|
105
|
+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
|
|
106
|
+
|
|
107
|
+
## Conventions
|
|
108
|
+
|
|
109
|
+
`agex pr` emits a **"Next step:"** footer at the end of every command
|
|
110
|
+
that names the right next verb (the same chain `agex learn cicd`
|
|
111
|
+
documents) — follow that rather than memorizing an order. `workflow.sh
|
|
112
|
+
help` mirrors the verb table when you need the steward-flavored
|
|
113
|
+
extensions (`status`, `await`) on top.
|
|
114
|
+
|
|
115
|
+
Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`,
|
|
116
|
+
`skill/<name>`. PR / comment signature: `- <nick> (Claude)`, where
|
|
117
|
+
`<nick>` is resolved by `agex` from the agent's own `culture.yaml`
|
|
118
|
+
(first agent's `suffix`), falling back to the git-repo basename. agex
|
|
119
|
+
auto-appends the signature on `pr open` and `pr reply` only when the
|
|
120
|
+
body isn't already signed.
|
|
121
|
+
|
|
122
|
+
## Triage rules
|
|
123
|
+
|
|
124
|
+
For every comment, decide **FIX** or **PUSHBACK** with reasoning.
|
|
125
|
+
|
|
126
|
+
Default to **FIX** for: portability complaints (always valid for
|
|
127
|
+
Steward — recurring bug class), test or doc requests, style nits
|
|
128
|
+
aligned with workspace conventions.
|
|
129
|
+
|
|
130
|
+
Default to **PUSHBACK** for: architecture opinions that conflict with
|
|
131
|
+
workspace `CLAUDE.md` or the all-backends rule; greenfield
|
|
132
|
+
false-positives (e.g. "add tests" before there's any source — defer
|
|
133
|
+
to a later PR, don't refuse).
|
|
134
|
+
|
|
135
|
+
### Alignment-delta rule
|
|
136
|
+
|
|
137
|
+
If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
|
|
138
|
+
`.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
|
|
139
|
+
PUSHBACK on each comment. Note any sibling that needs a follow-up PR
|
|
140
|
+
and mention it in your reply.
|
|
141
|
+
|
|
142
|
+
## Greenfield-aware steps
|
|
143
|
+
|
|
144
|
+
The lint and the workflow script are always-on. Stack-specific steps
|
|
145
|
+
are conditional and currently no-op (greenfield repo):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
[ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
|
|
149
|
+
[ -f pyproject.toml ] && bump_version_per_project_convention # see project README
|
|
150
|
+
[ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Revisit each line as the corresponding stack element actually lands.
|
|
154
|
+
A `pr lint --extra=tests,version,markdown` ask is filed upstream
|
|
155
|
+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
|
|
156
|
+
|
|
157
|
+
## Reply etiquette
|
|
158
|
+
|
|
159
|
+
Every comment must get a reply — no silent fixes. `agex pr reply`
|
|
160
|
+
includes thread-resolve by default. Reference the review-comment IDs
|
|
161
|
+
in the fix-up commit message.
|
|
162
|
+
|
|
163
|
+
The `status` extension queries SonarCloud directly (it predates the
|
|
164
|
+
upstream Sonar integration in `agex pr read`). Both surfaces are
|
|
165
|
+
trustworthy — `agex pr read` for display in the briefing, `status` for
|
|
166
|
+
the gate. Steward isn't yet a registered mesh agent, so the
|
|
167
|
+
post-merge IRC ping that Culture's `pr-review` includes is still
|
|
168
|
+
skipped — that returns when Steward joins the mesh.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Resolve the agent's nick for GitHub message signing.
|
|
5
|
+
# Order: first agent's `suffix` in <repo-root>/culture.yaml,
|
|
6
|
+
# then basename of the git repo root.
|
|
7
|
+
# Prints the nick to stdout. Always exits 0 — pr-reply.sh needs *some*
|
|
8
|
+
# nick to sign with — but if a culture.yaml exists and we couldn't
|
|
9
|
+
# extract a suffix from it, emits a stderr warning so a misconfigured
|
|
10
|
+
# manifest doesn't silently mask itself behind the basename fallback.
|
|
11
|
+
|
|
12
|
+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
13
|
+
if [[ -z "$repo_root" ]]; then
|
|
14
|
+
repo_root="$PWD"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
manifest="$repo_root/culture.yaml"
|
|
18
|
+
|
|
19
|
+
if [[ -f "$manifest" ]]; then
|
|
20
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
21
|
+
echo "_resolve-nick: python3 not found; cannot parse $manifest, falling back to repo basename" >&2
|
|
22
|
+
else
|
|
23
|
+
nick="$(python3 - "$manifest" <<'PY' 2>/dev/null || true
|
|
24
|
+
import re, sys
|
|
25
|
+
path = sys.argv[1]
|
|
26
|
+
with open(path, encoding="utf-8") as f:
|
|
27
|
+
for raw in f:
|
|
28
|
+
line = raw.rstrip("\n")
|
|
29
|
+
m = re.match(r"^[\s-]*\s*suffix:\s*(\S+)", line)
|
|
30
|
+
if m:
|
|
31
|
+
print(m.group(1).strip("'\""))
|
|
32
|
+
break
|
|
33
|
+
PY
|
|
34
|
+
)"
|
|
35
|
+
if [[ -n "$nick" ]]; then
|
|
36
|
+
printf '%s\n' "$nick"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
echo "_resolve-nick: $manifest exists but no suffix could be parsed; falling back to repo basename" >&2
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
basename "$repo_root"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Portability lint: catch path leaks and per-user config dependencies in
|
|
3
|
+
# committed docs/configs before they ship in a PR. Steward's recurring bug
|
|
4
|
+
# class.
|
|
5
|
+
#
|
|
6
|
+
# Usage: portability-lint.sh [--all]
|
|
7
|
+
# default: lint files modified vs HEAD (staged + unstaged)
|
|
8
|
+
# --all: lint all tracked files
|
|
9
|
+
#
|
|
10
|
+
# Exits 0 if clean, 1 if any leak is found.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
mode="${1:-diff}"
|
|
15
|
+
case "$mode" in
|
|
16
|
+
--all) files=$(git ls-files -- ':(exclude)*.lock') ;;
|
|
17
|
+
diff|--diff) files=$(git diff --diff-filter=AMR --name-only HEAD -- ':(exclude)*.lock') ;;
|
|
18
|
+
*) echo "Usage: $(basename "$0") [--all]" >&2; exit 2 ;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
[ -z "$files" ] && { echo "(no files to check)"; exit 0; }
|
|
22
|
+
|
|
23
|
+
# ----- Check 1: hard-coded /home/<user>/... paths -----
|
|
24
|
+
hits1=$(echo "$files" | xargs -r grep -nE '/home/[a-z][a-z0-9_-]+/' 2>/dev/null || true)
|
|
25
|
+
|
|
26
|
+
# ----- Check 2: per-user dotfile *config* refs in committed docs/configs -----
|
|
27
|
+
# Carve-outs (allowed, NOT flagged):
|
|
28
|
+
# - ~/.claude/skills/<x>/scripts/ vendored tool calls
|
|
29
|
+
# - ~/.culture/ Culture mesh data this skill is supposed to read
|
|
30
|
+
md_yaml=$(echo "$files" | grep -E '\.(md|ya?ml|toml|json|jsonc)$' || true)
|
|
31
|
+
if [ -n "$md_yaml" ]; then
|
|
32
|
+
hits2=$(echo "$md_yaml" | xargs -r grep -nE '~/\.[A-Za-z]' 2>/dev/null \
|
|
33
|
+
| grep -vE '~/\.claude/skills/[^[:space:]"]+/scripts/' \
|
|
34
|
+
| grep -vE '~/\.culture/' \
|
|
35
|
+
|| true)
|
|
36
|
+
else
|
|
37
|
+
hits2=""
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
fail=0
|
|
41
|
+
if [ -n "$hits1" ]; then
|
|
42
|
+
echo "❌ Hard-coded /home/<user>/ paths:"
|
|
43
|
+
echo "$hits1" | sed 's/^/ /'
|
|
44
|
+
echo " Fix: use ../sibling, repo URL, or \$WORKSPACE/sibling instead."
|
|
45
|
+
fail=1
|
|
46
|
+
fi
|
|
47
|
+
if [ -n "$hits2" ]; then
|
|
48
|
+
[ "$fail" -eq 1 ] && echo
|
|
49
|
+
echo "❌ Per-user ~/.<dotfile> config refs in committed doc/config:"
|
|
50
|
+
echo "$hits2" | sed 's/^/ /'
|
|
51
|
+
echo " Allowed carve-outs: ~/.claude/skills/.../scripts/ (tool calls), ~/.culture/ (mesh data)."
|
|
52
|
+
echo " Otherwise: commit a repo-local config or document a portable lookup."
|
|
53
|
+
fail=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ "$fail" -eq 0 ] && echo "✓ portability lint clean ($(echo "$files" | wc -l | tr -d ' ') files checked)"
|
|
57
|
+
exit $fail
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Reply to a PR review comment, optionally resolve its thread.
|
|
5
|
+
# Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER COMMENT_ID "body"
|
|
6
|
+
|
|
7
|
+
REPO=""
|
|
8
|
+
RESOLVE=false
|
|
9
|
+
PRINT_BODY=false
|
|
10
|
+
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
14
|
+
--resolve) RESOLVE=true; shift ;;
|
|
15
|
+
--print-body) PRINT_BODY=true; shift ;;
|
|
16
|
+
*) break ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
PR_NUMBER="${1:?Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] [--print-body] PR_NUMBER COMMENT_ID \"body\"}"
|
|
21
|
+
COMMENT_ID="${2:?Missing COMMENT_ID}"
|
|
22
|
+
BODY="${3:?Missing reply body}"
|
|
23
|
+
|
|
24
|
+
if [[ "$PRINT_BODY" != true && -z "$REPO" ]]; then
|
|
25
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Sign with the agent's nick. Resolved per invocation so siblings that
|
|
29
|
+
# vendor this skill pick up their own culture.yaml suffix automatically.
|
|
30
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
NICK="$("$SCRIPT_DIR/_resolve-nick.sh")"
|
|
32
|
+
SIG="- ${NICK} (Claude)"
|
|
33
|
+
if ! printf '%s' "$BODY" | grep -qFx -- "$SIG"; then
|
|
34
|
+
BODY="${BODY}
|
|
35
|
+
|
|
36
|
+
${SIG}"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ "$PRINT_BODY" == true ]]; then
|
|
40
|
+
printf '%s\n' "$BODY"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Post reply
|
|
45
|
+
REPLY_URL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \
|
|
46
|
+
-f body="$BODY" \
|
|
47
|
+
--jq '.html_url')
|
|
48
|
+
echo "Replied: $REPLY_URL"
|
|
49
|
+
|
|
50
|
+
# Resolve thread if requested
|
|
51
|
+
if [[ "$RESOLVE" == true ]]; then
|
|
52
|
+
# Find the thread ID for this comment
|
|
53
|
+
THREAD_ID=$(gh api graphql -f query="
|
|
54
|
+
{
|
|
55
|
+
repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
|
|
56
|
+
pullRequest(number: $PR_NUMBER) {
|
|
57
|
+
reviewThreads(first: 100) {
|
|
58
|
+
nodes {
|
|
59
|
+
id
|
|
60
|
+
comments(first: 100) {
|
|
61
|
+
nodes { databaseId }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}" --jq ".data.repository.pullRequest.reviewThreads.nodes[] | select(any(.comments.nodes[]; .databaseId == $COMMENT_ID)) | .id")
|
|
68
|
+
|
|
69
|
+
if [[ -n "$THREAD_ID" ]]; then
|
|
70
|
+
RESOLVED=$(gh api graphql -f query="
|
|
71
|
+
mutation { resolveReviewThread(input: {threadId: \"$THREAD_ID\"}) { thread { isResolved } } }
|
|
72
|
+
" --jq '.data.resolveReviewThread.thread.isResolved')
|
|
73
|
+
echo "Resolved: $RESOLVED (thread $THREAD_ID)"
|
|
74
|
+
else
|
|
75
|
+
echo "Warning: could not find thread for comment $COMMENT_ID"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# pr-status.sh — one-shot status overview for a Steward PR.
|
|
3
|
+
#
|
|
4
|
+
# Combines five things review feedback usually scatters across:
|
|
5
|
+
# 1. PR state (open / merged / closed) + branch + author
|
|
6
|
+
# 2. CI checks (build / lint / unit / sonarcloud / cf-pages / etc.)
|
|
7
|
+
# 3. Review-bot pipeline status (Copilot, qodo, SonarCloud, Cloudflare)
|
|
8
|
+
# 4. SonarCloud quality gate + open-issue count
|
|
9
|
+
# 5. Inline-thread resolved-vs-unresolved tally
|
|
10
|
+
#
|
|
11
|
+
# Usage: scripts/pr-status.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER
|
|
12
|
+
#
|
|
13
|
+
# Defaults:
|
|
14
|
+
# --repo auto-detected via `gh repo view`
|
|
15
|
+
# --sonar-key derived from repo as `<owner>_<name>` (SonarCloud convention)
|
|
16
|
+
#
|
|
17
|
+
# Requires: gh, jq, curl, python3.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
REPO=""
|
|
22
|
+
SONAR_KEY=""
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
27
|
+
--sonar-key) SONAR_KEY="$2"; shift 2 ;;
|
|
28
|
+
*) break ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
PR_NUMBER="${1:?Usage: pr-status.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER}"
|
|
33
|
+
|
|
34
|
+
if [[ -z "$REPO" ]]; then
|
|
35
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
36
|
+
fi
|
|
37
|
+
# Sonar key precedence: explicit --sonar-key flag > SONAR_PROJECT_KEY env >
|
|
38
|
+
# `<owner>_<repo>` derivation. Mirrors pr-comments.sh so SKILL.md's claim
|
|
39
|
+
# that the env var works for both scripts is true.
|
|
40
|
+
if [[ -z "$SONAR_KEY" ]]; then
|
|
41
|
+
SONAR_KEY="${SONAR_PROJECT_KEY:-${REPO%%/*}_${REPO##*/}}"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── 1. PR header ──────────────────────────────────────────────────────────
|
|
45
|
+
# appsec-divergence: pass --repo "$REPO" (upstream omits it — agentculture/steward#34)
|
|
46
|
+
PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json \
|
|
47
|
+
number,title,state,isDraft,mergedAt,mergedBy,baseRefName,headRefName,author,url)
|
|
48
|
+
|
|
49
|
+
echo "════════════════════════════════════════════════════════════════════"
|
|
50
|
+
echo "$PR_JSON" | jq -r '
|
|
51
|
+
"PR #\(.number) — \(.title)",
|
|
52
|
+
" \(.url)",
|
|
53
|
+
" Author: \(.author.login)",
|
|
54
|
+
" Branch: \(.headRefName) → \(.baseRefName)",
|
|
55
|
+
" State: \(if .state == "MERGED" then "MERGED at \(.mergedAt) by \(.mergedBy.login)" elif .state == "OPEN" and .isDraft then "OPEN (draft)" else .state end)"
|
|
56
|
+
'
|
|
57
|
+
echo "════════════════════════════════════════════════════════════════════"
|
|
58
|
+
|
|
59
|
+
# ── 2. CI checks ──────────────────────────────────────────────────────────
|
|
60
|
+
echo
|
|
61
|
+
echo "── CI checks ─────────────────────────────────────────────────────────"
|
|
62
|
+
# `gh pr checks` exits non-zero when checks are still pending/failing.
|
|
63
|
+
# We don't care about its exit code here; capture and pretty-print.
|
|
64
|
+
# appsec-divergence: pass --repo "$REPO" (upstream omits it — agentculture/steward#34)
|
|
65
|
+
CHECKS=$(gh pr checks "$PR_NUMBER" --repo "$REPO" 2>/dev/null || true)
|
|
66
|
+
if [[ -z "$CHECKS" ]]; then
|
|
67
|
+
echo " (no checks reported)"
|
|
68
|
+
else
|
|
69
|
+
echo "$CHECKS" | awk -F'\t' '
|
|
70
|
+
{
|
|
71
|
+
name = $1
|
|
72
|
+
state = $2
|
|
73
|
+
dur = $3
|
|
74
|
+
sym = "?"
|
|
75
|
+
if (state == "pass") sym = "✅"
|
|
76
|
+
else if (state == "fail") sym = "❌"
|
|
77
|
+
else if (state == "skipping") sym = "⏭"
|
|
78
|
+
else if (state == "pending" || state == "queued" || state == "in_progress") sym = "…"
|
|
79
|
+
printf " %s %-22s %-10s %s\n", sym, name, state, dur
|
|
80
|
+
}
|
|
81
|
+
'
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── 3. Review bots & comment pipeline ────────────────────────────────────
|
|
85
|
+
echo
|
|
86
|
+
echo "── Review pipeline ───────────────────────────────────────────────────"
|
|
87
|
+
|
|
88
|
+
# Inline-thread tally via GraphQL (resolved vs unresolved).
|
|
89
|
+
THREADS_JSON=$(gh api graphql -f query="
|
|
90
|
+
{
|
|
91
|
+
repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
|
|
92
|
+
pullRequest(number: $PR_NUMBER) {
|
|
93
|
+
reviewThreads(first: 100) {
|
|
94
|
+
nodes { id isResolved comments(first: 1) { nodes { author { login } } } }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}" --jq '.data.repository.pullRequest.reviewThreads.nodes')
|
|
99
|
+
|
|
100
|
+
INLINE_TOTAL=$(echo "$THREADS_JSON" | jq 'length')
|
|
101
|
+
INLINE_RESOLVED=$(echo "$THREADS_JSON" | jq '[.[] | select(.isResolved)] | length')
|
|
102
|
+
INLINE_PENDING=$((INLINE_TOTAL - INLINE_RESOLVED))
|
|
103
|
+
|
|
104
|
+
# Per-bot inline counts.
|
|
105
|
+
COPILOT_INLINE=$(echo "$THREADS_JSON" | jq '[.[] | select((.comments.nodes[0].author.login // "") | startswith("Copilot"))] | length')
|
|
106
|
+
QODO_INLINE=$(echo "$THREADS_JSON" | jq '[.[] | select((.comments.nodes[0].author.login // "") | startswith("qodo"))] | length')
|
|
107
|
+
|
|
108
|
+
# Issue-level comments (qodo summary, sonarcloud quality-gate body, cf-pages preview, etc.).
|
|
109
|
+
# Skip --paginate to avoid array concatenation; per_page=100 covers typical PRs.
|
|
110
|
+
ISSUE=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100")
|
|
111
|
+
QODO_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | startswith("qodo"))] | length')
|
|
112
|
+
SONARQUBE_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | startswith("sonarqubecloud"))] | length')
|
|
113
|
+
CFPAGES_ISSUE=$(echo "$ISSUE" | jq '[.[] | select((.user.login // "") | test("cloudflare"))] | length')
|
|
114
|
+
COPILOT_TOPLEVEL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/reviews?per_page=100" \
|
|
115
|
+
| jq '[.[] | select((.user.login // "") | startswith("copilot")) | select((.body // "") != "")] | length')
|
|
116
|
+
|
|
117
|
+
# Cloudflare deploy URL hidden in issue-comment bodies (look for pages.dev).
|
|
118
|
+
CF_URL=$(echo "$ISSUE" | jq -r '[.[].body // "" | scan("https?://[a-z0-9.-]+\\.pages\\.dev[^\\s)\"<]*")] | first // ""')
|
|
119
|
+
|
|
120
|
+
printf " %-12s %s\n" "Copilot" "$([[ "$COPILOT_TOPLEVEL" -gt 0 || "$COPILOT_INLINE" -gt 0 ]] && echo "✅ overview×$COPILOT_TOPLEVEL, inline×$COPILOT_INLINE" || echo "— no posts yet")"
|
|
121
|
+
printf " %-12s %s\n" "qodo" "$([[ "$QODO_ISSUE" -gt 0 || "$QODO_INLINE" -gt 0 ]] && echo "✅ summary×$QODO_ISSUE, inline×$QODO_INLINE" || echo "— no posts yet")"
|
|
122
|
+
printf " %-12s %s\n" "Cloudflare" "$([[ -n "$CF_URL" ]] && echo "✅ $CF_URL" || ([[ "$CFPAGES_ISSUE" -gt 0 ]] && echo "✅ ($CFPAGES_ISSUE comments)" || echo "— no deploy preview"))"
|
|
123
|
+
|
|
124
|
+
# ── 4. SonarCloud quality gate + open issues ─────────────────────────────
|
|
125
|
+
SONAR_QG=$(curl -s "https://sonarcloud.io/api/qualitygates/project_status?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}")
|
|
126
|
+
SONAR_QG_STATUS=$(echo "$SONAR_QG" | jq -r '.projectStatus.status // "UNKNOWN"')
|
|
127
|
+
SONAR_OPEN=$(curl -s "https://sonarcloud.io/api/issues/search?componentKeys=${SONAR_KEY}&pullRequest=${PR_NUMBER}&statuses=OPEN,CONFIRMED&ps=1" \
|
|
128
|
+
| jq -r '.total // 0')
|
|
129
|
+
SONAR_HOTSPOTS=$(curl -s "https://sonarcloud.io/api/hotspots/search?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}&status=TO_REVIEW&ps=1" \
|
|
130
|
+
| jq -r '.paging.total // 0')
|
|
131
|
+
|
|
132
|
+
case "$SONAR_QG_STATUS" in
|
|
133
|
+
OK) SONAR_SYM="✅" ;;
|
|
134
|
+
ERROR) SONAR_SYM="❌" ;;
|
|
135
|
+
WARN) SONAR_SYM="⚠ " ;;
|
|
136
|
+
*) SONAR_SYM="?" ;;
|
|
137
|
+
esac
|
|
138
|
+
printf " %-12s %s Quality Gate %s, %d OPEN issue(s), %d hotspot(s)\n" \
|
|
139
|
+
"SonarCloud" "$SONAR_SYM" "$SONAR_QG_STATUS" "$SONAR_OPEN" "$SONAR_HOTSPOTS"
|
|
140
|
+
|
|
141
|
+
# When SonarCloud has OPEN issues, list them — saves a follow-up curl.
|
|
142
|
+
if [[ "$SONAR_OPEN" != "0" ]]; then
|
|
143
|
+
echo
|
|
144
|
+
echo " SonarCloud OPEN issues:"
|
|
145
|
+
curl -s "https://sonarcloud.io/api/issues/search?componentKeys=${SONAR_KEY}&pullRequest=${PR_NUMBER}&statuses=OPEN,CONFIRMED&ps=20" \
|
|
146
|
+
| jq -r '.issues[] | " • [\(.rule)] \(.component | sub("^[^:]+:"; ""))(:\(.line // "?")) (\(.severity)) — \(.message)"'
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# ── 5. Tally + summary ────────────────────────────────────────────────────
|
|
150
|
+
echo
|
|
151
|
+
echo "── Inline threads ────────────────────────────────────────────────────"
|
|
152
|
+
printf " Total: %d Resolved: %d Unresolved: %d\n" \
|
|
153
|
+
"$INLINE_TOTAL" "$INLINE_RESOLVED" "$INLINE_PENDING"
|
|
154
|
+
|
|
155
|
+
if [[ "$INLINE_PENDING" -gt 0 ]]; then
|
|
156
|
+
echo
|
|
157
|
+
echo " Unresolved threads:"
|
|
158
|
+
echo "$THREADS_JSON" | jq -r '
|
|
159
|
+
.[] | select(.isResolved == false) |
|
|
160
|
+
" • \(.comments.nodes[0].author.login): thread \(.id)"
|
|
161
|
+
'
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
echo
|
|
165
|
+
echo "(For full comment bodies: agex pr read --agent claude-code $PR_NUMBER)"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Steward cicd workflow — thin layer over `agex pr` plus two steward
|
|
5
|
+
# extensions (`status`, `await`) for SonarCloud gating and triage flow.
|
|
6
|
+
#
|
|
7
|
+
# Subcommands:
|
|
8
|
+
# lint `agex pr lint --exit-on-violation`. Same rules
|
|
9
|
+
# steward used to vendor in portability-lint.sh
|
|
10
|
+
# (which still ships for `steward doctor`).
|
|
11
|
+
# open [gh-pr flags] `agex pr open --delayed-read "$@"`. Creates the
|
|
12
|
+
# PR, then polls 180s for an initial briefing.
|
|
13
|
+
# Body via --body-file PATH or stdin; --title is
|
|
14
|
+
# required.
|
|
15
|
+
# read [PR] [--wait N] `agex pr read "$@"`. One-shot briefing today;
|
|
16
|
+
# pass --wait N to poll for reviewer readiness.
|
|
17
|
+
# Covers what create-pr-and-wait / pr-comments /
|
|
18
|
+
# wait-and-check / poll-readiness used to do.
|
|
19
|
+
# reply <PR> `agex pr reply <PR>` (JSONL on stdin). agex
|
|
20
|
+
# auto-signs from culture.yaml; same JSONL
|
|
21
|
+
# shape as the old pr-batch.sh.
|
|
22
|
+
# delta `agex pr delta`. Sibling alignment dump.
|
|
23
|
+
#
|
|
24
|
+
# status <PR> Steward extension: pr-status.sh — SonarCloud
|
|
25
|
+
# gate, OPEN issues, hotspots, unresolved
|
|
26
|
+
# inline-thread tally, deploy-preview URL.
|
|
27
|
+
# Source of truth for the `await` gate.
|
|
28
|
+
# await <PR> Steward extension: `read --wait` for the
|
|
29
|
+
# briefing, then `status` for the gate. Exits
|
|
30
|
+
# non-zero on SonarCloud ERROR or unresolved
|
|
31
|
+
# threads. Tunables:
|
|
32
|
+
# STEWARD_PR_AWAIT_WAIT (default 1800)
|
|
33
|
+
# — seconds passed to `read --wait`.
|
|
34
|
+
# STEWARD_PR_AWAIT_SECONDS (legacy)
|
|
35
|
+
# — fixed pre-sleep, deprecated.
|
|
36
|
+
#
|
|
37
|
+
# help print this message
|
|
38
|
+
|
|
39
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
40
|
+
|
|
41
|
+
# agex's `--agent` flag accepts only claude-code|codex|copilot|acp. The
|
|
42
|
+
# workspace culture.yaml convention is `backend: claude`, so we always
|
|
43
|
+
# pass --agent explicitly to insulate steward from that naming gap.
|
|
44
|
+
# Override via STEWARD_AGEX_AGENT if you're running under codex/copilot/acp.
|
|
45
|
+
AGEX_AGENT="${STEWARD_AGEX_AGENT:-claude-code}"
|
|
46
|
+
|
|
47
|
+
require_agex() {
|
|
48
|
+
if ! command -v agex >/dev/null 2>&1; then
|
|
49
|
+
echo "✗ agex not on PATH. Install agex-cli (>=0.1)." >&2
|
|
50
|
+
echo " uv tool install agex-cli # or pip install agex-cli" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cmd="${1:-help}"
|
|
56
|
+
shift || true
|
|
57
|
+
|
|
58
|
+
case "$cmd" in
|
|
59
|
+
lint)
|
|
60
|
+
require_agex
|
|
61
|
+
exec agex pr lint --agent "$AGEX_AGENT" --exit-on-violation "$@"
|
|
62
|
+
;;
|
|
63
|
+
open)
|
|
64
|
+
require_agex
|
|
65
|
+
exec agex pr open --agent "$AGEX_AGENT" --delayed-read "$@"
|
|
66
|
+
;;
|
|
67
|
+
read)
|
|
68
|
+
require_agex
|
|
69
|
+
exec agex pr read --agent "$AGEX_AGENT" "$@"
|
|
70
|
+
;;
|
|
71
|
+
reply)
|
|
72
|
+
require_agex
|
|
73
|
+
PR="${1:?Usage: workflow.sh reply <PR> (JSONL on stdin)}"
|
|
74
|
+
exec agex pr reply --agent "$AGEX_AGENT" "$PR"
|
|
75
|
+
;;
|
|
76
|
+
delta)
|
|
77
|
+
require_agex
|
|
78
|
+
exec agex pr delta --agent "$AGEX_AGENT" "$@"
|
|
79
|
+
;;
|
|
80
|
+
status)
|
|
81
|
+
PR="${1:?Usage: workflow.sh status <PR>}"
|
|
82
|
+
exec bash "$SCRIPT_DIR/pr-status.sh" "$PR"
|
|
83
|
+
;;
|
|
84
|
+
await)
|
|
85
|
+
require_agex
|
|
86
|
+
PR="${1:?Usage: workflow.sh await <PR>}"
|
|
87
|
+
|
|
88
|
+
# Legacy fixed-sleep escape hatch.
|
|
89
|
+
if [ -n "${STEWARD_PR_AWAIT_SECONDS:-}" ]; then
|
|
90
|
+
echo "warning: STEWARD_PR_AWAIT_SECONDS is deprecated; prefer STEWARD_PR_AWAIT_WAIT." >&2
|
|
91
|
+
echo "→ sleeping ${STEWARD_PR_AWAIT_SECONDS}s (legacy fixed-sleep) before agex pr read …" >&2
|
|
92
|
+
sleep "$STEWARD_PR_AWAIT_SECONDS"
|
|
93
|
+
WAIT_ARGS=()
|
|
94
|
+
else
|
|
95
|
+
WAIT="${STEWARD_PR_AWAIT_WAIT:-1800}"
|
|
96
|
+
WAIT_ARGS=(--wait "$WAIT")
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# 1. agex pr read --wait — readiness loop + briefing.
|
|
100
|
+
# Capture rc from the command itself (not from the negated test —
|
|
101
|
+
# `if ! cmd; then rc=$?` would store the if-test status, always 0
|
|
102
|
+
# in the failure branch, masking the real exit code).
|
|
103
|
+
echo "── agex pr read ──────────────────────────────────────────────────────" >&2
|
|
104
|
+
if agex pr read --agent "$AGEX_AGENT" "$PR" "${WAIT_ARGS[@]}"; then
|
|
105
|
+
READ_RC=0
|
|
106
|
+
else
|
|
107
|
+
READ_RC=$?
|
|
108
|
+
fi
|
|
109
|
+
if [ "$READ_RC" -ne 0 ]; then
|
|
110
|
+
echo "✗ agex pr read failed (exit $READ_RC)" >&2
|
|
111
|
+
exit "$READ_RC"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# 2. pr-status.sh — authoritative gate (Sonar QG, unresolved threads).
|
|
115
|
+
echo >&2
|
|
116
|
+
echo "── pr-status ─────────────────────────────────────────────────────────" >&2
|
|
117
|
+
if STATUS_OUT=$(bash "$SCRIPT_DIR/pr-status.sh" "$PR" 2>&1); then
|
|
118
|
+
STATUS_RC=0
|
|
119
|
+
else
|
|
120
|
+
STATUS_RC=$?
|
|
121
|
+
fi
|
|
122
|
+
printf '%s\n' "$STATUS_OUT"
|
|
123
|
+
if [ "$STATUS_RC" -ne 0 ]; then
|
|
124
|
+
echo >&2
|
|
125
|
+
echo "✗ pr-status.sh failed (exit $STATUS_RC) — cannot determine PR state" >&2
|
|
126
|
+
exit "$STATUS_RC"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# 3. Gate. Markers in pr-status.sh output:
|
|
130
|
+
# "Quality Gate ERROR" → Sonar fail
|
|
131
|
+
# "Unresolved: N" with N>0 → unresolved threads
|
|
132
|
+
SONAR_FAIL=0
|
|
133
|
+
UNRESOLVED=0
|
|
134
|
+
if printf '%s\n' "$STATUS_OUT" | grep -qE 'Quality Gate ERROR'; then
|
|
135
|
+
SONAR_FAIL=1
|
|
136
|
+
fi
|
|
137
|
+
if PENDING=$(printf '%s\n' "$STATUS_OUT" | grep -oE 'Unresolved:[[:space:]]+[0-9]+' | grep -oE '[0-9]+$' | head -1); then
|
|
138
|
+
[ -n "${PENDING:-}" ] && [ "$PENDING" -gt 0 ] && UNRESOLVED=1
|
|
139
|
+
fi
|
|
140
|
+
if [ "$SONAR_FAIL" -eq 1 ] || [ "$UNRESOLVED" -eq 1 ]; then
|
|
141
|
+
echo >&2
|
|
142
|
+
[ "$SONAR_FAIL" -eq 1 ] && echo "✗ SonarCloud quality gate ERROR" >&2
|
|
143
|
+
[ "$UNRESOLVED" -eq 1 ] && echo "✗ ${PENDING} unresolved review thread(s)" >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
echo >&2
|
|
147
|
+
echo "✓ no SonarCloud ERROR, no unresolved threads" >&2
|
|
148
|
+
;;
|
|
149
|
+
help|--help|-h)
|
|
150
|
+
sed -n '4,38p' "${BASH_SOURCE[0]}" | sed 's/^# *//'
|
|
151
|
+
;;
|
|
152
|
+
*)
|
|
153
|
+
echo "unknown subcommand: $cmd" >&2
|
|
154
|
+
echo "run '$(basename "$0") help' for usage." >&2
|
|
155
|
+
exit 2
|
|
156
|
+
;;
|
|
157
|
+
esac
|