Skip to content

Commit 9ee84d1

Browse files
Add four-layer docs guardrails with transitional pre-push mode.
Introduce git hooks, unified QA gate, CI workflow, and Cursor workflow docs so publishing is blocked until validation passes, while allowing push-range checks for the historical wip branch. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3e248ca commit 9ee84d1

12 files changed

Lines changed: 971 additions & 2 deletions

File tree

.cursor/rules/devdocs-hook-reference-quality.mdc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ Use this when editing generated or manually corrected hook reference pages.
1616
- Module links use native module repositories, not `PrestaShop/PrestaShop/modules/...`.
1717
- Do not use `files: {}` when a reviewer or source code identifies a real origin.
1818
- Delete pages for hooks that no longer dispatch in the target branch.
19+
- Never accept branch-only replacements (`8.x` -> `9.1.x`) without path existence verification.
20+
- If origin is `module`, `files[].url` must not be empty.
1921

2022
## Call Example Rules
2123

2224
- The call block must show the actual hook invocation, not adjacent code.
2325
- Include the full current parameter array, including `id_shop`, `chain`, by-reference values, and return flags when present.
2426
- Do not keep truncated fragments like only the hook name or first array keys.
2527
- Use `twig` fences for `renderhook(...)` and `php` fences for `Hook::exec(...)`.
28+
- Use `smarty` fences for Smarty `{hook h='...'}` snippets.
2629
- Remove copied constants, comments, copyright headers, `include(...)`, layout markup, and unrelated code.
2730
- Smarty `{hook ...}` snippets must not end with `;`.
2831

@@ -31,4 +34,4 @@ Use this when editing generated or manually corrected hook reference pages.
3134
- Search all changed hook pages for the same pattern after fixing one comment.
3235
- Verify deleted/empty-source candidates with source search, not only `hook.xml`.
3336
- Treat allow-list constants as references, not proof that a hook is dispatched.
34-
- Re-run hook validation, source-link checks, and Hugo build before push.
37+
- Re-run `scripts/qa_docs_gate.py` and Hugo build before push.

.cursor/rules/devdocs-systematic-quality-gates.mdc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ alwaysApply: true
77

88
Apply this to every docs area: hooks, Admin API, modules, themes, webservice, basics, contribution guides, and all version branches.
99

10+
## Four-Layer Protection Model
11+
12+
- Layer 1, Git controls: use `origin` as fetch-only and push only via `publish`.
13+
- Layer 1, Git hooks: `pre-commit` and `pre-push` must be active through `core.hooksPath=.githooks`.
14+
- Layer 2, QA gate: run `scripts/qa_docs_gate.py` as the single source of validation truth.
15+
- Layer 3, Cursor workflow: always run full-scope audits, not point fixes.
16+
- Layer 4, GitHub protection: require PR checks (`build` and `docs-qa`) before merge.
17+
1018
## Work Systemically
1119

1220
- Treat review comments and maintainer follow-up commits as examples of a class of issue.
@@ -31,10 +39,22 @@ Apply this to every docs area: hooks, Admin API, modules, themes, webservice, ba
3139

3240
## Verification Before Push
3341

34-
- Run area-specific validators and a CI-equivalent Hugo build.
42+
- Run the unified gate: `python3 scripts/qa_docs_gate.py --scope changed --base-ref upstream/9.x --rebuild-index`.
43+
- Run final gate before push: `make qa-final` (strict) or `make qa-final-transitional` (listed transitional branches).
44+
- Ensure `pre-commit` performs Hugo build and hook quality checks.
3545
- Check changed links and examples against source.
3646
- Do not push until validation passes.
3747

48+
## Known Failure Patterns
49+
50+
- Branch-only URL replacement (`8.x` to `9.1.x`) without verifying path existence.
51+
- Module origins linked to `PrestaShop/PrestaShop/modules/...` instead of native module repositories.
52+
- `files: {}` used even though source or reviewer provides a concrete origin file.
53+
- Hook page kept while hook is obsolete and has no valid origin in target branch.
54+
- Call snippet includes unrelated context or misses required invocation content.
55+
- Wrong code fence language for snippet type (`php`, `twig`, `smarty`).
56+
- Truncated `Hook::exec(...)` examples with incomplete parameter arrays.
57+
3858
## Continuous Improvement
3959

4060
When a new failure pattern is discovered:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Docs Safe Workflow
2+
3+
Use this skill for any PrestaShop documentation update, especially hook metadata, migration work, and review-comment follow-up.
4+
5+
## Goals
6+
7+
- Enforce systemic quality, not point fixes.
8+
- Ensure links, snippets, and metadata stay valid for the target branch.
9+
- Run deterministic QA before commit and before push.
10+
11+
## Push policy modes
12+
13+
- **Strict (default):** all `feat/*` and `fix/*` branches. Full branch diff from `upstream/9.x`, `feat/*`/`fix/*` naming required, `make qa-final`.
14+
- **Transitional:** branches listed in `.githooks/push-policy.json`. Checks only the push range (new commits), allows `local/*` when explicitly listed, uses `make qa-final-transitional`.
15+
16+
## Mandatory Sequence
17+
18+
1. Identify target branch and source-of-truth repositories.
19+
2. Perform a full-scope audit for the changed area.
20+
3. Apply changes only after mapping all similar occurrences.
21+
4. Run:
22+
- `python3 scripts/qa_docs_gate.py --scope changed --base-ref upstream/9.x --rebuild-index`
23+
- local Hugo build via pre-commit hook
24+
5. Before push, run final gate:
25+
- strict branch: `make qa-final`
26+
- transitional branch (listed in `.githooks/push-policy.json`): `make qa-final-transitional`
27+
28+
## Policies
29+
30+
- No partial fixes: if one pattern is broken, check full scope for the same pattern.
31+
- Review comments are examples, not full scope.
32+
- Do not replace branch names in URLs without path existence checks.
33+
- Do not link module origins to `PrestaShop/PrestaShop/modules/...`.
34+
- Do not keep obsolete hook pages with no valid source.
35+
- Do not keep truncated call snippets or wrong code fence languages.

.githooks/pre-commit

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ "${SKIP_DOCS_PRECOMMIT:-0}" == "1" ]]; then
5+
echo "[pre-commit] SKIP_DOCS_PRECOMMIT=1, skipping checks."
6+
exit 0
7+
fi
8+
9+
REPO_ROOT="$(git rev-parse --show-toplevel)"
10+
STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACMR || true)"
11+
12+
if [[ -z "$STAGED_FILES" ]]; then
13+
echo "[pre-commit] No staged files, skipping."
14+
exit 0
15+
fi
16+
17+
if ! echo "$STAGED_FILES" | rg -q '\.md$|^\.github/workflows/build\.yml$'; then
18+
echo "[pre-commit] No docs-related staged files, skipping."
19+
exit 0
20+
fi
21+
22+
echo "[pre-commit] Running docs QA gate (staged scope)..."
23+
python3 "$REPO_ROOT/scripts/qa_docs_gate.py" --scope staged --rebuild-index
24+
25+
if [[ "${SKIP_HUGO_PRECOMMIT:-0}" == "1" ]]; then
26+
echo "[pre-commit] SKIP_HUGO_PRECOMMIT=1, skipping Hugo build."
27+
exit 0
28+
fi
29+
30+
DEV_SITE="$REPO_ROOT/.reference/devdocs-site"
31+
HUGO_IMAGE="${HUGO_DOCKER_IMAGE:-hugomods/hugo:exts-0.121.1}"
32+
VERSION="${DEVDOCS_VERSION:-9}"
33+
34+
echo "[pre-commit] Running local CI-like Hugo build for /$VERSION..."
35+
36+
if [[ ! -d "$DEV_SITE/.git" ]]; then
37+
git clone --depth 1 https://github.com/PrestaShop/devdocs-site.git "$DEV_SITE"
38+
fi
39+
40+
git -C "$DEV_SITE" submodule update --init --depth 1 src/themes/ps-docs-theme
41+
42+
if [[ ! -d "$DEV_SITE/src/content/1.7/.git" ]]; then
43+
rm -rf "$DEV_SITE/src/content/1.7"
44+
git clone --depth 1 --branch 1.7.x https://github.com/PrestaShop/docs.git "$DEV_SITE/src/content/1.7"
45+
fi
46+
47+
if [[ ! -d "$DEV_SITE/src/content/8/.git" ]]; then
48+
rm -rf "$DEV_SITE/src/content/8"
49+
git clone --depth 1 --branch 8.x https://github.com/PrestaShop/docs.git "$DEV_SITE/src/content/8"
50+
fi
51+
52+
TARGET="$DEV_SITE/src/content/$VERSION"
53+
rm -rf "$TARGET"
54+
mkdir -p "$(dirname "$TARGET")"
55+
56+
rsync -a --delete \
57+
--exclude ".git" \
58+
--exclude ".reference" \
59+
--exclude ".cursor" \
60+
--exclude ".specstory" \
61+
--exclude ".vscode" \
62+
--exclude "node_modules" \
63+
"$REPO_ROOT/" "$TARGET/"
64+
65+
docker run --rm -v "$DEV_SITE:/site" -w /site/src "$HUGO_IMAGE" hugo >/dev/null
66+
67+
echo "[pre-commit] Hugo build passed."

.githooks/pre-push

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ "${SKIP_DOCS_PREPUSH:-0}" == "1" ]]; then
5+
echo "[pre-push] SKIP_DOCS_PREPUSH=1, skipping checks."
6+
exit 0
7+
fi
8+
9+
REPO_ROOT="$(git rev-parse --show-toplevel)"
10+
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
11+
HEAD_SHA="$(git rev-parse HEAD)"
12+
BASE_REF="${DOCS_BASE_REF:-upstream/9.x}"
13+
POLICY_MODE="$(python3 "$REPO_ROOT/scripts/docs_push_policy.py" "$BRANCH")"
14+
15+
PUSH_LOCAL_SHA="$HEAD_SHA"
16+
REMOTE_SHA=""
17+
while read -r _ local_sha _ remote_sha; do
18+
[[ -z "${local_sha:-}" ]] && continue
19+
PUSH_LOCAL_SHA="$local_sha"
20+
if [[ "${remote_sha:-}" =~ ^0+$ ]]; then
21+
REMOTE_SHA=""
22+
else
23+
REMOTE_SHA="$remote_sha"
24+
fi
25+
done
26+
27+
if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
28+
BASE_REF="origin/9.x"
29+
fi
30+
31+
if [[ "$POLICY_MODE" == "transitional" ]]; then
32+
echo "[pre-push] Transitional mode enabled for branch '$BRANCH'."
33+
if [[ -n "$REMOTE_SHA" ]]; then
34+
DIFF_RANGE="${REMOTE_SHA}..${PUSH_LOCAL_SHA}"
35+
elif git rev-parse --verify "refs/remotes/publish/${BRANCH}" >/dev/null 2>&1; then
36+
DIFF_RANGE="refs/remotes/publish/${BRANCH}..${PUSH_LOCAL_SHA}"
37+
elif git rev-parse --verify "refs/remotes/origin/${BRANCH}" >/dev/null 2>&1; then
38+
DIFF_RANGE="refs/remotes/origin/${BRANCH}..${PUSH_LOCAL_SHA}"
39+
else
40+
UPSTREAM_REF="$(git rev-parse --abbrev-ref --symbolic-full-name "${BRANCH}@{upstream}" 2>/dev/null || true)"
41+
UPSTREAM_BRANCH="${UPSTREAM_REF##*/}"
42+
if [[ -n "$UPSTREAM_REF" && "$UPSTREAM_BRANCH" == "$BRANCH" ]] && git rev-parse --verify "$UPSTREAM_REF" >/dev/null 2>&1; then
43+
DIFF_RANGE="${UPSTREAM_REF}..${PUSH_LOCAL_SHA}"
44+
else
45+
DIFF_RANGE=""
46+
fi
47+
fi
48+
else
49+
echo "[pre-push] Strict mode for branch '$BRANCH'."
50+
DIFF_RANGE="${BASE_REF}...HEAD"
51+
fi
52+
53+
if [[ "$POLICY_MODE" != "transitional" ]]; then
54+
if [[ "$BRANCH" =~ ^(local|wip|scratch)/ ]]; then
55+
echo "[pre-push] Push is blocked for branch '$BRANCH'."
56+
echo "[pre-push] Rename branch to feat/* or fix/* before publishing."
57+
exit 1
58+
fi
59+
60+
if [[ ! "$BRANCH" =~ ^(feat|fix)/ ]]; then
61+
echo "[pre-push] Branch '$BRANCH' does not match allowed patterns feat/* or fix/*."
62+
exit 1
63+
fi
64+
fi
65+
66+
FORBIDDEN_RE='^(\.specstory/|tmp_groups/|\.cursor/plans/|\.reference/)'
67+
if [[ "$POLICY_MODE" == "transitional" && -z "$DIFF_RANGE" ]]; then
68+
echo "[pre-push] Transitional mode: no remote push range resolved; skipping forbidden-path scan."
69+
echo "[pre-push] Set upstream to the same branch on publish/origin or pass PUSH_RANGE to make qa-final-transitional."
70+
elif [[ -n "$DIFF_RANGE" ]] && git diff --name-only --diff-filter=ACMR "$DIFF_RANGE" | rg -q "$FORBIDDEN_RE"; then
71+
echo "[pre-push] Found forbidden paths in checked diff range ($DIFF_RANGE):"
72+
git diff --name-only --diff-filter=ACMR "$DIFF_RANGE" | rg "$FORBIDDEN_RE" || true
73+
exit 1
74+
fi
75+
76+
STAMP_FILE="$REPO_ROOT/.git/qa/docs_gate_final_${BRANCH//\//__}.json"
77+
if [[ ! -f "$STAMP_FILE" ]]; then
78+
echo "[pre-push] Missing final QA stamp for branch '$BRANCH'."
79+
if [[ "$POLICY_MODE" == "transitional" ]]; then
80+
echo "[pre-push] Run:"
81+
echo " make qa-final-transitional"
82+
else
83+
echo "[pre-push] Run:"
84+
echo " make qa-final"
85+
fi
86+
exit 1
87+
fi
88+
89+
read_stamp_field() {
90+
python3 - "$STAMP_FILE" "$1" <<'PY'
91+
import json, pathlib, sys
92+
p = pathlib.Path(sys.argv[1])
93+
field = sys.argv[2]
94+
data = json.loads(p.read_text(encoding="utf-8"))
95+
print(data.get(field, ""))
96+
PY
97+
}
98+
99+
STAMP_SHA="$(read_stamp_field sha)"
100+
STAMP_MODE="$(read_stamp_field mode)"
101+
STAMP_RANGE="$(read_stamp_field diff_range)"
102+
103+
if [[ "$STAMP_SHA" != "$HEAD_SHA" ]]; then
104+
echo "[pre-push] Final QA stamp is stale."
105+
echo "[pre-push] stamped_sha=$STAMP_SHA"
106+
echo "[pre-push] current_sha=$HEAD_SHA"
107+
echo "[pre-push] Re-run final QA after latest commit."
108+
exit 1
109+
fi
110+
111+
if [[ "$POLICY_MODE" == "transitional" ]]; then
112+
if [[ "$STAMP_MODE" != "transitional" ]]; then
113+
echo "[pre-push] Branch requires transitional QA stamp (mode=transitional)."
114+
echo "[pre-push] Run: make qa-final-transitional"
115+
exit 1
116+
fi
117+
else
118+
if [[ -n "$STAMP_MODE" && "$STAMP_MODE" != "strict" ]]; then
119+
echo "[pre-push] Branch requires strict QA stamp (mode=strict)."
120+
echo "[pre-push] Run: make qa-final"
121+
exit 1
122+
fi
123+
fi
124+
125+
if [[ "$POLICY_MODE" == "transitional" && -n "$STAMP_RANGE" && -n "$DIFF_RANGE" && "$STAMP_RANGE" != "$DIFF_RANGE" ]]; then
126+
echo "[pre-push] Transitional QA stamp range does not match push range."
127+
echo "[pre-push] stamped_range=$STAMP_RANGE"
128+
echo "[pre-push] push_range=$DIFF_RANGE"
129+
echo "[pre-push] Re-run: make qa-final-transitional PUSH_RANGE='$DIFF_RANGE'"
130+
exit 1
131+
fi
132+
133+
echo "[pre-push] All push gates passed ($POLICY_MODE mode${DIFF_RANGE:+, range: $DIFF_RANGE})."

.githooks/push-policy.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"version": 1,
3+
"default_mode": "strict",
4+
"transitional_branches": ["local/wip-docs-artifacts"],
5+
"notes": "Transitional mode validates only commits/files in the push range. Remove branches from transitional_branches once rebased onto a clean feat/* or fix/* branch."
6+
}

.github/workflows/docs-qa.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Docs QA Gate
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- "9.x"
8+
- "feat/**"
9+
- "fix/**"
10+
11+
jobs:
12+
qa:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Setup Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.11"
24+
25+
- name: Ensure reference mirrors
26+
run: |
27+
mkdir -p .reference
28+
[[ -d .reference/prestashop/.git ]] || git clone --depth 1 --branch 9.1.x https://github.com/PrestaShop/PrestaShop.git .reference/prestashop
29+
[[ -d .reference/classic-theme/.git ]] || git clone --depth 1 --branch develop https://github.com/PrestaShop/classic-theme.git .reference/classic-theme
30+
[[ -d .reference/hummingbird/.git ]] || git clone --depth 1 --branch develop https://github.com/PrestaShop/hummingbird.git .reference/hummingbird
31+
32+
- name: Resolve base branch ref
33+
id: base
34+
run: |
35+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
36+
echo "ref=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
37+
git fetch origin "${{ github.base_ref }}" --depth=1
38+
else
39+
echo "ref=origin/9.x" >> "$GITHUB_OUTPUT"
40+
git fetch origin 9.x --depth=1
41+
fi
42+
43+
- name: Run docs QA gate
44+
run: |
45+
python3 scripts/qa_docs_gate.py \
46+
--scope changed \
47+
--base-ref "${{ steps.base.outputs.ref }}" \
48+
--rebuild-index

0 commit comments

Comments
 (0)