Compare commits

..

16 Commits

Author SHA1 Message Date
3086f333ed Document cockpit local, NAS supervision, and ops workflow
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-18 18:21:52 +01:00
c1c3c19d13 Merge pull request 'Use Europe/Paris build time for ops health manifest' (#278) from ops/builtat-paris-time into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 30s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 2s
Reviewed-on: #278
2026-03-18 11:14:11 +01:00
ddcd0acd4d Use Europe/Paris build time for ops health manifest
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 49s
2026-03-18 11:11:54 +01:00
9bc4eeb3e7 Merge pull request 'Add ops health manifest to deploy pipeline' (#277) from ops/ops-health-deploy into main
All checks were successful
CI / build-and-anchors (push) Successful in 44s
Proposer Apply (Queue) / apply-proposer (push) Successful in 19s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 11m8s
Reviewed-on: #277
2026-03-18 10:31:32 +01:00
7a9a5319ac Add ops health manifest to deploy pipeline
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 58s
CI / build-and-anchors (pull_request) Successful in 1m3s
2026-03-18 10:28:13 +01:00
7d75de5c9f Merge pull request 'docs: formalize localhost auto-sync architecture' (#276) from docs/localhost-auto-sync into main
All checks were successful
CI / build-and-anchors (push) Successful in 51s
Proposer Apply (Queue) / apply-proposer (push) Successful in 19s
SMOKE / smoke (push) Successful in 6s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
Reviewed-on: #276
2026-03-16 21:19:51 +01:00
69c91cb661 docs: formalize localhost auto-sync architecture
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 47s
CI / build-and-anchors (pull_request) Successful in 45s
2026-03-16 21:14:41 +01:00
a1bfbf4405 Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#275) from bot/proposer-273-ce91b3d8cf02 into main
All checks were successful
CI / build-and-anchors (push) Successful in 44s
Proposer Apply (Queue) / apply-proposer (push) Successful in 33s
SMOKE / smoke (push) Successful in 12s
Deploy staging+live (annotations) / deploy (push) Successful in 9m57s
Reviewed-on: #275
2026-03-16 16:46:20 +01:00
archicratie-bot
be26b425d8 edit: apply ticket #274 (/archicrat-ia/prologue/#p-23-d91a7b78)
All checks were successful
CI / build-and-anchors (push) Successful in 51s
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-16 15:43:09 +00:00
archicratie-bot
abf88e7037 edit: apply ticket #273 (/archicrat-ia/prologue/#p-22-a416d473) 2026-03-16 15:42:47 +00:00
04fee32fdb Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#272) from bot/proposer-270-39655773c199 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 26s
CI / build-and-anchors (push) Successful in 43s
SMOKE / smoke (push) Successful in 8s
Deploy staging+live (annotations) / deploy (push) Successful in 9m20s
Reviewed-on: #272
2026-03-16 13:57:16 +01:00
archicratie-bot
fbddf5c3fc edit: apply ticket #271 (/archicrat-ia/prologue/#p-17-b8c5bf21)
All checks were successful
CI / build-and-anchors (push) Successful in 50s
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (pull_request) Successful in 46s
2026-03-16 12:54:46 +00:00
archicratie-bot
bad748df3a edit: apply ticket #270 (/archicrat-ia/prologue/#p-7-64a0ca9c) 2026-03-16 12:54:21 +00:00
0066cf8601 Merge pull request 'fix(actions): harden proposer queue against duplicate batch PRs' (#269) from hotfix/proposer-close-verify into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 21s
Deploy staging+live (annotations) / deploy (push) Successful in 33s
CI / build-and-anchors (push) Successful in 44s
SMOKE / smoke (push) Successful in 4s
Reviewed-on: #269
2026-03-16 13:42:52 +01:00
5d3473d66c fix(actions): harden proposer queue against duplicate batch PRs
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-16 13:39:09 +01:00
f9d34110e4 Merge pull request 'proposer: apply 2 tickets on /archicrat-ia/prologue/' (#266) from bot/proposer-264-20260316-120249 into main
All checks were successful
Proposer Apply (Queue) / apply-proposer (push) Successful in 20s
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 7s
Deploy staging+live (annotations) / deploy (push) Successful in 7m47s
Reviewed-on: #266
2026-03-16 13:18:27 +01:00
8 changed files with 2172 additions and 177 deletions

View File

@@ -297,6 +297,19 @@ jobs:
docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true
docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true
BUILD_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
PUBLIC_OPS_ENV=staging \
PUBLIC_OPS_UPSTREAM=web_blue \
PUBLIC_BUILD_SHA="${AFTER}" \
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
node scripts/write-ops-health.mjs
test -f public/__ops/health.json
echo "=== public/__ops/health.json (blue/staging) ==="
cat public/__ops/health.json
docker compose -p "$PROJ" -f docker-compose.yml build web_blue docker compose -p "$PROJ" -f docker-compose.yml build web_blue
docker rm -f archicratie-web-blue || true docker rm -f archicratie-web-blue || true
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue
@@ -306,6 +319,11 @@ jobs:
wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index" wait_url "http://127.0.0.1:8081/annotations-index.json" "blue annotations-index"
wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js" wait_url "http://127.0.0.1:8081/pagefind/pagefind.js" "blue pagefind.js"
wait_url "http://127.0.0.1:8081/__ops/health.json" "blue ops health"
curl -fsS --max-time 6 "http://127.0.0.1:8081/__ops/health.json" \
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(blue)=$CANON" echo "canonical(blue)=$CANON"
echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || { echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || {
@@ -353,6 +371,19 @@ jobs:
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true
} }
BUILD_TIME_RAW="$(TZ=Europe/Paris date '+%Y-%m-%dT%H:%M:%S%z')"
BUILD_TIME="${BUILD_TIME_RAW:0:${#BUILD_TIME_RAW}-2}:${BUILD_TIME_RAW:${#BUILD_TIME_RAW}-2}"
PUBLIC_OPS_ENV=prod \
PUBLIC_OPS_UPSTREAM=web_green \
PUBLIC_BUILD_SHA="${AFTER}" \
PUBLIC_BUILD_TIME="${BUILD_TIME}" \
node scripts/write-ops-health.mjs
test -f public/__ops/health.json
echo "=== public/__ops/health.json (green/prod) ==="
cat public/__ops/health.json
# build/restart green # build/restart green
if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then
echo "❌ build green failed"; rollback; exit 4 echo "❌ build green failed"; rollback; exit 4
@@ -366,6 +397,11 @@ jobs:
if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi if ! wait_url "http://127.0.0.1:8082/annotations-index.json" "green annotations-index"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi if ! wait_url "http://127.0.0.1:8082/pagefind/pagefind.js" "green pagefind.js"; then rollback; exit 4; fi
if ! wait_url "http://127.0.0.1:8082/__ops/health.json" "green ops health"; then rollback; exit 4; fi
curl -fsS --max-time 6 "http://127.0.0.1:8082/__ops/health.json" \
| python3 -c 'import sys, json; j=json.load(sys.stdin); print("env=", j.get("env")); print("upstream=", j.get("upstream")); print("buildSha=", j.get("buildSha")); print("builtAt=", j.get("builtAt"))'
CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" CANON="$(curl -fsS --max-time 6 "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)"
echo "canonical(green)=$CANON" echo "canonical(green)=$CANON"
echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || { echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || {

View File

@@ -130,8 +130,6 @@ jobs:
echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}" echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}"
if [[ "$EVENT_NAME" == "issues" ]]; then if [[ "$EVENT_NAME" == "issues" ]]; then
# Gitea peut fournir un payload "issues/labeled" sans label exploitable.
# On ne skip QUE si le label est explicitement présent ET différent de state/approved.
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip" echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip"
echo 'SKIP=1' >> /tmp/proposer.env echo 'SKIP=1' >> /tmp/proposer.env
@@ -218,6 +216,43 @@ jobs:
echo "Target batch:" echo "Target batch:"
grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env
- name: Derive deterministic batch identity
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
export TARGET_ISSUES TARGET_CHEMIN
node --input-type=module - <<'NODE'
import fs from "node:fs";
import crypto from "node:crypto";
const issues = String(process.env.TARGET_ISSUES || "")
.trim()
.split(/\s+/)
.filter(Boolean)
.sort((a, b) => Number(a) - Number(b));
const chemin = String(process.env.TARGET_CHEMIN || "").trim();
const keySource = `${chemin}::${issues.join(",")}`;
const hash = crypto.createHash("sha1").update(keySource).digest("hex").slice(0, 12);
const primary = issues[0] || "0";
const batchBranch = `bot/proposer-${primary}-${hash}`;
fs.appendFileSync(
"/tmp/proposer.env",
[
`BATCH_KEY=${JSON.stringify(keySource)}`,
`BATCH_HASH=${JSON.stringify(hash)}`,
`BATCH_BRANCH=${JSON.stringify(batchBranch)}`
].join("\n") + "\n"
);
NODE
echo "Batch identity:"
grep -E '^(BATCH_KEY|BATCH_HASH|BATCH_BRANCH)=' /tmp/proposer.env
- name: Inspect open proposer PRs - name: Inspect open proposer PRs
env: env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }} FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
@@ -233,6 +268,8 @@ jobs:
-o /tmp/open_pulls.json -o /tmp/open_pulls.json
export TARGET_ISSUES="${TARGET_ISSUES:-}" export TARGET_ISSUES="${TARGET_ISSUES:-}"
export BATCH_BRANCH="${BATCH_BRANCH:-}"
export BATCH_KEY="${BATCH_KEY:-}"
node --input-type=module - <<'NODE' >> /tmp/proposer.env node --input-type=module - <<'NODE' >> /tmp/proposer.env
import fs from "node:fs"; import fs from "node:fs";
@@ -243,15 +280,21 @@ jobs:
.split(/\s+/) .split(/\s+/)
.filter(Boolean); .filter(Boolean);
const batchBranch = String(process.env.BATCH_BRANCH || "");
const batchKey = String(process.env.BATCH_KEY || "");
const proposerOpen = Array.isArray(pulls) const proposerOpen = Array.isArray(pulls)
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-")) ? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
: []; : [];
const current = proposerOpen.find((pr) => { const sameBatch = proposerOpen.find((pr) => {
const ref = String(pr?.head?.ref || ""); const ref = String(pr?.head?.ref || "");
const title = String(pr?.title || ""); const title = String(pr?.title || "");
const body = String(pr?.body || ""); const body = String(pr?.body || "");
if (batchBranch && ref === batchBranch) return true;
if (batchKey && body.includes(`Batch-Key: ${batchKey}`)) return true;
return issues.some((n) => return issues.some((n) =>
ref.startsWith(`bot/proposer-${n}-`) || ref.startsWith(`bot/proposer-${n}-`) ||
title.includes(`#${n}`) || title.includes(`#${n}`) ||
@@ -262,10 +305,11 @@ jobs:
const out = []; const out = [];
if (current) { if (sameBatch) {
out.push("SKIP=1"); out.push("SKIP=1");
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`); out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
out.push(`OPEN_PR_URL=${JSON.stringify(String(current.html_url || current.url || ""))}`); out.push(`OPEN_PR_URL=${JSON.stringify(String(sameBatch.html_url || sameBatch.url || ""))}`);
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(sameBatch?.head?.ref || ""))}`);
} else if (proposerOpen.length > 0) { } else if (proposerOpen.length > 0) {
const first = proposerOpen[0]; const first = proposerOpen[0];
out.push("SKIP=1"); out.push("SKIP=1");
@@ -277,6 +321,22 @@ jobs:
process.stdout.write(out.join("\n") + (out.length ? "\n" : "")); process.stdout.write(out.join("\n") + (out.length ? "\n" : ""));
NODE NODE
- name: Guard on remote batch branch before heavy work
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
if git ls-remote --exit-code --heads origin "$BATCH_BRANCH" >/dev/null 2>&1; then
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="batch_branch_exists_without_pr"' >> /tmp/proposer.env
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
echo "Remote batch branch already exists -> skip duplicate materialization"
exit 0
fi
echo "Remote batch branch is free"
- name: Comment issue if queued / skipped - name: Comment issue if queued / skipped
if: ${{ always() }} if: ${{ always() }}
env: env:
@@ -306,7 +366,13 @@ jobs:
MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main." MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main."
;; ;;
issue_already_has_open_pr) issue_already_has_open_pr)
MSG="This ticket already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}" MSG="This batch already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
;;
batch_branch_exists_without_pr)
MSG="This batch already has a remote batch branch (${OPEN_PR_BRANCH:-"(unknown branch)"}). Manual inspection is required before any new proposer PR is created."
;;
batch_branch_already_materialized)
MSG="This batch was already materialized by another run on branch ${OPEN_PR_BRANCH:-"(unknown branch)"}. No duplicate PR was created."
;; ;;
explicit_issue_missing_chemin) explicit_issue_missing_chemin)
MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable." MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable."
@@ -328,6 +394,7 @@ jobs:
;; ;;
esac esac
export MSG
node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json
const msg = process.env.MSG || ""; const msg = process.env.MSG || "";
process.stdout.write(JSON.stringify({ body: msg })); process.stdout.write(JSON.stringify({ body: msg }));
@@ -384,8 +451,7 @@ jobs:
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}" git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
START_SHA="$(git rev-parse HEAD)" START_SHA="$(git rev-parse HEAD)"
TS="$(date -u +%Y%m%d-%H%M%S)" BR="$BATCH_BRANCH"
BR="bot/proposer-${TARGET_PRIMARY_ISSUE}-${TS}"
echo "BRANCH=$BR" >> /tmp/proposer.env echo "BRANCH=$BR" >> /tmp/proposer.env
git checkout -b "$BR" git checkout -b "$BR"
@@ -518,6 +584,27 @@ jobs:
--data-binary @/tmp/proposer.failure.comment.json || true --data-binary @/tmp/proposer.failure.comment.json || true
done done
- name: Late guard against duplicate batch materialization
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${REBASE_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
REMOTE_SHA="$(git ls-remote --heads origin "$BATCH_BRANCH" | awk 'NR==1 {print $1}')"
if [[ -n "${REMOTE_SHA:-}" && "${REMOTE_SHA}" != "${END_SHA:-}" ]]; then
echo 'SKIP=1' >> /tmp/proposer.env
echo 'SKIP_REASON="batch_branch_already_materialized"' >> /tmp/proposer.env
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
echo "Remote batch branch already exists at $REMOTE_SHA -> skip duplicate push/PR"
exit 0
fi
echo "Late guard OK"
- name: Push bot branch - name: Push bot branch
if: ${{ always() }} if: ${{ always() }}
env: env:
@@ -557,13 +644,39 @@ jobs:
test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; } test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; }
OPEN_PRS_JSON="$(curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100")"
export OPEN_PRS_JSON BATCH_BRANCH BATCH_KEY
EXISTING_PR_URL="$(node --input-type=module -e '
const pulls = JSON.parse(process.env.OPEN_PRS_JSON || "[]");
const branch = String(process.env.BATCH_BRANCH || "");
const key = String(process.env.BATCH_KEY || "");
const current = Array.isArray(pulls)
? pulls.find((pr) => {
const ref = String(pr?.head?.ref || "");
const body = String(pr?.body || "");
return (branch && ref === branch) || (key && body.includes(`Batch-Key: ${key}`));
})
: null;
process.stdout.write(current ? String(current.html_url || current.url || "") : "");
')"
if [[ -n "${EXISTING_PR_URL:-}" ]]; then
echo "PR already exists for this batch: $EXISTING_PR_URL"
exit 0
fi
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}" PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
else else
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}" PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
fi fi
export PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER export PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER BATCH_KEY
node --input-type=module -e ' node --input-type=module -e '
import fs from "node:fs"; import fs from "node:fs";
@@ -581,6 +694,7 @@ jobs:
...issues.map((n) => ` - #${n}`), ...issues.map((n) => ` - #${n}`),
`- Branche: ${process.env.BRANCH || ""}`, `- Branche: ${process.env.BRANCH || ""}`,
`- Commit: ${process.env.END_SHA || "unknown"}`, `- Commit: ${process.env.END_SHA || "unknown"}`,
`- Batch-Key: ${process.env.BATCH_KEY || ""}`,
"", "",
"Merge si CI OK." "Merge si CI OK."
].join("\n"); ].join("\n");
@@ -597,97 +711,6 @@ jobs:
); );
' '
echo "Creating proposer PR..."
PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
--data-binary @/tmp/proposer.pr.json)"
PR_URL="$(node --input-type=module -e 'const pr = JSON.parse(process.argv[1] || "{}"); console.log(pr.html_url || pr.url || "");' "$PR_JSON")"
test -n "$PR_URL" || {
echo "PR URL missing. Raw: $PR_JSON"
exit 1
}
echo "PR created: $PR_URL"
for ISSUE in $TARGET_ISSUES; do
export ISSUE PR_URL
node --input-type=module -e '
import fs from "node:fs";
const issue = process.env.ISSUE || "";
const url = process.env.PR_URL || "";
const msg =
`PR proposer créée pour le ticket #${issue} : ${url}\n\n` +
`Le ticket est clôturé automatiquement ; la discussion peut se poursuivre dans la PR.`;
fs.writeFileSync(
"/tmp/proposer.issue.close.comment.json",
JSON.stringify({ body: msg })
);
'
echo "Commenting issue #$ISSUE ..."
COMMENT_HTTP="$(curl -sS -o /tmp/proposer.comment.out.json -w '%{http_code}' -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE/comments" \
--data-binary @/tmp/proposer.issue.close.comment.json || true)"
echo "Issue #$ISSUE comment HTTP=$COMMENT_HTTP"
if [[ ! "$COMMENT_HTTP" =~ ^2 ]]; then
echo "Failed to comment issue #$ISSUE"
cat /tmp/proposer.comment.out.json || true
exit 1
fi
echo "Closing issue #$ISSUE ..."
CLOSE_HTTP="$(curl -sS -o /tmp/proposer.close.out.json -w '%{http_code}' -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
--data-binary '{"state":"closed"}' || true)"
echo "Issue #$ISSUE close HTTP=$CLOSE_HTTP"
if [[ ! "$CLOSE_HTTP" =~ ^2 ]]; then
echo "Failed to close issue #$ISSUE"
cat /tmp/proposer.close.out.json || true
exit 1
fi
echo "Verifying issue #$ISSUE state ..."
VERIFY_HTTP="$(curl -sS -o /tmp/proposer.verify.out.json -w '%{http_code}' \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" || true)"
echo "Issue #$ISSUE verify HTTP=$VERIFY_HTTP"
if [[ ! "$VERIFY_HTTP" =~ ^2 ]]; then
echo "Failed to re-read issue #$ISSUE after close"
cat /tmp/proposer.verify.out.json || true
exit 1
fi
ISSUE_STATE="$(node --input-type=module -e '
import fs from "node:fs";
const j = JSON.parse(fs.readFileSync("/tmp/proposer.verify.out.json", "utf8"));
console.log(String(j.state || ""));
')"
echo "Issue #$ISSUE state=$ISSUE_STATE"
[[ "$ISSUE_STATE" == "closed" ]] || {
echo "Issue #$ISSUE is not closed after PATCH"
cat /tmp/proposer.verify.out.json || true
exit 1
}
done
echo "PR: $PR_URL"
PR_JSON="$(curl -fsS -X POST \ PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \ -H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -710,8 +733,8 @@ jobs:
const issue = process.env.ISSUE || ""; const issue = process.env.ISSUE || "";
const url = process.env.PR_URL || ""; const url = process.env.PR_URL || "";
const msg = const msg =
`PR proposer créée pour le ticket #${issue} : ${url}\n\n` + `PR proposer creee pour le ticket #${issue} : ${url}\n\n` +
`Le ticket est clôturé automatiquement ; la discussion peut se poursuivre dans la PR.`; `Le ticket est cloture automatiquement ; la discussion peut se poursuivre dans la PR.`;
fs.writeFileSync( fs.writeFileSync(
"/tmp/proposer.issue.close.comment.json", "/tmp/proposer.issue.close.comment.json",
@@ -730,6 +753,17 @@ jobs:
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \ "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
--data-binary '{"state":"closed"}' --data-binary '{"state":"closed"}'
ISSUE_STATE="$(curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" | \
node --input-type=module -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const j = JSON.parse(s || "{}"); process.stdout.write(String(j.state || "")); });')"
[[ "$ISSUE_STATE" == "closed" ]] || {
echo "Issue #$ISSUE is still not closed after PATCH"
exit 1
}
done done
echo "PR: $PR_URL" echo "PR: $PR_URL"

3
.gitignore vendored
View File

@@ -30,4 +30,5 @@ public/favicon_io.zip
.DS_Store .DS_Store
# local temp workspace # local temp workspace
.tmp/ .tmp/
public/__ops/health.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,147 @@
# START-HERE — Archicratie / Édition Web (v2) # START-HERE — Archicratie / Édition Web (v3)
> Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO) > Onboarding + exploitation “nickel chrome” (DEV → Gitea → CI → Release → Blue/Green → Edge/SSO → localhost auto-sync)
## 0) TL;DR (la règle dor) ## 0) TL;DR (la règle dor)
- **Gitea = source canonique**.
- **main est protégé** : toute modification passe par **branche → PR → CI → merge**. - **Gitea = source canonique**.
- **Le NAS nest pas la source** : si un hotfix est fait sur NAS, on **backporte** via PR immédiatement. - **`main` est protégée** : toute modification passe par **branche → PR → CI → merge**.
- **Le site est statique Astro** : la prod sert du HTML (nginx), laccès est contrôlé au niveau reverse-proxy (Traefik + Authelia). - **Le NAS nest pas la source** : si un hotfix est fait sur NAS, il doit être **backporté immédiatement** via PR.
- **Le site est statique Astro** : la prod sert du HTML via nginx ; laccès est contrôlé au niveau reverse-proxy (Traefik + Authelia).
- **Le localhost automatique nest pas le repo de dev** : il tourne depuis un **worktree dédié**, synchronisé sur `origin/main`.
---
## 1) Architecture mentale (ultra simple) ## 1) Architecture mentale (ultra simple)
- **DEV (Mac Studio)** : édition + tests + commit + push
- **Gitea** : dépôt canon + PR + CI (CI.yaml) - **DEV canonique (Mac Studio)** : édition, dev, tests, commits, pushes
- **NAS (DS220+)** : déploiement “blue/green” - **Gitea** : dépôt canonique, PR, CI, workflows éditoriaux
- `web_blue` (staging upstream) → `127.0.0.1:8081` - **NAS (DS220+)** : déploiement blue/green
- `web_green` (live upstream)`127.0.0.1:8082` - `web_blue` → staging upstream → `127.0.0.1:8081`
- **Edge (Traefik)** : route les hosts - `web_green` → live upstream → `127.0.0.1:8082`
- **Edge (Traefik)** : routage des hosts
- `staging.archicratie...` → 8081 - `staging.archicratie...` → 8081
- `archicratie...` → 8082 - `archicratie...` → 8082
- **Authelia** devant, via middleware `chain-auth@file` - **Authelia** devant, via middleware `chain-auth@file`
- **Localhost auto-sync**
- un **repo canonique de développement**
- un **worktree localhost miroir de `origin/main`**
- un **agent de sync**
- un **agent Astro**
---
## 2) Répertoires & conventions (repo) ## 2) Répertoires & conventions (repo)
### 2.1 Contenu canon (édition) ### 2.1 Contenu canon (édition)
- `src/content/**` : contenu MD / MDX canon (Astro content collections)
- `src/pages/**` : routes Astro (index, [...slug], etc.) - `src/content/**` : contenu MD / MDX canon
- `src/components/**` : composants UI (SiteNav, TOC, SidePanel, etc.) - `src/pages/**` : routes Astro
- `src/layouts/**` : layouts (EditionLayout, SiteLayout) - `src/components/**` : composants UI
- `src/layouts/**` : layouts
- `src/styles/**` : CSS global - `src/styles/**` : CSS global
### 2.2 Annotations (pré-Édition “tickets”) ### 2.2 Annotations (pré-Édition “tickets”)
- `src/annotations/<workKey>/<slug>.yml` - `src/annotations/<workKey>/<slug>.yml`
- Exemple : `src/annotations/archicrat-ia/prologue.yml` - Exemple :
- Objectif : stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`). `src/annotations/archicrat-ia/prologue.yml`
Objectif :
stocker “Références / Médias / Commentaires” par page et par paragraphe (`p-...`).
### 2.3 Scripts (tooling / build) ### 2.3 Scripts (tooling / build)
- `scripts/inject-anchor-aliases.mjs` : injection aliases dans dist
- `scripts/dedupe-ids-dist.mjs` : retire IDs dupliqués dans dist - `scripts/inject-anchor-aliases.mjs` : injection aliases dans `dist`
- `scripts/build-para-index.mjs` : index paragraphes (postbuild / predev) - `scripts/dedupe-ids-dist.mjs` : retrait IDs dupliqués
- `scripts/build-annotations-index.mjs` : index annotations (postbuild / predev) - `scripts/build-para-index.mjs` : index paragraphes
- `scripts/check-anchors.mjs` : contrat stabilité dancres (CI) - `scripts/build-annotations-index.mjs` : index annotations
- `scripts/check-anchors.mjs` : contrat stabilité dancres
- `scripts/check-annotations*.mjs` : sanity YAML + médias - `scripts/check-annotations*.mjs` : sanity YAML + médias
> Important : les scripts sont **partie intégrante** de la stabilité (IDs/ancres/indexation). > Important : ces scripts ne sont pas accessoires.
> On évite “la magie” : tout est scripté + vérifié. > Ils font partie du contrat de stabilité éditoriale.
## 3) Workflow Git “pro” (main protégé) ---
### 3.1 Cycle standard (toute modif)
en bash :
## 3) Les trois espaces à ne jamais confondre
### 3.1 Repo canonique de développement
```text
/Volumes/FunIA/dev/archicratie-edition/site
```
Usage :
- développement normal
- branches de travail
- nouvelles fonctionnalités
- corrections manuelles
- commits
- pushes
- PR
### 3.2 Worktree localhost miroir de `main`
```text
/Users/s-funia/ops-local/archicratie/localhost-worktree
```
Branche attendue :
```text
localhost-sync
```
Usage :
- exécuter le localhost automatique
- refléter `origin/main`
- ne jamais servir despace de développement
### 3.3 Ops local hors repo
```text
/Users/s-funia/ops-local/archicratie
```
Usage :
- scripts dexploitation
- état
- logs
- automatisation `launchd`
---
## 4) Pourquoi cette séparation existe
Il ne faut pas utiliser le repo canonique de développement comme serveur localhost permanent.
Sinon on mélange :
- travail en cours
- commits non poussés
- essais temporaires
- état réellement publié sur `main`
Le résultat devient ambigu.
La séparation retenue est donc :
- **repo canonique** = espace de développement
- **worktree localhost** = miroir exécutable de `origin/main`
- **ops local** = scripts et automatisation
Cest cette séparation qui rend le système lisible, robuste et opérable.
---
## 5) Workflow Git “pro” (main protégée)
### 5.1 Cycle standard (toute modif)
```bash
git checkout main git checkout main
git pull --ff-only git pull --ff-only
@@ -60,37 +156,48 @@ npm run test:anchors
git add -A git add -A
git commit -m "xxx: description claire" git commit -m "xxx: description claire"
git push -u origin "$BR" git push -u origin "$BR"
```
### 3.2 PR vers main ### 5.2 PR vers `main`
Ouvrir PR dans Gitea - ouvrir une PR dans Gitea
- attendre une CI verte
- merger
- laisser les workflows faire le reste
CI doit être verte ### 5.3 Cas spécial : hotfix prod (NAS)
Merge PR → main On peut faire un hotfix durgence côté NAS si nécessaire.
### 3.3 Cas spécial : hotfix prod (NAS) Mais létat final doit toujours revenir dans Gitea :
On peut faire un hotfix “urgence” en prod/staging si nécessaire… - branche
- PR
- CI
- merge
MAIS : létat final doit revenir dans Gitea : branche → PR → CI → merge. ---
## 4) Déploiement (NAS) — principe ## 6) Déploiement (NAS) — principe
### 4.1 Release pack
On génère un pack “reproductible” (source + config + scripts) puis on déploie. ### 6.1 Release pack
### 4.2 Blue/Green On génère un pack reproductible, puis on déploie.
web_blue = staging upstream (8081) ### 6.2 Blue/Green
web_green = live upstream (8082) - `web_blue` = staging (`8081`)
- `web_green` = live (`8082`)
Edge Traefik sélectionne quel host pointe vers quel upstream. Le reverse-proxy choisit lupstream selon le host demandé.
## 5) Check-list “≤ 10 commandes” (happy path complet) ---
### 5.1 DEV (Mac)
## 7) Happy path complet
### 7.1 DEV (Mac)
```bash
git checkout main && git pull --ff-only git checkout main && git pull --ff-only
git checkout -b chore/my-change-$(date +%Y%m%d) git checkout -b chore/my-change-$(date +%Y%m%d)
@@ -99,55 +206,258 @@ rm -rf .astro node_modules/.vite dist
npm run build npm run build
npm run test:anchors npm run test:anchors
npm run dev npm run dev
```
### 5.2 Push + PR ### 7.2 Push + PR
```bash
git add -A git add -A
git commit -m "chore: my change" git commit -m "chore: my change"
git push -u origin chore/my-change-YYYYMMDD git push -u origin chore/my-change-YYYYMMDD
# ouvrir PR dans Gitea ```
### 5.3 Déploiement NAS (résumé) Puis ouvrir la PR dans Gitea.
Voir docs/runbooks/DEPLOY-BLUE-GREEN.md. ### 7.3 Déploiement NAS
## 6) Problèmes “classiques” + diagnostic rapide Voir :
### 6.1 “Le staging ne ressemble pas au local”
# Comparer upstream direct 8081 vs 8082 : ```text
docs/runbooks/DEPLOY-BLUE-GREEN.md
```
---
## 8) Localhost auto-sync — ce quil faut retenir
Le localhost automatique sert à voir **la vérité de `main`**, pas à développer du neuf.
### 8.1 Scripts principaux
#### Script de sync
```text
~/ops-local/archicratie/auto-sync-localhost.sh
```
Rôle :
- fetch `origin/main`
- réaligner le worktree localhost
- lancer `npm ci` si besoin
- redéclencher lagent Astro si nécessaire
#### Script Astro
```text
~/ops-local/archicratie/run-astro-localhost.sh
```
Rôle :
- lancer `astro dev`
- depuis le bon worktree
- avec le bon runtime Node
- sur `127.0.0.1:4321`
> Oui : ce script est nécessaire.
> Il isole proprement le lancement du serveur Astro dans un contexte `launchd` stable.
### 8.2 LaunchAgents
#### Agent sync
```text
~/Library/LaunchAgents/me.archicratie.localhost-sync.plist
```
#### Agent Astro
```text
~/Library/LaunchAgents/me.archicratie.localhost-astro.plist
```
### 8.3 Document de référence
Pour tout le détail dexploitation du localhost automatique, lire :
```text
docs/OPS-LOCALHOST-AUTO-SYNC.md
```
---
## 9) Règle dor : il y a deux usages locaux distincts
### 9.1 Voir ce qui est réellement sur `main`
Utiliser :
```text
http://127.0.0.1:4321
```
Ce localhost doit être considéré comme :
**un miroir local exécutable de `origin/main`**
### 9.2 Développer / tester une nouvelle fonctionnalité
Utiliser le repo canonique :
```bash
cd /Volumes/FunIA/dev/archicratie-edition/site
npm run dev
```
Donc :
- **localhost auto-sync** = vérité de `main`
- **localhost de dev manuel** = expérimentation en cours
Il ne faut pas les confondre.
---
## 10) Ce quil ne faut pas faire
### 10.1 Ne pas développer dans le worktree localhost
Le worktree localhost est piloté automatiquement.
Il peut être :
- réaligné
- nettoyé
- redémarré
Donc :
- pas de commits dedans
- pas de dev feature dedans
- pas dexpérimentation de fond dedans
### 10.2 Ne pas utiliser le repo canonique comme miroir auto-sync
Sinon on mélange :
- espace de dev
- état publié
- serveur local permanent
### 10.3 Ne pas remettre les scripts ops sur un volume externe
Les scripts dops doivent rester sous `HOME`.
Le fait de les mettre sous `/Volumes/...` a déjà provoqué des erreurs du type :
```text
Operation not permitted
```
### 10.4 Ne pas supprimer `run-astro-localhost.sh`
Ce script fait partie de larchitecture actuelle.
Le supprimer reviendrait à réintroduire le flou entre sync Git et exécution dAstro.
---
## 11) Commandes de contrôle essentielles
### 11.1 État global
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
### 11.2 État Git
```bash
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
git -C ~/ops-local/archicratie/localhost-worktree branch --show-current
```
### 11.3 État LaunchAgents
```bash
launchctl print "gui/$(id -u)/me.archicratie.localhost-sync" | sed -n '1,160p'
launchctl print "gui/$(id -u)/me.archicratie.localhost-astro" | sed -n '1,160p'
```
### 11.4 État logs
```bash
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
tail -n 80 ~/Library/Logs/archicratie-localhost-sync.err.log
tail -n 80 ~/Library/Logs/archicratie-localhost-astro.err.log
```
### 11.5 État serveur
```bash
lsof -nP -iTCP:4321 -sTCP:LISTEN
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
ps -p "$PID" -o pid=,command=
lsof -a -p "$PID" -d cwd
```
### 11.6 Vérification contenu
```bash
curl -s http://127.0.0.1:4321/archicrat-ia/prologue/ | grep -n "taxe Zucman"
```
---
## 12) Problèmes classiques + diagnostic
### 12.1 “Le staging ne ressemble pas au local”
Comparer les upstream directs :
```bash
curl -sS http://127.0.0.1:8081/ | head -n 2 curl -sS http://127.0.0.1:8081/ | head -n 2
curl -sS http://127.0.0.1:8082/ | head -n 2 curl -sS http://127.0.0.1:8082/ | head -n 2
```
# Vérifier quel routeur edge répond (header diag) : Vérifier le routeur edge :
```bash
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \ curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | grep -iE 'HTTP/|location:|x-archi-router'
```
# Lire docs/runbooks/EDGE-TRAEFIK.md. Voir :
### 6.2 Canonical incorrect (localhost en prod) ```text
docs/runbooks/EDGE-TRAEFIK.md
```
Cause racine : site dans Astro = PUBLIC_SITE non injecté au build. ### 12.2 Canonical incorrect
Fix canonique : voir docs/runbooks/ENV-PUBLIC_SITE.md. Cause probable : `PUBLIC_SITE` mal injecté au build.
Test : Test :
```bash
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1 curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -1
```
### 6.3 Contrat “anchors” en échec après migration dURL Voir :
Quand on déplace des routes (ex: /archicratie/archicrat-ia/* → /archicrat-ia/*), le test dancres peut échouer même si les IDs nont pas changé, car les pages ont changé de chemin. ```text
docs/runbooks/ENV-PUBLIC_SITE.md
```
# Procédure safe : ### 12.3 Contrat anchors en échec après migration dURL
Backup baseline : Procédure safe :
```bash
cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S) cp -a tests/anchors-baseline.json /tmp/anchors-baseline.json.bak.$(date +%F-%H%M%S)
Mettre à jour les clés (chemins) sans toucher aux IDs :
node - <<'NODE' node - <<'NODE'
import fs from 'fs'; import fs from 'fs';
const p='tests/anchors-baseline.json'; const p='tests/anchors-baseline.json';
@@ -161,16 +471,213 @@ fs.writeFileSync(p, JSON.stringify(out,null,2)+'\n');
console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length); console.log('updated keys:', Object.keys(j).length, '->', Object.keys(out).length);
NODE NODE
Re-run :
npm run test:anchors npm run test:anchors
```
## 7) Ce que létape 9 doit faire (orientation) ### 12.4 “Le localhost auto-sync ne montre pas les dernières modifs”
Stabiliser le pipeline “tickets → YAML annotations” Commande réflexe :
Formaliser la spec YAML + merge + anti-doublon (voir docs/EDITORIAL-ANNOTATIONS-SPEC.md) ```bash
~/ops-local/archicratie/doctor-localhost.sh
```
Durcir lonboarding (ce START-HERE + runbooks) Puis :
Éviter les régressions par tests (anchors / annotations / smoke) ```bash
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
```
Si les SHA diffèrent :
- le sync na pas tourné
- ou lagent sync a un problème
### 12.5 “Le SHA est bon mais le contenu web est faux”
Vérifier quel Astro écoute réellement :
```bash
lsof -nP -iTCP:4321 -sTCP:LISTEN
PID="$(lsof -tiTCP:4321 -sTCP:LISTEN | head -n 1)"
ps -p "$PID" -o pid=,command=
lsof -a -p "$PID" -d cwd
```
Attendu :
- commande contenant `astro dev`
- cwd = `~/ops-local/archicratie/localhost-worktree`
### 12.6 Erreur `EBADENGINE`
Cause probable :
- Node 23 utilisé au lieu de Node 22
Résolution :
- forcer `node@22` dans les scripts et les LaunchAgents
### 12.7 Erreur `Operation not permitted`
Cause probable :
- scripts dops placés sous `/Volumes/...`
Résolution :
- garder les scripts sous :
```text
~/ops-local/archicratie
```
### 12.8 Erreur `EPERM` sur `astro.mjs`
Cause probable :
- ancien worktree sur volume externe
- ancien chemin résiduel
- Astro lancé depuis un mauvais emplacement
Résolution :
- worktree localhost sous :
```text
~/ops-local/archicratie/localhost-worktree
```
- scripts cohérents avec ce chemin
- réinstallation propre via :
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
---
## 13) Redémarrage machine
Après reboot, le comportement attendu est :
1. le LaunchAgent sync se recharge
2. le LaunchAgent Astro se recharge
3. le worktree localhost est réaligné
4. Astro redémarre sur `127.0.0.1:4321`
### Vérification rapide après reboot
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
Si nécessaire :
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
---
## 14) Procédure de secours manuelle
### Forcer un sync
```bash
~/ops-local/archicratie/auto-sync-localhost.sh
```
### Réinstaller proprement le dispositif local
```bash
~/ops-local/archicratie/install-localhost-sync.sh
```
### Diagnostic complet
```bash
~/ops-local/archicratie/doctor-localhost.sh
```
---
## 15) Décision dexploitation finale
La politique retenue est la suivante :
- **repo canonique** = espace de développement
- **worktree localhost** = miroir automatique de `main`
- **ops sous HOME** = scripts, logs, automation
- **LaunchAgent sync** = réalignement Git
- **LaunchAgent Astro** = exécution stable du serveur local
- **Astro local** = lancé uniquement depuis le worktree localhost
Cette séparation rend le dispositif plus :
- lisible
- robuste
- opérable
- antifragile
---
## 16) Résumé opératoire
### Pour voir la vérité de `main`
Ouvrir :
```text
http://127.0.0.1:4321
```
Le serveur doit provenir de :
```text
/Users/s-funia/ops-local/archicratie/localhost-worktree
```
### Pour développer
Travailler dans :
```text
/Volumes/FunIA/dev/archicratie-edition/site
```
avec les commandes habituelles.
### Pour réparer vite
```bash
~/ops-local/archicratie/doctor-localhost.sh
~/ops-local/archicratie/auto-sync-localhost.sh
```
---
## 17) Mémoire courte
Si un jour plus rien nest clair, repartir de ces commandes :
```bash
~/ops-local/archicratie/doctor-localhost.sh
git -C ~/ops-local/archicratie/localhost-worktree rev-parse HEAD
git -C /Volumes/FunIA/dev/archicratie-edition/site ls-remote origin refs/heads/main
lsof -nP -iTCP:4321 -sTCP:LISTEN
```
Puis lire :
```bash
tail -n 120 ~/ops-local/archicratie/logs/auto-sync-localhost.log
tail -n 120 ~/ops-local/archicratie/logs/astro-localhost.log
```
---
## 18) Statut actuel visé
Quand tout fonctionne correctement :
- le worktree localhost pointe sur le même SHA que `origin/main`
- `astro dev` écoute sur `127.0.0.1:4321`
- son cwd est `~/ops-local/archicratie/localhost-worktree`
- le contenu servi correspond au contenu mergé sur `main`
Cest létat de référence à préserver.

View File

@@ -0,0 +1,20 @@
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const outDir = path.join(root, "public", "__ops");
const outFile = path.join(outDir, "health.json");
const payload = {
service: "archicratie-site",
env: process.env.PUBLIC_OPS_ENV || "unknown",
upstream: process.env.PUBLIC_OPS_UPSTREAM || "unknown",
buildSha: process.env.PUBLIC_BUILD_SHA || "unknown",
builtAt: process.env.PUBLIC_BUILD_TIME || new Date().toISOString(),
};
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(outFile, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
console.log(`✅ ops health written: ${outFile}`);
console.log(payload);

View File

@@ -1,7 +1,11 @@
{ {
"/archicrat-ia/prologue/": { "/archicrat-ia/prologue/": {
"p-0-d7974f88": "p-0-e729df02", "p-0-d7974f88": "p-0-e729df02",
"p-17-b8c5bf21": "p-17-3deef56b",
"p-22-a416d473": "p-22-5bfa283b",
"p-23-d91a7b78": "p-23-0e7b37e9",
"p-4-8ed4f807": "p-4-90b2a1cc", "p-4-8ed4f807": "p-4-90b2a1cc",
"p-5-85126fa5": "p-5-d788c546" "p-5-85126fa5": "p-5-d788c546",
"p-7-64a0ca9c": "p-7-4efdb1d4"
} }
} }

View File

@@ -26,7 +26,7 @@ Ce qui revient à dire que la question politique — au sens fort — na peut
Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient lordre en surplomb. Comme le rappelle Michel Foucault, il ny a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. Cest précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque. Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient lordre en surplomb. Comme le rappelle Michel Foucault, il ny a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. Cest précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque.
Ce qui émerge nest pas de nouveaux principes, ni une nouvelle idéologie, mais une exigence beaucoup plus modeste, mais aussi beaucoup plus difficile à satisfaire : celle de trouver dans les relations elles-mêmes — entre groupes, entre institutions, entre individus, entre temporalités — les ressources nécessaires pour maintenir leurs mondes viables. Autrement dit : cest *dans* les tensions, *à même* les conflits, *au sein* des alliances, *au cœur* des désaccords et des polémiques, que semble se construire la régulation. Non plus *au-dessus*, par un décret transcendant, mais *au-dedans*, par un agencement toujours révisable. Cest cela que nous voulons dire — sans technicité inutile — quand nous parlons dun déplacement vers une *instance de régulation située de co-viabilité* : un espace commun où les forces hétérogènes, souvent antagonistes, peuvent coexister, se contredire, se confronter, séprouver, sans se détruire mutuellement. Ce qui émerge nest pas de nouveaux principes, ni une nouvelle idéologie, mais une exigence beaucoup plus modeste, mais aussi beaucoup plus difficile à satisfaire : celle de trouver dans les relations elles-mêmes — entre groupes, entre institutions, entre individus, entre temporalités — les ressources nécessaires pour maintenir leurs mondes viables. Autrement dit : cest dans les tensions, à même les conflits, au sein des alliances, au cœur des désaccords et des polémiques, que semble se construire la régulation. Non plus au-dessus, par un décret transcendant, mais au-dedans, par un agencement toujours révisable. Cest cela que nous voulons dire — sans technicité inutile — quand nous parlons dun déplacement vers une instance de régulation située de co-viabilité : un espace commun où les forces hétérogènes, souvent antagonistes, peuvent coexister, se contredire, se confronter, séprouver, sans pour autant systématiquement se détruire mutuellement.
Penser le politique depuis cette approche, cest renoncer à lidée même quun ordre puisse se fonder définitivement, une fois pour toutes. Cest reconnaître que ce qui fait tenir une société nest jamais un principe unique, un commandement souverain, une légitimité première, mais *un espace dépreuve toujours rejoué* où se négocient, se recadrent, sopposent, sajustent des forces hétérogènes dont laccord est constamment partiel, toujours temporaire, perpétuellement instable. Penser le politique depuis cette approche, cest renoncer à lidée même quun ordre puisse se fonder définitivement, une fois pour toutes. Cest reconnaître que ce qui fait tenir une société nest jamais un principe unique, un commandement souverain, une légitimité première, mais *un espace dépreuve toujours rejoué* où se négocient, se recadrent, sopposent, sajustent des forces hétérogènes dont laccord est constamment partiel, toujours temporaire, perpétuellement instable.
@@ -46,7 +46,7 @@ Cela ne veut pas dire que le politique ait disparu, mais plutôt quil tend pe
Cest un marché carbone qui, au nom de seuils agrégés à léchelle continentale, conduit à la fermeture dun site industriel local, sans quaucune figure politique ne puisse rendre visible ni opposable larbitrage opéré. Cest un algorithme de régulation hospitalière qui, face à une tension budgétaire ou épidémiologique, déprogramme automatiquement des interventions chirurgicales — sans quaucun médecin, aucun patient, aucun responsable politique ne puisse véritablement en discuter les critères. Cest une plateforme numérique de traitement des titres de séjour qui suspend une demande pour “anomalie de saisie”, sans contact humain, sans justification claire, sans voie de recours instituée. Cest un logiciel de pilotage budgétaire, adossé à des indicateurs defficience, qui impose la réduction dune politique sociale sans passage par une arène délibérative. Cest aussi un score algorithmique de risque bancaire qui écarte discrètement une famille dun prêt, bien avant quelle ait pu formuler son projet. Cest un marché carbone qui, au nom de seuils agrégés à léchelle continentale, conduit à la fermeture dun site industriel local, sans quaucune figure politique ne puisse rendre visible ni opposable larbitrage opéré. Cest un algorithme de régulation hospitalière qui, face à une tension budgétaire ou épidémiologique, déprogramme automatiquement des interventions chirurgicales — sans quaucun médecin, aucun patient, aucun responsable politique ne puisse véritablement en discuter les critères. Cest une plateforme numérique de traitement des titres de séjour qui suspend une demande pour “anomalie de saisie”, sans contact humain, sans justification claire, sans voie de recours instituée. Cest un logiciel de pilotage budgétaire, adossé à des indicateurs defficience, qui impose la réduction dune politique sociale sans passage par une arène délibérative. Cest aussi un score algorithmique de risque bancaire qui écarte discrètement une famille dun prêt, bien avant quelle ait pu formuler son projet.
Contrairement aux apparences, ce qui soffre au regard nest plus la figure massive du pouvoir trônant dans la clarté de ses apparats, mais la trame patiente dune régulation en mouvement. Disparues, les instances fixes ; effacée, la demeure solennelle de lautorité. Le réel geste de gouvernance sinsinue insidieusement dans des protocoles, se glisse sournoisement dans la routine, sentrelace irrémédiablement dans les habitudes, se ramifie inextricablement dans dinnombrables appareils sans visage. Nul acte inaugural nen marque ostensiblement la naissance, nulle proclamation nen scande les rythmes. On constate seulement que la régulation avance sans fracas, tisse patiemment la toile discrète sur laquelle se déplacent nos vies. Ce nest plus tant le décret ni la loi qui pèsent,bien plus les enchevêtrements de normes, limperceptible maillage de procédures et lajustement continu de directives flexibles. Contrairement aux apparences, ce qui soffre au regard nest plus la figure massive du pouvoir trônant dans la clarté de ses apparats, mais la trame patiente dune régulation en mouvement. Disparues, les instances fixes ; effacée, la demeure solennelle de lautorité. Le réel geste de gouvernance sinsinue dans des protocoles, se glisse sournoisement dans la routine, sentrelace irrémédiablement dans les habitudes, se ramifie inextricablement dans dinnombrables appareils sans visage. Nul acte inaugural nen marque ostensiblement la naissance, nulle proclamation nen scande les rythmes. On constate seulement que la régulation avance sans fracas, tisse patiemment la toile discrète sur laquelle se déplacent nos vies. Ce nest plus tant le décret ni la loi qui pèsent, bien plus les enchevêtrements de normes, limperceptible maillage de procédures et lajustement continu de directives flexibles.
La contrainte naccable plus par lostentation de lordre, mais sinocule par la subtilité des systèmes. Ainsi, il sagit désormais de façonner, par lagencement soigné déquilibres, de données, de flux, où chacun se trouve relié, indexé, impliqué à même cette dentelle administrative, sans jamais croiser le centre, sans jamais savoir nommer celui ou ce qui agit. La régulation moderne tresse ainsi un univers de seuils mobiles et dagencements souples, où lon ne peut jamais tout à fait fixer le moment ni le lieu du pouvoir agissant — mais où, à chaque pli de la vie collective, se lit lempreinte dune architecture invisible. La contrainte naccable plus par lostentation de lordre, mais sinocule par la subtilité des systèmes. Ainsi, il sagit désormais de façonner, par lagencement soigné déquilibres, de données, de flux, où chacun se trouve relié, indexé, impliqué à même cette dentelle administrative, sans jamais croiser le centre, sans jamais savoir nommer celui ou ce qui agit. La régulation moderne tresse ainsi un univers de seuils mobiles et dagencements souples, où lon ne peut jamais tout à fait fixer le moment ni le lieu du pouvoir agissant — mais où, à chaque pli de la vie collective, se lit lempreinte dune architecture invisible.
@@ -56,9 +56,9 @@ Cela signifie que le politique sest décousu de ses formes historiques. Il co
Autrement dit, nous avons changé dépoque sans encore avoir pu changé de lexique. Nous continuons de penser avec des formes obsolètes ce qui sactive sous nos yeux. Nous employons les mots dhier pour décrire des processus qui les excèdent de toutes parts. Nous parlons de gouvernements, là où il faudrait parler de structures de régulation composite. Nous discutons de lois, là où il faudrait décrire des protocoles, des seuils, des scénographies dajustement, des mécanismes de *feedback* algorithmique, des normes sans normalisateurs. Autrement dit, nous avons changé dépoque sans encore avoir pu changé de lexique. Nous continuons de penser avec des formes obsolètes ce qui sactive sous nos yeux. Nous employons les mots dhier pour décrire des processus qui les excèdent de toutes parts. Nous parlons de gouvernements, là où il faudrait parler de structures de régulation composite. Nous discutons de lois, là où il faudrait décrire des protocoles, des seuils, des scénographies dajustement, des mécanismes de *feedback* algorithmique, des normes sans normalisateurs.
Cette disjonction entre lexpérience vécue de la contrainte et le vocabulaire disponible pour la dire nest pas quun problème théorique. Elle produit une désorientation profonde. Elle empêche de penser le réel, de localiser les responsabilités et rend inopérantes les critiques. Elle altère la capacité collective à formuler des exigences, jusquà dissoudre les repères et les registres daction. Cette disjonction entre lexpérience vécue de la contrainte et le vocabulaire disponible pour l'exprimer nest pas quun problème théorique. Elle produit une désorientation profonde. Elle empêche de penser le réel, de localiser les responsabilités et rend inopérantes les critiques. Elle altère la capacité collective à formuler des exigences, jusquà dissoudre les repères et les registres daction.
Cette impuissance démocratique généralisée à nommer, situer, orienter les formes réelles de la régulation se donne parfois à voir dans des situations dapparente clarté — et cest peut-être le plus troublant. Prenons un exemple rendu brûlant par lactualité française en 2025 : la proposition de ce que lon appelle la *taxe Zucman*. Formulée par léminent économiste Gabriel Zucman, cette mesure vise à instaurer un impôt minimal annuel sur le patrimoine des ultra-riches en France et dans le monde — au-delà dun seuil (autour de 100 millions deuros). Le taux proposé est denviron 2 % sur la valeur totale du patrimoine net, quil soit liquide ou partiellement non liquide (actions non cotées, participations, biens immobiliers), ce qui pose des défis de paiement et dévaluation. Cette impuissance démocratique à nommer, situer et orienter les formes réelles de la régulation — impuissance qui tend à se généraliser — apparaît parfois au grand jour dans des situations dapparente clarté, ce qui est peutêtre le plus troublant. En témoigne un exemple rendu brûlant par lactualité française de 2025 : la proposition de ce quon a appelé la taxe Zucman. Formulée par léconomiste Gabriel Zucman, cette mesure prévoit linstauration dun impôt minimal annuel sur le patrimoine des ultra-riches, en France comme à léchelle mondiale, audelà dun seuil denviron 100 millions deuros. Le taux proposé, de lordre de 2% de la valeur totale du patrimoine net, quil soit liquide ou non (actions non cotées, participations, biens immobiliers), soulève toutefois dimportants problèmes de paiement et dévaluation.
Lidée est de corriger ce que Zucman identifie comme un déséquilibre fiscal majeur : les très grandes fortunes paient aujourdhui, proportionnellement, beaucoup moins que ce que permettrait une imposition équitable et progressive, notamment en raison de lévasion fiscale, de la mise sous structures opaques par *holding*, du transfert du patrimoine privée en patrimoine professionnel ou de la dissociation entre richesse effective et revenu imposable. Lidée est de corriger ce que Zucman identifie comme un déséquilibre fiscal majeur : les très grandes fortunes paient aujourdhui, proportionnellement, beaucoup moins que ce que permettrait une imposition équitable et progressive, notamment en raison de lévasion fiscale, de la mise sous structures opaques par *holding*, du transfert du patrimoine privée en patrimoine professionnel ou de la dissociation entre richesse effective et revenu imposable.