trae-coding-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/bin/coding-engine.js +16 -0
- package/lib/cli.js +86 -0
- package/lib/index.js +6 -0
- package/lib/setup.js +290 -0
- package/package.json +32 -0
- package/templates/.agents/skills/coding-architecture/SKILL.md +347 -0
- package/templates/.agents/skills/coding-code-review/SKILL.md +95 -0
- package/templates/.agents/skills/coding-cve-remediation/SKILL.md +163 -0
- package/templates/.agents/skills/coding-db-schema/SKILL.md +273 -0
- package/templates/.agents/skills/coding-deploy-config/SKILL.md +129 -0
- package/templates/.agents/skills/coding-docs-audit/SKILL.md +285 -0
- package/templates/.agents/skills/coding-docs-audit-autofix/SKILL.md +33 -0
- package/templates/.agents/skills/coding-http-api/SKILL.md +269 -0
- package/templates/.agents/skills/coding-issue-autodev/SKILL.md +172 -0
- package/templates/.agents/skills/coding-merge-request/SKILL.md +199 -0
- package/templates/.agents/skills/coding-observability/SKILL.md +193 -0
- package/templates/.agents/skills/coding-refactor-insight/SKILL.md +89 -0
- package/templates/.agents/skills/coding-release-merge/SKILL.md +224 -0
- package/templates/.agents/skills/coding-runtime-adapter/SKILL.md +115 -0
- package/templates/.agents/skills/coding-testing/SKILL.md +169 -0
- package/templates/AGENTS.md +179 -0
- package/templates/MR-Template-Default.md +33 -0
- package/templates/Makefile +370 -0
- package/templates/REGISTRY.md +53 -0
- package/templates/scripts/check-adr.sh +283 -0
- package/templates/scripts/check-comment-i18n.py +209 -0
- package/templates/scripts/check-comment-i18n.sh +38 -0
- package/templates/scripts/check-doc-refs.sh +40 -0
- package/templates/scripts/check-docs.sh +103 -0
- package/templates/scripts/check-go-names.sh +59 -0
- package/templates/scripts/check-iam-actions.py +126 -0
- package/templates/scripts/check-migration-sql-immutability.sh +232 -0
- package/templates/scripts/check-mr-desc.sh +165 -0
- package/templates/scripts/check-mr-size.sh +128 -0
- package/templates/scripts/check-mr-title.sh +123 -0
- package/templates/scripts/check-openapi.py +221 -0
- package/templates/scripts/check-rdb-structured-queries.sh +98 -0
- package/templates/scripts/check-repository-transactions.sh +100 -0
- package/templates/scripts/check-runbooks.sh +229 -0
- package/templates/scripts/check-skill-router-sync.py +331 -0
- package/templates/scripts/check-skills.sh +243 -0
- package/templates/scripts/docs-audit-to-issues.py +373 -0
- package/templates/scripts/docs-verification-reminder.sh +63 -0
- package/templates/scripts/find-ci-failures.sh +133 -0
- package/templates/scripts/find-stale-mrs.sh +72 -0
- package/templates/scripts/gen-adr-index.sh +122 -0
- package/templates/scripts/open-mr.sh +536 -0
- package/templates/scripts/regen-agent-md.sh +149 -0
- package/templates/scripts/run-mr-hygiene.sh +140 -0
- package/templates/scripts/runbook.sh +91 -0
- package/templates/scripts/self-maintenance-digest.py +125 -0
- package/templates/scripts/send-self-maintenance-lark-card.py +116 -0
- package/templates/scripts/skill-graph.sh +114 -0
- package/templates/scripts/skill-impact.sh +134 -0
- package/templates/scripts/skill-propose.sh +302 -0
- package/templates/scripts/skill-router-hook.sh +152 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
fail=0
|
|
5
|
+
|
|
6
|
+
is_generated_or_frontend_path() {
|
|
7
|
+
local path="$1"
|
|
8
|
+
case "$path" in
|
|
9
|
+
web/*|*/web/*|\
|
|
10
|
+
.agents/*|*/.agents/*|\
|
|
11
|
+
.codex/*|*/.codex/*|\
|
|
12
|
+
*/thrift_gen/*|thrift_gen/*|\
|
|
13
|
+
*/third_party/*|third_party/*|\
|
|
14
|
+
*/vendor/*|vendor/*|\
|
|
15
|
+
*/node_modules/*|node_modules/*)
|
|
16
|
+
return 0
|
|
17
|
+
;;
|
|
18
|
+
esac
|
|
19
|
+
return 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
while IFS= read -r file; do
|
|
23
|
+
if is_generated_or_frontend_path "$file"; then
|
|
24
|
+
continue
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
dir=$(dirname "$file")
|
|
28
|
+
base=$(basename "$dir")
|
|
29
|
+
if [[ "$base" == *"_"* || "$base" =~ [A-Z] ]]; then
|
|
30
|
+
printf 'Go directory name must not use snake_case or uppercase: %s\n' "$dir" >&2
|
|
31
|
+
fail=1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
pkg=$(awk '/^package[[:space:]]+/ { print $2; exit }' "$file")
|
|
35
|
+
if [[ -z "$pkg" ]]; then
|
|
36
|
+
continue
|
|
37
|
+
fi
|
|
38
|
+
if [[ ! "$pkg" =~ ^[a-z][a-z0-9]*(_test)?$ ]]; then
|
|
39
|
+
printf 'Go package name must be lowercase alnum, with optional _test suffix: %s: package %s\n' "$file" "$pkg" >&2
|
|
40
|
+
fail=1
|
|
41
|
+
fi
|
|
42
|
+
if [[ "$pkg" != "main" ]]; then
|
|
43
|
+
want="$base"
|
|
44
|
+
if [[ "$pkg" == *_test ]]; then
|
|
45
|
+
want="${base}_test"
|
|
46
|
+
fi
|
|
47
|
+
if [[ "$pkg" != "$want" ]]; then
|
|
48
|
+
printf 'Go package name must match directory name: %s: package %s, want %s\n' "$file" "$pkg" "$want" >&2
|
|
49
|
+
fail=1
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
done < <(find . \
|
|
53
|
+
-path './.git' -prune -o \
|
|
54
|
+
-path './.agents' -prune -o \
|
|
55
|
+
-path './.codex' -prune -o \
|
|
56
|
+
-path './web' -prune -o \
|
|
57
|
+
-type f -name '*.go' -print | sed 's#^\./##' | sort)
|
|
58
|
+
|
|
59
|
+
exit "$fail"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check TOP Action synchronization between server.thrift and IAM registration."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
IDL_METHOD_RE = re.compile(r"^\s*[\w.]+\s+([A-Z]\w*)\s*\(", re.MULTILINE)
|
|
13
|
+
IAM_ACTION_RE = re.compile(r"^\s*-\s+Action:\s*([A-Za-z0-9_]+)\s*$")
|
|
14
|
+
IAM_FIELD_RE = re.compile(r"^\s{6}([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)\s*$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_text(path: Path) -> str:
|
|
18
|
+
try:
|
|
19
|
+
return path.read_text(encoding="utf-8")
|
|
20
|
+
except OSError as exc:
|
|
21
|
+
raise SystemExit(f"check-iam-actions: cannot read {path}: {exc}") from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def thrift_methods(path: Path) -> set[str]:
|
|
25
|
+
return set(IDL_METHOD_RE.findall(read_text(path)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def iam_actions(path: Path, ignored_path_prefixes: tuple[str, ...]) -> dict[str, str]:
|
|
29
|
+
actions: dict[str, str] = {}
|
|
30
|
+
current_action: str | None = None
|
|
31
|
+
current_path = ""
|
|
32
|
+
|
|
33
|
+
def flush() -> None:
|
|
34
|
+
nonlocal current_action, current_path
|
|
35
|
+
if current_action is None:
|
|
36
|
+
return
|
|
37
|
+
if not ignored_iam_path(current_path, ignored_path_prefixes):
|
|
38
|
+
actions[current_action] = current_path
|
|
39
|
+
current_action = None
|
|
40
|
+
current_path = ""
|
|
41
|
+
|
|
42
|
+
for line in read_text(path).splitlines():
|
|
43
|
+
action_match = IAM_ACTION_RE.match(line)
|
|
44
|
+
if action_match:
|
|
45
|
+
flush()
|
|
46
|
+
current_action = action_match.group(1)
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if current_action is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
field_match = IAM_FIELD_RE.match(line)
|
|
53
|
+
if not field_match:
|
|
54
|
+
continue
|
|
55
|
+
key, value = field_match.groups()
|
|
56
|
+
if key == "Path":
|
|
57
|
+
current_path = unquote(value)
|
|
58
|
+
|
|
59
|
+
flush()
|
|
60
|
+
return actions
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unquote(value: str) -> str:
|
|
64
|
+
value = value.strip()
|
|
65
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
66
|
+
return value[1:-1]
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ignored_iam_path(path: str, prefixes: tuple[str, ...]) -> bool:
|
|
71
|
+
return any(path == prefix or path.startswith(prefix + "/") for prefix in prefixes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> int:
|
|
75
|
+
parser = argparse.ArgumentParser(
|
|
76
|
+
description="Check that IDL TOP methods and IAM Actions are synchronized."
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("--idl", type=Path, default=Path("api/idl/server.thrift"))
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--iam",
|
|
81
|
+
type=Path,
|
|
82
|
+
default=Path("deployments/coding-engine/templates/server/iam-register-codingserver.yaml"),
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--ignore-iam-path-prefix",
|
|
86
|
+
action="append",
|
|
87
|
+
default=["/v1/agents"],
|
|
88
|
+
help="Ignore IAM Actions whose Path equals or starts with this prefix.",
|
|
89
|
+
)
|
|
90
|
+
args = parser.parse_args()
|
|
91
|
+
|
|
92
|
+
idl_action_names = thrift_methods(args.idl)
|
|
93
|
+
iam_action_paths = iam_actions(args.iam, tuple(args.ignore_iam_path_prefix))
|
|
94
|
+
iam_action_names = set(iam_action_paths)
|
|
95
|
+
|
|
96
|
+
missing_iam = sorted(idl_action_names - iam_action_names)
|
|
97
|
+
stale_iam = sorted(iam_action_names - idl_action_names)
|
|
98
|
+
|
|
99
|
+
if not missing_iam and not stale_iam:
|
|
100
|
+
print(
|
|
101
|
+
f"[OK] IAM Actions match IDL methods "
|
|
102
|
+
f"({len(idl_action_names)} IDL, {len(iam_action_names)} IAM after filters)"
|
|
103
|
+
)
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
if missing_iam:
|
|
107
|
+
print("IDL methods missing from IAM registration:", file=sys.stderr)
|
|
108
|
+
for action in missing_iam:
|
|
109
|
+
print(f" - {action}", file=sys.stderr)
|
|
110
|
+
|
|
111
|
+
if stale_iam:
|
|
112
|
+
print("IAM Actions not present in IDL methods:", file=sys.stderr)
|
|
113
|
+
for action in stale_iam:
|
|
114
|
+
print(f" - {action} (Path: {iam_action_paths[action] or '<empty>'})", file=sys.stderr)
|
|
115
|
+
|
|
116
|
+
print(
|
|
117
|
+
"Hint: update api/idl/server.thrift and "
|
|
118
|
+
"deployments/coding-engine/templates/server/iam-register-codingserver.yaml together. "
|
|
119
|
+
"IAM Actions with Path under /v1/agents are treated as REST exceptions.",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Guard source migration SQL history: committed migration files are append-only.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
BASE="${SQL_IMMUTABILITY_BASE:-origin/release}"
|
|
7
|
+
MIGRATION_DIALECT_RE='(mysql|postgres|dm)'
|
|
8
|
+
VERSIONED_MIGRATION_SQL_RE="^migration/${MIGRATION_DIALECT_RE}/(up|down)/v[0-9]+[.][0-9]+[.][0-9]+/[0-9]{3}[.]sql$"
|
|
9
|
+
|
|
10
|
+
while [ "$#" -gt 0 ]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--base)
|
|
13
|
+
[ "$#" -ge 2 ] || { echo "usage: $0 [--base <git-ref>]" >&2; exit 2; }
|
|
14
|
+
BASE="$2"
|
|
15
|
+
shift 2
|
|
16
|
+
;;
|
|
17
|
+
--base=*)
|
|
18
|
+
BASE="${1#--base=}"
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
-h|--help)
|
|
22
|
+
echo "usage: $0 [--base <git-ref>]"
|
|
23
|
+
echo
|
|
24
|
+
echo "Base must be a git ref resolvable in this worktree, for example origin/release."
|
|
25
|
+
echo "SQL_IMMUTABILITY_BASE provides the same value when --base is omitted."
|
|
26
|
+
echo "Set SQL_IMMUTABILITY_REQUIRE_CLEAN=1 to fail when migration/*.sql has local changes."
|
|
27
|
+
echo "Default base is origin/release; use --base or SQL_IMMUTABILITY_BASE for other MR targets."
|
|
28
|
+
echo "The repository must have enough source and target branch history to compute merge-base."
|
|
29
|
+
exit 0
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "unknown argument: $1" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
39
|
+
echo "check migration SQL immutability: not inside a git worktree" >&2
|
|
40
|
+
exit 2
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if ! BASE_COMMIT="$(git rev-parse --verify -q "${BASE}^{commit}")"; then
|
|
44
|
+
cat >&2 <<EOF
|
|
45
|
+
check migration SQL immutability: base ref ${BASE} is not available.
|
|
46
|
+
Fetch the target branch first, for example:
|
|
47
|
+
git fetch --filter=blob:none origin release:refs/remotes/origin/release
|
|
48
|
+
Or pass an available target ref explicitly:
|
|
49
|
+
$0 --base <git-ref>
|
|
50
|
+
EOF
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if ! MERGE_BASE="$(git merge-base "$BASE_COMMIT" HEAD)"; then
|
|
55
|
+
cat >&2 <<EOF
|
|
56
|
+
check migration SQL immutability: cannot compute merge-base with ${BASE}.
|
|
57
|
+
Fetch source and target branch history first, for example:
|
|
58
|
+
git fetch --filter=blob:none origin release:refs/remotes/origin/release
|
|
59
|
+
For shallow clones, fetch enough history for both source and target branches.
|
|
60
|
+
EOF
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
WORK="$(mktemp -d)"
|
|
65
|
+
trap 'rm -rf "$WORK"' EXIT INT TERM
|
|
66
|
+
|
|
67
|
+
violations="$WORK/violations"
|
|
68
|
+
invalid="$WORK/invalid"
|
|
69
|
+
additions="$WORK/additions"
|
|
70
|
+
dirty="$WORK/dirty"
|
|
71
|
+
: >"$violations"
|
|
72
|
+
: >"$invalid"
|
|
73
|
+
: >"$additions"
|
|
74
|
+
: >"$dirty"
|
|
75
|
+
|
|
76
|
+
is_versioned_migration_sql() {
|
|
77
|
+
printf '%s\n' "$1" | grep -Eq "$VERSIONED_MIGRATION_SQL_RE" || return 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
status_porcelain="$WORK/status-porcelain"
|
|
81
|
+
if ! git status --porcelain --untracked-files=all -- migration >"$status_porcelain"; then
|
|
82
|
+
echo "check migration SQL immutability: git status failed for migration/" >&2
|
|
83
|
+
exit 2
|
|
84
|
+
fi
|
|
85
|
+
awk -v re="${VERSIONED_MIGRATION_SQL_RE#^}" '$0 ~ re || $0 ~ re " -> " { print }' "$status_porcelain" >"$dirty"
|
|
86
|
+
|
|
87
|
+
if [ -s "$dirty" ]; then
|
|
88
|
+
if [ "${SQL_IMMUTABILITY_REQUIRE_CLEAN:-}" = "1" ]; then
|
|
89
|
+
cat >&2 <<'EOF'
|
|
90
|
+
Migration SQL working tree must be clean before running this guard in strict mode.
|
|
91
|
+
Commit or discard local migration/*.sql changes first; this check validates the
|
|
92
|
+
committed branch diff against the target branch and does not require unrelated
|
|
93
|
+
working tree files to be clean.
|
|
94
|
+
|
|
95
|
+
Dirty migration SQL changes:
|
|
96
|
+
EOF
|
|
97
|
+
sed 's/^/ /' "$dirty" >&2
|
|
98
|
+
exit 2
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
cat >&2 <<'EOF'
|
|
102
|
+
warning: local migration SQL changes are not included in the committed branch
|
|
103
|
+
immutability check. CI runs this guard in strict mode.
|
|
104
|
+
|
|
105
|
+
Dirty migration SQL changes:
|
|
106
|
+
EOF
|
|
107
|
+
sed 's/^/ /' "$dirty" >&2
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
diff_status_z="$WORK/diff-status-z"
|
|
111
|
+
if ! git diff -M --name-status -z --diff-filter=DMRT "$MERGE_BASE"...HEAD >"$diff_status_z"; then
|
|
112
|
+
cat >&2 <<EOF
|
|
113
|
+
check migration SQL immutability: git diff failed against base ${BASE}.
|
|
114
|
+
This may happen with bad revisions or partial/shallow clones missing objects.
|
|
115
|
+
Fetch enough history/objects for both source and target branches and retry.
|
|
116
|
+
EOF
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
while IFS= read -r -d '' status; do
|
|
121
|
+
kind="${status:0:1}"
|
|
122
|
+
case "$kind" in
|
|
123
|
+
M|D|T)
|
|
124
|
+
IFS= read -r -d '' path
|
|
125
|
+
if is_versioned_migration_sql "$path"; then
|
|
126
|
+
printf '%s\t%s\n' "$kind" "$path" >>"$violations"
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
R)
|
|
130
|
+
IFS= read -r -d '' old_path
|
|
131
|
+
IFS= read -r -d '' new_path
|
|
132
|
+
if is_versioned_migration_sql "$old_path"; then
|
|
133
|
+
printf '%s\t%s -> %s\n' "$kind" "$old_path" "$new_path" >>"$violations"
|
|
134
|
+
fi
|
|
135
|
+
if ! is_versioned_migration_sql "$new_path" \
|
|
136
|
+
&& [ "${new_path#migration/}" != "$new_path" ] \
|
|
137
|
+
&& [ "${old_path#migration/}" = "$old_path" ] \
|
|
138
|
+
&& printf '%s' "$new_path" | grep -Eq '[.]sql$'; then
|
|
139
|
+
printf '%s\t%s -> %s (renaming an external .sql into migration/ is not allowed; add new migrations only as versioned up/down/vX.Y.Z/NNN.sql paths)\n' "$kind" "$old_path" "$new_path" >>"$violations"
|
|
140
|
+
fi
|
|
141
|
+
if is_versioned_migration_sql "$new_path" && ! is_versioned_migration_sql "$old_path"; then
|
|
142
|
+
printf '%s\t%s -> %s (rename into migration is not allowed; versioned migrations must be added as new files)\n' "$kind" "$old_path" "$new_path" >>"$violations"
|
|
143
|
+
fi
|
|
144
|
+
;;
|
|
145
|
+
*)
|
|
146
|
+
IFS= read -r -d '' _unused || true
|
|
147
|
+
;;
|
|
148
|
+
esac
|
|
149
|
+
done <"$diff_status_z"
|
|
150
|
+
|
|
151
|
+
if [ -s "$violations" ]; then
|
|
152
|
+
cat >&2 <<'EOF'
|
|
153
|
+
Existing source migration SQL files are immutable, including comment-only changes.
|
|
154
|
+
Do not modify, delete, or rename SQL files that already exist on the target branch.
|
|
155
|
+
For the same release version, append the next NNN.sql file. For a new version,
|
|
156
|
+
create a new vX.Y.Z directory and start at 001.sql.
|
|
157
|
+
|
|
158
|
+
Rejected changes:
|
|
159
|
+
EOF
|
|
160
|
+
sed 's/^/ /' "$violations" >&2
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
git diff -M --name-only --diff-filter=A "$MERGE_BASE"...HEAD -- migration \
|
|
165
|
+
| awk '/[.]sql$/ { print }' >"$additions"
|
|
166
|
+
|
|
167
|
+
if [ ! -s "$additions" ]; then
|
|
168
|
+
exit 0
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
while IFS= read -r path; do
|
|
172
|
+
case "$path" in
|
|
173
|
+
migration/mysql/up/v*/*.sql|migration/mysql/down/v*/*.sql|migration/postgres/up/v*/*.sql|migration/postgres/down/v*/*.sql|migration/dm/up/v*/*.sql|migration/dm/down/v*/*.sql)
|
|
174
|
+
file="${path##*/}"
|
|
175
|
+
version="${path%/*}"
|
|
176
|
+
version="${version##*/}"
|
|
177
|
+
if ! printf '%s\n' "$version" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
178
|
+
printf '%s: version directory must be vX.Y.Z\n' "$path" >>"$invalid"
|
|
179
|
+
fi
|
|
180
|
+
if ! printf '%s\n' "$file" | grep -Eq '^[0-9]{3}[.]sql$'; then
|
|
181
|
+
printf '%s: migration filename must be a three-digit NNN.sql sequence\n' "$path" >>"$invalid"
|
|
182
|
+
fi
|
|
183
|
+
;;
|
|
184
|
+
*)
|
|
185
|
+
printf '%s: migration SQL must live under migration/{mysql,postgres,dm}/{up,down}/vX.Y.Z/NNN.sql; dialect directory names are lowercase\n' "$path" >>"$invalid"
|
|
186
|
+
;;
|
|
187
|
+
esac
|
|
188
|
+
done <"$additions"
|
|
189
|
+
|
|
190
|
+
if [ -s "$invalid" ]; then
|
|
191
|
+
cat >&2 <<'EOF'
|
|
192
|
+
New source migration SQL files must use versioned NNN.sql paths:
|
|
193
|
+
EOF
|
|
194
|
+
sed 's/^/ /' "$invalid" >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
awk -F/ '{ print $1 "/" $2 "/" $3 "/" $4 }' "$additions" | sort -u >"$WORK/dirs"
|
|
199
|
+
|
|
200
|
+
while IFS= read -r dir; do
|
|
201
|
+
base_max="$(
|
|
202
|
+
git ls-tree -r --name-only "$BASE_COMMIT" -- "$dir" \
|
|
203
|
+
| awk -F/ '
|
|
204
|
+
/[0-9][0-9][0-9][.]sql$/ {
|
|
205
|
+
n = $NF
|
|
206
|
+
sub(/[.]sql$/, "", n)
|
|
207
|
+
if ((n + 0) > max) max = n + 0
|
|
208
|
+
}
|
|
209
|
+
END { print max + 0 }
|
|
210
|
+
'
|
|
211
|
+
)"
|
|
212
|
+
expected=$((base_max + 1))
|
|
213
|
+
while IFS= read -r num; do
|
|
214
|
+
actual=$((10#$num))
|
|
215
|
+
if [ "$actual" -ne "$expected" ]; then
|
|
216
|
+
printf '%s/%03d.sql: expected next migration file, found %03d.sql\n' "$dir" "$expected" "$actual" >>"$invalid"
|
|
217
|
+
expected=$((actual + 1))
|
|
218
|
+
continue
|
|
219
|
+
fi
|
|
220
|
+
expected=$((expected + 1))
|
|
221
|
+
done < <(awk -F/ -v dir="$dir" '$0 ~ "^" dir "/" { print $NF }' "$additions" | sed 's/[.]sql$//' | sort -n)
|
|
222
|
+
done <"$WORK/dirs"
|
|
223
|
+
|
|
224
|
+
if [ -s "$invalid" ]; then
|
|
225
|
+
cat >&2 <<'EOF'
|
|
226
|
+
New migration SQL files must be appended in sequence.
|
|
227
|
+
For an existing version directory, use the next NNN.sql after the target branch.
|
|
228
|
+
For a new version directory, start at 001.sql and keep new files contiguous.
|
|
229
|
+
EOF
|
|
230
|
+
sed 's/^/ /' "$invalid" >&2
|
|
231
|
+
exit 1
|
|
232
|
+
fi
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Hard guardrail: block MRs whose description is missing, left as the raw
|
|
3
|
+
# template, or whose 变更说明 section contains only placeholder comments.
|
|
4
|
+
#
|
|
5
|
+
# Pairs with scripts/open-mr.sh (producer-side apply helper); this is
|
|
6
|
+
# the receiver-side gate, invoked from .gitlab-ci.yml on every MR pipeline.
|
|
7
|
+
#
|
|
8
|
+
# Description source, checked in order:
|
|
9
|
+
# 1. --description-file <path> ('-' reads STDIN)
|
|
10
|
+
# 2. GitLab API fetch (MR_LINT_TOKEN + CI_MERGE_REQUEST_IID set; CI-only)
|
|
11
|
+
# 3. CI_MERGE_REQUEST_DESCRIPTION env var (GitLab built-in; ~2700-char trunc)
|
|
12
|
+
#
|
|
13
|
+
# Exit codes:
|
|
14
|
+
# 0 description is acceptable
|
|
15
|
+
# 1 description fails one or more checks -- MR should be blocked
|
|
16
|
+
# 2 bad usage / missing dependency / not running on an MR
|
|
17
|
+
#
|
|
18
|
+
# Local usage:
|
|
19
|
+
# scripts/check-mr-desc.sh --description-file /tmp/desc.md
|
|
20
|
+
# echo "..." | scripts/check-mr-desc.sh --description-file -
|
|
21
|
+
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
cd "$(dirname "$0")/.."
|
|
24
|
+
|
|
25
|
+
# shellcheck source=scripts/lib/cli-args.sh
|
|
26
|
+
. scripts/lib/cli-args.sh
|
|
27
|
+
|
|
28
|
+
TEMPLATE_DIR=".gitlab/merge_request_templates"
|
|
29
|
+
MIN_BODY_CHARS=20
|
|
30
|
+
|
|
31
|
+
DESC_FILE=""
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--description-file)
|
|
35
|
+
DESC_FILE=$(require_value --description-file "${2:-}"); shift 2 ;;
|
|
36
|
+
--description-file=*)
|
|
37
|
+
DESC_FILE="${1#--description-file=}"; shift ;;
|
|
38
|
+
-h|--help)
|
|
39
|
+
sed -n '2,21p' "$0"; exit 0 ;;
|
|
40
|
+
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
description=""
|
|
45
|
+
source_label=""
|
|
46
|
+
|
|
47
|
+
if [ -n "$DESC_FILE" ]; then
|
|
48
|
+
if [ "$DESC_FILE" = "-" ]; then
|
|
49
|
+
description=$(cat)
|
|
50
|
+
source_label="stdin"
|
|
51
|
+
else
|
|
52
|
+
[ -f "$DESC_FILE" ] || { echo "error: file not found: $DESC_FILE" >&2; exit 2; }
|
|
53
|
+
description=$(cat "$DESC_FILE")
|
|
54
|
+
source_label="file:$DESC_FILE"
|
|
55
|
+
fi
|
|
56
|
+
elif [ -n "${MR_LINT_TOKEN:-}" ] && [ -n "${CI_MERGE_REQUEST_IID:-}" ]; then
|
|
57
|
+
# CI-only path: bytedcli isn't on the alpine runner. curl+jq are installed
|
|
58
|
+
# by .gitlab-ci.yml's before_script only when MR_LINT_TOKEN is set.
|
|
59
|
+
api_base="${CI_API_V4_URL:-https://code.byted.org/api/v4}"
|
|
60
|
+
project_id="${CI_MERGE_REQUEST_PROJECT_ID:-${CI_PROJECT_ID:-}}"
|
|
61
|
+
if [ -z "$project_id" ]; then
|
|
62
|
+
echo "error: MR_LINT_TOKEN set but no CI_MERGE_REQUEST_PROJECT_ID/CI_PROJECT_ID" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
command -v curl >/dev/null || { echo "missing curl" >&2; exit 2; }
|
|
66
|
+
command -v jq >/dev/null || { echo "missing jq" >&2; exit 2; }
|
|
67
|
+
api_url="${api_base}/projects/${project_id}/merge_requests/${CI_MERGE_REQUEST_IID}"
|
|
68
|
+
resp=$(curl -fsS --max-time 20 \
|
|
69
|
+
--header "PRIVATE-TOKEN: ${MR_LINT_TOKEN}" \
|
|
70
|
+
"$api_url") || {
|
|
71
|
+
echo "error: failed to fetch MR via API: $api_url" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
}
|
|
74
|
+
# `jq -e` exits 1 on null/empty -- catches API shape drift before it
|
|
75
|
+
# manifests downstream as a misleading "description is empty" verdict.
|
|
76
|
+
description=$(jq -er '.description // empty' <<<"$resp") || {
|
|
77
|
+
echo "error: MR API response had no .description field" >&2
|
|
78
|
+
exit 2
|
|
79
|
+
}
|
|
80
|
+
source_label="api:!${CI_MERGE_REQUEST_IID}"
|
|
81
|
+
elif [ -n "${CI_MERGE_REQUEST_DESCRIPTION:-}" ]; then
|
|
82
|
+
description="$CI_MERGE_REQUEST_DESCRIPTION"
|
|
83
|
+
source_label="env:CI_MERGE_REQUEST_DESCRIPTION (may be truncated)"
|
|
84
|
+
elif [ -n "${CI_MERGE_REQUEST_IID:-}" ]; then
|
|
85
|
+
echo "error: running on an MR pipeline but no description source available." >&2
|
|
86
|
+
echo " set CI_MERGE_REQUEST_DESCRIPTION, or provide MR_LINT_TOKEN for API fetch." >&2
|
|
87
|
+
exit 2
|
|
88
|
+
else
|
|
89
|
+
echo "error: not running on an MR pipeline and no --description-file given." >&2
|
|
90
|
+
echo " see --help for local usage." >&2
|
|
91
|
+
exit 2
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Used for template-equality comparison only.
|
|
95
|
+
normalize() {
|
|
96
|
+
tr -d '\r' \
|
|
97
|
+
| sed -E 's/[[:space:]]+/ /g' \
|
|
98
|
+
| sed -E 's/^ +//; s/ +$//'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
desc_norm=$(printf '%s' "$description" | normalize)
|
|
102
|
+
|
|
103
|
+
failures=()
|
|
104
|
+
|
|
105
|
+
if [ -z "$desc_norm" ]; then
|
|
106
|
+
failures+=("description is empty")
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ -d "$TEMPLATE_DIR" ]; then
|
|
110
|
+
for tpl in "$TEMPLATE_DIR"/*.md; do
|
|
111
|
+
[ -f "$tpl" ] || continue
|
|
112
|
+
tpl_norm=$(normalize <"$tpl")
|
|
113
|
+
if [ -n "$tpl_norm" ] && [ "$desc_norm" = "$tpl_norm" ]; then
|
|
114
|
+
failures+=("description is unchanged from template: ${tpl}")
|
|
115
|
+
break
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# When 变更说明 header is present, only its section counts as prose; otherwise
|
|
121
|
+
# fall back to checking the whole document. Either way the strip+count pipeline
|
|
122
|
+
# below runs exactly once.
|
|
123
|
+
if grep -q '^## 变更说明' <<<"$description"; then
|
|
124
|
+
section_body=$(awk '
|
|
125
|
+
/^## 变更说明[[:space:]]*$/ { capture=1; next }
|
|
126
|
+
capture && /^---[[:space:]]*$/ { exit }
|
|
127
|
+
capture && /^## / { exit }
|
|
128
|
+
capture { print }
|
|
129
|
+
' <<<"$description")
|
|
130
|
+
else
|
|
131
|
+
section_body="$description"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# One pass: strip multi-line HTML comments, drop the `**类型:**` metadata line.
|
|
135
|
+
body_clean=$(perl -0777 -pe 's/<!--.*?-->//gs; s/^\*\*类型:\*\*.*\n?//mg' <<<"$section_body")
|
|
136
|
+
body_chars=$(printf '%s' "$body_clean" | tr -d '[:space:]' | wc -c | tr -d ' ')
|
|
137
|
+
|
|
138
|
+
if [ "${body_chars:-0}" -lt "$MIN_BODY_CHARS" ]; then
|
|
139
|
+
failures+=("变更说明 section has <${MIN_BODY_CHARS} chars of real content (got ${body_chars:-0}); only placeholder comments?")
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
printf " source: %s\n" "$source_label"
|
|
143
|
+
printf " chars: %d (raw)\n" "$(printf '%s' "$description" | wc -c | tr -d ' ')"
|
|
144
|
+
printf " body chars: %d (变更说明, after stripping comments)\n" "${body_chars:-0}"
|
|
145
|
+
|
|
146
|
+
if [ "${#failures[@]}" -eq 0 ]; then
|
|
147
|
+
echo " [OK] MR description passes lint"
|
|
148
|
+
exit 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
echo
|
|
153
|
+
echo " [BLOCK] MR description failed ${#failures[@]} check(s):"
|
|
154
|
+
for f in "${failures[@]}"; do
|
|
155
|
+
echo " - $f"
|
|
156
|
+
done
|
|
157
|
+
echo
|
|
158
|
+
echo " How to fix:"
|
|
159
|
+
echo " 1. Open the MR and edit Description in the GitLab UI, or"
|
|
160
|
+
echo " 2. Draft desc.md from the actual branch diff and apply it with \`make open-mr ARGS=\"--body-file desc.md\"\`."
|
|
161
|
+
echo " 3. Iterate locally: \`make check-mr-desc FILE=desc.md\` (or pipe via --description-file -)."
|
|
162
|
+
echo
|
|
163
|
+
echo " Template lives at: ${TEMPLATE_DIR}/Default.md"
|
|
164
|
+
} >&2
|
|
165
|
+
exit 1
|