chore(spec): 归档 add-blog-infra——Phase 0 七任务收敛交付
- T1-T7 全部勾选,spec_verify --all 七项 exit=0 入 runs.jsonl - check 门禁收敛;交付:备份三件套/gitea 28.2.0/Artalk 2.10.0/ git.wait4x.online + /artalk 公网接入/发布链地基与回滚预案 - 生产 Halo 全程未动(healthy + 200 复查通过) - 含 spec pre-commit/pre-push 门禁钩子入库 Spec: 2026-10-24_add-blog-infra Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
f16b9487d3
commit
0f7c2dfe5b
8 files changed
+412
-26
No files matched your search
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# The source hook fails closed unless rendered; install_git_hooks.py
|
||||
# replaces these placeholders with absolute paths in installed copies.
|
||||
repo=$(git rev-parse --show-toplevel)
|
||||
checker="/home/wait4x/.pi/agent/skills/spec/scripts/check_all_spec_packages.py"
|
||||
# Checker resolution:
|
||||
# 1. SPEC_CHECK_ALL_SCRIPT environment override — the explicit operator
|
||||
# escape hatch; the path in effect is echoed to stderr so a swap is
|
||||
# never silent.
|
||||
# 2. The absolute path rendered by install_git_hooks.py, already held in
|
||||
# $checker when the placeholder above was replaced. Repository content
|
||||
# can never replace the gate checker; an unrendered template fails
|
||||
# closed below.
|
||||
if [ -n "${SPEC_CHECK_ALL_SCRIPT:-}" ]; then
|
||||
checker="$SPEC_CHECK_ALL_SCRIPT"
|
||||
printf '%s\n' "spec gate: using SPEC_CHECK_ALL_SCRIPT override: $checker" >&2
|
||||
fi
|
||||
|
||||
if [ ! -f "$checker" ]; then
|
||||
printf '%s\n' \
|
||||
"error: Spec gate checker not found at: $checker" \
|
||||
" The checker script is expected at the path above." \
|
||||
" Fix:" \
|
||||
" 1. Re-run: python3 scripts/install_git_hooks.py --root \"$repo\"" \
|
||||
" 2. The checker lives in the spec skill package under scripts/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python="/home/wait4x/miniconda3/bin/python3.13"
|
||||
python_placeholder_prefix=__SPEC_PYTHON_
|
||||
python_placeholder="${python_placeholder_prefix}PLACEHOLDER__"
|
||||
if [ "$python" = "$python_placeholder" ]; then
|
||||
python=${SPEC_PYTHON:-$(command -v python3 || true)}
|
||||
fi
|
||||
if [ -z "$python" ]; then
|
||||
printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$python" != "${python##*/}" ] && [ ! -x "$python" ]; then
|
||||
printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Branch-aware gate routing (AUD-07): the full gate guards the irreversible
|
||||
# paths — commits on the main branch (the merge destination) and commits
|
||||
# carrying archive paths on any branch. In-flight WIP commits on spec/*
|
||||
# integration branches get the structural lint only; the full gate
|
||||
# re-engages at the main-branch merge (pre-push + push_spec_package).
|
||||
# The channel is decided by objective Git facts (branch name, staged
|
||||
# paths), never by a committer-declared footer.
|
||||
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
||||
case "$branch" in
|
||||
spec/*)
|
||||
if git diff --cached --name-only --diff-filter=ACMRT 2>/dev/null | grep -q "specs/archive/"; then
|
||||
: # Archive-carrying commits keep the full gate on every branch.
|
||||
else
|
||||
printf '%s\n' \
|
||||
"spec gate: WIP commit on integration branch $branch — structural lint only;" \
|
||||
"the full gate runs for main-branch and archive-carrying commits" >&2
|
||||
exec "$python" "$checker" --root "$repo" --lint-only
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
baseline=""
|
||||
for ref in refs/heads/main refs/heads/master; do
|
||||
if baseline=$(git rev-parse --verify "$ref" 2>/dev/null); then
|
||||
break
|
||||
fi
|
||||
baseline=""
|
||||
done
|
||||
set --
|
||||
if [ -n "$baseline" ]; then
|
||||
set -- --legacy-baseline "$baseline"
|
||||
fi
|
||||
"$python" "$checker" --root "$repo" "$@"
|
||||
"$python" "$checker" --root "$repo" --index "$@"
|
||||
Executable
+192
@@ -0,0 +1,192 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo=$(git rev-parse --show-toplevel)
|
||||
checker="/home/wait4x/.pi/agent/skills/spec/scripts/check_all_spec_packages.py"
|
||||
# Checker resolution:
|
||||
# 1. SPEC_CHECK_ALL_SCRIPT environment override — the explicit operator
|
||||
# escape hatch; the path in effect is echoed to stderr so a swap is
|
||||
# never silent.
|
||||
# 2. The absolute path rendered by install_git_hooks.py, already held in
|
||||
# $checker when the placeholder above was replaced. Repository content
|
||||
# can never replace the gate checker; an unrendered template fails
|
||||
# closed below.
|
||||
if [ -n "${SPEC_CHECK_ALL_SCRIPT:-}" ]; then
|
||||
checker="$SPEC_CHECK_ALL_SCRIPT"
|
||||
printf '%s\n' "spec gate: using SPEC_CHECK_ALL_SCRIPT override: $checker" >&2
|
||||
fi
|
||||
|
||||
if [ ! -f "$checker" ]; then
|
||||
printf '%s\n' \
|
||||
"error: Spec gate checker not found at: $checker" \
|
||||
" The checker script is expected at the path above." \
|
||||
" Fix:" \
|
||||
" 1. Re-run: python3 scripts/install_git_hooks.py --root \"$repo\"" \
|
||||
" 2. The checker lives in the spec skill package under scripts/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python="/home/wait4x/miniconda3/bin/python3.13"
|
||||
python_placeholder_prefix=__SPEC_PYTHON_
|
||||
python_placeholder="${python_placeholder_prefix}PLACEHOLDER__"
|
||||
if [ "$python" = "$python_placeholder" ]; then
|
||||
python=${SPEC_PYTHON:-$(command -v python3 || true)}
|
||||
fi
|
||||
if [ -z "$python" ]; then
|
||||
printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$python" != "${python##*/}" ] && [ ! -x "$python" ]; then
|
||||
printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mode=${SPEC_PUSH_GATE:-touched}
|
||||
case "$mode" in
|
||||
touched|all) ;;
|
||||
*) printf '%s\n' "error: SPEC_PUSH_GATE must be touched or all" >&2; exit 1 ;;
|
||||
esac
|
||||
zero=0000000000000000000000000000000000000000
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/spec-pre-push.XXXXXX")
|
||||
baseline_ref="refs/spec-gate/legacy-baseline-$$"
|
||||
cleanup() {
|
||||
git update-ref -d "$baseline_ref" >/dev/null 2>&1 || true
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
remote_name=${1:-}
|
||||
legacy_baseline=$zero
|
||||
if [ -n "$remote_name" ]; then
|
||||
main_branch=${SPEC_MAIN_BRANCH:-}
|
||||
if [ -z "$main_branch" ]; then
|
||||
symref=$(git ls-remote --symref "$remote_name" HEAD) \
|
||||
|| { printf '%s\n' "error: cannot resolve authoritative remote HEAD for $remote_name" >&2; exit 1; }
|
||||
main_branch=$(printf '%s\n' "$symref" | sed -n 's#^ref: refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$#\1#p' | sed -n '1p')
|
||||
main_branch=${main_branch:-main}
|
||||
fi
|
||||
remote_main=$(git ls-remote --heads "$remote_name" "refs/heads/$main_branch") \
|
||||
|| { printf '%s\n' "error: cannot resolve authoritative $remote_name/$main_branch" >&2; exit 1; }
|
||||
legacy_baseline=$(printf '%s\n' "$remote_main" | sed -n 's/[[:space:]].*$//p' | sed -n '1p')
|
||||
legacy_baseline=${legacy_baseline:-$zero}
|
||||
if [ "$legacy_baseline" != "$zero" ]; then
|
||||
git fetch --no-tags "$remote_name" "+refs/heads/$main_branch:$baseline_ref" >/dev/null 2>&1 \
|
||||
|| { printf '%s\n' "error: cannot fetch authoritative $remote_name/$main_branch baseline" >&2; exit 1; }
|
||||
fetched_baseline=$(git rev-parse --verify "$baseline_ref^{commit}") \
|
||||
|| { printf '%s\n' "error: authoritative remote-main baseline object is unavailable" >&2; exit 1; }
|
||||
[ "$fetched_baseline" = "$legacy_baseline" ] \
|
||||
|| { printf '%s\n' "error: authoritative remote-main baseline changed during pre-push" >&2; exit 1; }
|
||||
fi
|
||||
fi
|
||||
specs_args=""
|
||||
if [ -n "${SPEC_SPECS_DIRS:-}" ]; then
|
||||
old_ifs=$IFS
|
||||
IFS=,
|
||||
for specs_dir in $SPEC_SPECS_DIRS; do
|
||||
case "$specs_dir" in
|
||||
''|*[!A-Za-z0-9._/-]*)
|
||||
printf '%s\n' "error: SPEC_SPECS_DIRS must be comma-separated trusted relative paths" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
specs_args="$specs_args --specs-dir $specs_dir"
|
||||
done
|
||||
IFS=$old_ifs
|
||||
fi
|
||||
input_path=$tmp_dir/refs
|
||||
cat >"$input_path"
|
||||
index=0
|
||||
|
||||
while read -r local_ref local_sha remote_ref remote_sha; do
|
||||
[ -n "${local_sha:-}" ] || continue
|
||||
[ "$local_sha" = "$zero" ] && continue
|
||||
case "$remote_ref" in
|
||||
refs/heads/spec/*)
|
||||
# AUD-07: in-flight handoff. Integration branches carry work in
|
||||
# progress (WIP commits, handoff.md) through the remote; the
|
||||
# irreversible path is the main-branch merge, which keeps the full
|
||||
# gate below.
|
||||
printf '%s\n' \
|
||||
"spec gate: releasing in-flight push of $remote_ref (integration-branch handoff);" \
|
||||
"the full gate applies when this work merges to the main branch" >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
index=$((index + 1))
|
||||
files_path=$tmp_dir/files-$index
|
||||
commits_path=$tmp_dir/commits-$index
|
||||
footer_slugs=$tmp_dir/footer-slugs-$index
|
||||
footer_window_base=""
|
||||
: >"$files_path"
|
||||
: >"$commits_path"
|
||||
: >"$footer_slugs"
|
||||
|
||||
if [ "$remote_sha" = "$zero" ]; then
|
||||
git log --name-only --pretty=format: "$local_sha" --not --remotes >"$files_path" \
|
||||
|| { printf '%s\n' "error: cannot enumerate files for pushed ref $local_ref" >&2; exit 1; }
|
||||
git rev-list --no-merges "$local_sha" --not --remotes >"$commits_path" \
|
||||
|| { printf '%s\n' "error: cannot enumerate commits for pushed ref $local_ref" >&2; exit 1; }
|
||||
else
|
||||
git diff --name-only "$remote_sha" "$local_sha" >"$files_path" \
|
||||
|| { printf '%s\n' "error: cannot diff pushed ref $local_ref" >&2; exit 1; }
|
||||
git rev-list --no-merges "$remote_sha..$local_sha" >"$commits_path" \
|
||||
|| { printf '%s\n' "error: cannot enumerate commits for pushed ref $local_ref" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
scope=$($python "$checker" --root "$repo" --scope-paths-file "$files_path" $specs_args) \
|
||||
|| { printf '%s\n' "error: cannot derive trusted Spec scope for $local_ref" >&2; exit 1; }
|
||||
touched_slugs=$(printf '%s\n' "$scope" | sed -n 's/^SPEC //p')
|
||||
|
||||
if [ -z "$touched_slugs" ]; then
|
||||
while read -r commit; do
|
||||
[ -n "$commit" ] || continue
|
||||
message=$(git show -s --format=%B "$commit" 2>/dev/null || true)
|
||||
slugs=$(printf '%s\n' "$message" | sed -nE 's/^Spec:[[:space:]]*([a-z0-9][a-z0-9_-]*[a-z0-9]|[a-z0-9])[[:space:]]*$/\1/p')
|
||||
if printf '%s\n' "$message" | grep -qE '^Spec:[[:space:]]*none[[:space:]]*(\([[:space:]]*\))?[[:space:]]*$'; then
|
||||
printf '%s\n' \
|
||||
"error: pushed commit ${commit}: 'Spec: none' lite channel requires a non-empty reason" \
|
||||
" Write 'Spec: none (<reason>)' in the commit footer and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$slugs" ]; then
|
||||
# F11 lite channel: `Spec: none (<non-empty reason>)` attributes a
|
||||
# trivial unpackaged commit. It contributes no package slug, so a
|
||||
# none-only push skips the package gate below; the full gate still
|
||||
# applies to every slug-attributed commit in the same ref.
|
||||
if printf '%s\n' "$message" | grep -qE '^Spec:[[:space:]]*none[[:space:]]*\([^()]+\)[[:space:]]*$'; then
|
||||
continue
|
||||
fi
|
||||
printf '%s\n' \
|
||||
"error: pushed commit ${commit} has no Spec: <slug> attribution" \
|
||||
" Retro-pack the work into a completed Development Record and amend the commit with its Spec footer," \
|
||||
" or attribute trivial unpackaged work with 'Spec: none (<reason>)', and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$slugs" >>"$footer_slugs"
|
||||
done <"$commits_path"
|
||||
touched_slugs=$(sort -u "$footer_slugs")
|
||||
# F9 (AUD-06): footer-attributed pushes carry the push-window base so the
|
||||
# single judge inside check_all can warn (observation period) when a cited
|
||||
# archive's recorded commit interval predates this delivery window.
|
||||
if [ "$remote_sha" != "$zero" ]; then
|
||||
footer_window_base=$(git merge-base "$remote_sha" "$local_sha" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
# A ref deletion, or a ref with no new commits/files, has nothing to gate.
|
||||
[ -n "$touched_slugs" ] || continue
|
||||
|
||||
set --
|
||||
if [ "$mode" != "all" ]; then
|
||||
for slug in $touched_slugs; do
|
||||
set -- "$@" --slug "$slug"
|
||||
done
|
||||
fi
|
||||
if [ -n "$footer_window_base" ]; then
|
||||
set -- "$@" --footer-window-base "$footer_window_base"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
set -- "$@" $specs_args
|
||||
set -- "$@" --legacy-baseline "$legacy_baseline"
|
||||
"$python" "$checker" --root "$repo" --revision "$local_sha" --require-archived "$@"
|
||||
done <"$input_path"
|
||||
Reference in new issue
Block a user