Add the local project, framework submodule, hooks and templates

After the repositories and the mirror, the script now creates the local
project: the directory, git on main, credential-free origin (and github)
remotes, and the license history from Gitea. Then it adds the framework as
a submodule over SSH, installs its skills and git hooks once (plan gate
optional), and copies the AGENTS.md and artifact registry templates.

Nothing is overwritten without a yes: an existing directory, core.hooksPath,
AGENTS.md or docs/artifact-registry.md each ask first (default no). Without
SSH the framework steps can only be skipped, after a yes. No commit is made
in the new project. New optional config key FRAMEWORK_REPO.

New library files git.sh, localproject.sh and framework.sh. Tests run real
git against local bare repositories that stand in for Gitea and the
framework, with a private git configuration (769 checks). The README is now
the full guide.

Task: MIL-003#1
Task: MIL-003#2
Task: MIL-003#3
Task: MIL-003#4
Task: MIL-003#5
Refs #15
Refs #16
Refs #17
Refs #18
Refs #19

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 15:54:02 +08:00
co-authored by Claude Sonnet 5.5
parent 736cdb42fc
commit 0030334e5e
19 changed files with 1185 additions and 68 deletions
+153
View File
@@ -0,0 +1,153 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# framework.sh - The SQA-QC-Framework in the new project: submodule, skills, hooks and templates.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: is_framework_skipped, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates
# is_framework_skipped: succeed when the framework steps are left out because
# SSH to Gitea is not available (the Maintainer agreed to that).
is_framework_skipped() {
[[ ${STATE[skip_framework]:-0} == 1 ]]
}
# add_framework: git submodule add of the framework as "framework". SSH is
# needed for it; a failure says how to test the access.
add_framework() {
local label="Framework" dir="${PROJECT[directory]}" url err existing
url="$(framework_url)"
if is_framework_skipped; then
finish_step "$label" "skipped" "(no SSH access to Gitea)"
return 0
fi
begin_step "$label"
if [[ -e $dir/framework ]]; then
existing="$(git_project "$dir" config -f .gitmodules --get submodule.framework.url || true)"
if [[ $existing != "$url" ]]; then
die "'framework' already exists in $dir and is not the framework submodule ($url)"
fi
finish_step "$label" "reused" "$url (already a submodule)"
return 0
fi
make_temp_file
err="$REPLY"
if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then
die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')"
fi
finish_step "$label" "created" "$url"
}
# run_framework_script DIR SCRIPT [ARG...]: run one of the framework's own
# scripts inside the project. PROJECT_ROOT is set explicitly so that a
# PROJECT_ROOT in the caller's environment cannot point it elsewhere.
run_framework_script() {
local dir="$1" script="$2" out abs
shift 2
abs="$(cd "$dir" && pwd)"
make_temp_file
out="$REPLY"
if ! (cd "$abs" && env PROJECT_ROOT="$abs" bash "framework/scripts/$script" "$@") >"$out" 2>&1; then
die "the framework script $script failed: $(tail -n 3 "$out" | tr '\n' ' ')"
fi
}
# install_skills DIR: install the framework's skills once. The installer is
# safe to repeat but copies everything again, so a project that already has
# them is left alone.
install_skills() {
local dir="$1"
if [[ -f $dir/.agents/skills/.framework-skills && -f $dir/.claude/skills/.framework-skills ]]; then
STATE[skills_note]="skills already installed"
return 0
fi
run_framework_script "$dir" install-skills.sh
STATE[skills_note]="skills installed"
}
# install_hooks DIR: point core.hooksPath at the framework's hooks, and turn
# on the plan gate if chosen. A different hooks path that is already set is
# only replaced after a yes.
install_hooks() {
local dir="$1" current global gate=() gate_enabled
if ((PROJECT[is_plan_gate_enabled])); then
gate=(--enable-plan-gate)
fi
# Both settings are normally unset; git config exits 1 then.
current="$(git_project "$dir" config --local --get core.hooksPath || true)"
global="$(git_project "$dir" config --global --get core.hooksPath || true)"
gate_enabled="$(git_project "$dir" config --local --get planGate.enabled || true)"
if [[ -z $current && -n $global ]]; then
warn "your global core.hooksPath is '$global'; this project sets its own, which takes precedence here"
fi
if [[ -z $current ]]; then
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
STATE[hooks_note]="hooks installed"
elif [[ $current == framework/githooks ]]; then
STATE[hooks_note]="hooks already installed"
if ((PROJECT[is_plan_gate_enabled])) && [[ $gate_enabled != true ]]; then
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
fi
else
prompt_yes_no "core.hooksPath is already '$current'. Replace it with framework/githooks" n
if ((REPLY)); then
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
STATE[hooks_note]="hooks installed (replaced '$current')"
else
STATE[hooks_note]="kept the existing hooks path '$current'"
if ((PROJECT[is_plan_gate_enabled])); then
warn "the plan gate is not enabled because the framework hooks were not installed"
fi
fi
fi
}
# install_framework: skills, then hooks (and the plan gate). Each is done once.
install_framework() {
local label="Skills and hooks" dir="${PROJECT[directory]}" gate_state="plan gate off"
if is_framework_skipped; then
finish_step "$label" "skipped" "(no SSH access to Gitea)"
return 0
fi
begin_step "$label"
install_skills "$dir"
install_hooks "$dir"
if [[ $(git_project "$dir" config --local --get planGate.enabled || true) == true ]]; then
gate_state="plan gate on"
fi
finish_step "$label" "created" "(${STATE[skills_note]}; ${STATE[hooks_note]}; $gate_state)"
}
# copy_template DIR SOURCE TARGET: copy a framework template. An existing
# target is only replaced after a yes. The result goes to STATE[template_note].
copy_template() {
local dir="$1" source="$2" target="$3"
if [[ ! -f $dir/$source ]]; then
die "the framework has no $source; is the submodule complete?"
fi
if [[ -e $dir/$target ]]; then
prompt_yes_no "$target already exists. Replace it with the framework template" n
if ! ((REPLY)); then
STATE[template_note]="kept existing $target"
return 0
fi
fi
cp -- "$dir/$source" "$dir/$target"
STATE[template_note]="copied $target"
}
# copy_templates: AGENTS.md and docs/artifact-registry.md from the framework.
copy_templates() {
local label="Templates" dir="${PROJECT[directory]}" notes=""
if is_framework_skipped; then
finish_step "$label" "skipped" "(no SSH access to Gitea)"
return 0
fi
begin_step "$label"
mkdir -p -- "$dir/docs"
copy_template "$dir" framework/templates/AGENTS-template.md AGENTS.md
notes="${STATE[template_note]}"
copy_template "$dir" framework/templates/artifact-registry-template.md docs/artifact-registry.md
notes="$notes; ${STATE[template_note]}"
finish_step "$label" "created" "($notes)"
}