Compare commits

...

57 Commits

Author SHA1 Message Date
0b4a31a432 ci(deploy): make gate merge-proof (diff before..after)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 43s
CI / build-and-anchors (pull_request) Successful in 37s
2026-03-03 19:58:27 +01:00
c617dc3979 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#183) from testB-hotpatch-auto-20260303-183745 into main
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 7m50s
Reviewed-on: #183
2026-03-03 18:40:39 +01:00
1b95161de0 test: B hotpatch-auto gate (touch src/annotations)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-03 18:38:52 +01:00
ebd976bd46 Merge pull request 'chore: cleanup testA/testB markers' (#182) from chore/cleanup-testA-testB-20260303-175846 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 42s
Deploy staging+live (annotations) / deploy (push) Successful in 8m55s
Reviewed-on: #182
2026-03-03 18:01:40 +01:00
f8d57d8fe0 chore: cleanup testA/testB markers
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 38s
2026-03-03 18:00:01 +01:00
09a4d2c472 Merge pull request 'test: B hotpatch-auto gate (touch src/annotations)' (#181) from testB-hotpatch-auto-20260303-174037 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 8m0s
Reviewed-on: #181
2026-03-03 17:43:31 +01:00
1f6dc874d0 test: B hotpatch-auto gate (touch src/annotations)
All checks were successful
SMOKE / smoke (push) Successful in 2s
CI / build-and-anchors (push) Successful in 36s
CI / build-and-anchors (pull_request) Successful in 36s
2026-03-03 17:42:04 +01:00
4dd63945ee Merge pull request 'test: A full-auto gate (touch src/content)' (#180) from testA-full-auto-20260303-173032 into main
Some checks failed
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
Reviewed-on: #180
2026-03-03 17:36:14 +01:00
ba64b0694b test: A full-auto gate (touch src/content)
All checks were successful
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 41s
2026-03-03 17:34:32 +01:00
58e5ceda59 Merge pull request 'ci(deploy): auto FULL when content/anchors/pages/scripts change' (#179) from chore/deploy-gate-full-on-content-anchors-pages-scripts-20260303-171645 into main
All checks were successful
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 40s
Deploy staging+live (annotations) / deploy (push) Successful in 7m35s
Reviewed-on: #179
2026-03-03 17:20:36 +01:00
08f826ee01 ci(deploy): auto FULL when content/anchors/pages/scripts change
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 46s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-03 17:16:45 +01:00
3358d280ec Merge pull request 'edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)' (#178) from chore/migrate-content-archicrat-ia-root-20260303-132407 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #178
2026-03-03 15:04:07 +01:00
9cb0d5e416 content: wire archicrat-ia as first-class collection (routes + toc + schema)
All checks were successful
CI / build-and-anchors (push) Successful in 38s
CI / build-and-anchors (pull_request) Successful in 37s
SMOKE / smoke (push) Successful in 5s
2026-03-03 15:02:50 +01:00
a46f058917 edit: apply ticket #174 (/archicrat-ia/chapitre-3/#p-1-60c7ea48)
Some checks failed
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Failing after 39s
CI / build-and-anchors (pull_request) Failing after 36s
2026-03-03 14:27:35 +01:00
604b2199da Merge pull request 'ci: fix proposer apply workflow (checkout before APP_DIR detect)' (#177) from chore/fix-proposer-apply-checkout-order-20260303-122611 into main
All checks were successful
SMOKE / smoke (push) Successful in 14s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 1m6s
Reviewed-on: #177
2026-03-03 12:32:34 +01:00
d153f71be6 ci: fix proposer apply workflow (checkout before APP_DIR detect)
All checks were successful
SMOKE / smoke (push) Successful in 5s
CI / build-and-anchors (push) Successful in 41s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-03 12:26:11 +01:00
8f64e4b098 Merge pull request 'ci: fix proposer workflow (auto APP_DIR + guards)' (#176) from chore/fix-proposer-workflow-appdir-20260303-115843 into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 38s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
Reviewed-on: #176
2026-03-03 12:01:18 +01:00
459bf195d8 ci: fix proposer workflow (auto APP_DIR + guards)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-03 11:58:43 +01:00
0c46b0d19b Merge pull request 'ci: add Proposer Apply workflow (apply-ticket -> PR bot)' (#175) from chore/proposer-apply-workflow-20260302-234255 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 41s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #175
2026-03-02 23:49:57 +01:00
bfbdc7b688 ci: add Proposer Apply workflow (apply-ticket -> PR bot)
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 42s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 23:42:55 +01:00
8fd53dd4d2 Merge pull request 'anno: apply ticket #172' (#173) from bot/anno-172-20260302-200155 into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 37s
Deploy staging+live (annotations) / deploy (push) Successful in 48s
Reviewed-on: #173
2026-03-02 21:03:36 +01:00
archicratie-bot
c8bbee4f74 anno: apply ticket #172 (archicrat-ia/chapitre-3#p-1-60c7ea48 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 45s
CI / build-and-anchors (pull_request) Successful in 39s
SMOKE / smoke (push) Successful in 5s
2026-03-02 20:01:55 +00:00
04cdf54eb7 Merge pull request 'anno: apply ticket #169' (#171) from bot/anno-169-20260302-195320 into main
All checks were successful
SMOKE / smoke (push) Successful in 11s
CI / build-and-anchors (push) Successful in 43s
Deploy staging+live (annotations) / deploy (push) Successful in 56s
Reviewed-on: #171
2026-03-02 20:59:08 +01:00
archicratie-bot
d6bf645ae9 anno: apply ticket #169 (archicrat-ia/chapitre-3#p-0-ace27175 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 47s
SMOKE / smoke (push) Successful in 4s
CI / build-and-anchors (pull_request) Successful in 42s
2026-03-02 19:53:21 +00:00
1ca6bcbd81 Merge pull request 'ci: make anno apply/reject gates API-hard (approved/rejected label present)' (#170) from chore/fix-anno-apply-approved-gate-v1 into main
All checks were successful
SMOKE / smoke (push) Successful in 13s
Deploy staging+live (annotations) / deploy (push) Successful in 46s
CI / build-and-anchors (push) Successful in 42s
Reviewed-on: #170
2026-03-02 20:17:58 +01:00
dec5f8eba7 ci: make anno apply/reject gates API-hard (approved/rejected label present)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s
2026-03-02 20:12:29 +01:00
716c887045 Merge pull request 'ci: fix auto-label (no array fallback, retries, post-verify)' (#167) from chore/fix-auto-label-422-v1 into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 39s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
Reviewed-on: #167
2026-03-02 19:37:43 +01:00
9b1789a164 ci: fix auto-label (no array fallback, retries, post-verify)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 50s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 19:36:09 +01:00
17fa39c7ff Merge pull request 'ci: hard-gate anno apply/reject + fix JSON parsing' (#164) from chore/fix-anno-workflows-jsonparse-v3 into main
All checks were successful
SMOKE / smoke (push) Successful in 15s
CI / build-and-anchors (push) Successful in 45s
Deploy staging+live (annotations) / deploy (push) Successful in 55s
Reviewed-on: #164
2026-03-02 18:53:19 +01:00
8132e315f4 ci: hard-gate anno apply/reject + fix JSON parsing
All checks were successful
SMOKE / smoke (push) Successful in 10s
CI / build-and-anchors (push) Successful in 49s
CI / build-and-anchors (pull_request) Successful in 43s
2026-03-02 18:48:39 +01:00
8d993915d7 Merge pull request 'ci: stabilize anno apply/reject (event parsing + strict gating)' (#161) from chore/fix-anno-workflows-jsonparse-v2 into main
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 1m10s
Reviewed-on: #161
2026-03-02 12:49:18 +01:00
497bddd05d ci: fix anno apply/reject JSON parsing + hard gates
All checks were successful
SMOKE / smoke (push) Successful in 8s
CI / build-and-anchors (push) Successful in 48s
CI / build-and-anchors (pull_request) Successful in 40s
2026-03-02 12:47:37 +01:00
7c8e49c1a9 ci: stabilize anno apply/reject (event parsing + strict gating)
Some checks failed
CI / build-and-anchors (push) Successful in 52s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Failing after 12m55s
2026-03-02 11:10:53 +01:00
901d28b89b Merge pull request 'deploy: pin nas-deploy image by digest' (#159) from chore/pin-nas-deploy-image-digest into main
All checks were successful
SMOKE / smoke (push) Successful in 12s
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 45s
Reviewed-on: #159
2026-02-28 20:24:46 +01:00
43e2862c89 deploy: pin nas-deploy image by digest
All checks were successful
SMOKE / smoke (push) Successful in 6s
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 37s
2026-02-28 20:22:17 +01:00
73fb38c4d1 Merge pull request 'deploy: use prebaked nas-deploy image; remove apt-get step' (#158) from chore/deploy-use-prebaked-image into main
All checks were successful
SMOKE / smoke (push) Successful in 9s
CI / build-and-anchors (push) Successful in 36s
Deploy staging+live (annotations) / deploy (push) Successful in 39s
Reviewed-on: #158
2026-02-28 19:51:27 +01:00
a81d206aba deploy: use prebaked nas-deploy image; remove apt-get step
All checks were successful
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Successful in 39s
CI / build-and-anchors (pull_request) Successful in 36s
2026-02-28 19:49:25 +01:00
9801ea3cea Merge pull request 'ci: lock deploy workflow to nas-deploy runner' (#157) from chore/lock-nas-deploy into main
All checks were successful
SMOKE / smoke (push) Successful in 16s
CI / build-and-anchors (push) Successful in 44s
Deploy staging+live (annotations) / deploy (push) Successful in 3m40s
Reviewed-on: #157
2026-02-28 17:45:35 +01:00
c11189fe11 ci: lock deploy workflow to nas-deploy runner
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 44s
CI / build-and-anchors (pull_request) Successful in 40s
2026-02-28 17:43:19 +01:00
b47edb24cf Merge pull request 'ci: fix YAML newlines after runs-on (mac-ci)' (#156) from chore/fix-yaml-runs-on-newlines into main
Some checks failed
CI / build-and-anchors (push) Successful in 41s
SMOKE / smoke (push) Successful in 3s
Deploy staging+live (annotations) / deploy (push) Has been cancelled
Reviewed-on: #156
2026-02-28 15:54:48 +01:00
be191b09a0 ci: fix YAML newlines after runs-on (mac-ci)
All checks were successful
SMOKE / smoke (push) Successful in 25s
CI / build-and-anchors (push) Successful in 1m6s
CI / build-and-anchors (pull_request) Successful in 37s
2026-02-28 15:50:48 +01:00
e06587478d Merge pull request 'ci: route CI/bots to mac runner; keep deploy on NAS' (#155) from chore/route-ci-to-mac-runner into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 1m58s
Reviewed-on: #155
2026-02-28 15:29:35 +01:00
402ffb04cd ci: route CI/bots to mac runner; keep deploy on NAS 2026-02-28 15:28:49 +01:00
1cbfc02670 Merge pull request 'ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe' (#153) from chore/fix-anno-reject-close-guard into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m49s
Deploy staging+live (annotations) / deploy (push) Successful in 3m7s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #153
2026-02-28 10:05:26 +01:00
28d2fbbd2f ci: harden anno-reject (dispatch + conflict guard) and keep deploy concurrency safe
All checks were successful
CI / build-and-anchors (push) Successful in 2m21s
SMOKE / smoke (push) Successful in 19s
2026-02-28 09:55:37 +01:00
225368a952 Merge pull request 'ci: anno-apply gate on supported types (skip proposer)' (#149) from chore/fix-anno-apply-gate-types into main
All checks were successful
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
CI / build-and-anchors (push) Successful in 1m45s
SMOKE / smoke (push) Successful in 17s
Reviewed-on: #149
2026-02-27 20:06:57 +01:00
3574695041 ci: anno-apply gate on supported types (skip proposer)
All checks were successful
CI / build-and-anchors (push) Successful in 1m52s
SMOKE / smoke (push) Successful in 15s
2026-02-27 20:03:16 +01:00
ea68025a1d Merge pull request 'anno: apply ticket #144' (#148) from bot/anno-144-20260227-124313 into main
All checks were successful
CI / build-and-anchors (push) Successful in 2m5s
Deploy staging+live (annotations) / deploy (push) Successful in 2m8s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #148
2026-02-27 15:49:11 +01:00
3a08698003 Merge pull request 'anno: apply ticket #143' (#147) from bot/anno-143-20260227-124037 into main
Some checks failed
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Has been cancelled
Reviewed-on: #147
2026-02-27 15:48:28 +01:00
3d583608c2 Merge pull request 'anno: apply ticket #142' (#146) from bot/anno-142-20260227-123430 into main
Some checks failed
CI / build-and-anchors (push) Has been cancelled
Deploy staging+live (annotations) / deploy (push) Has been cancelled
SMOKE / smoke (push) Successful in 21s
Reviewed-on: #146
2026-02-27 15:47:44 +01:00
archicratie-bot
01ae95ab43 anno: apply ticket #144 (archicrat-ia/chapitre-3#p-0-ace27175 type/media)
All checks were successful
CI / build-and-anchors (push) Successful in 2m0s
SMOKE / smoke (push) Successful in 18s
2026-02-27 12:43:16 +00:00
archicratie-bot
0d5821c640 anno: apply ticket #143 (archicrat-ia/chapitre-1#p-1-8a6c18bf type/comment)
All checks were successful
CI / build-and-anchors (push) Successful in 1m54s
SMOKE / smoke (push) Successful in 16s
2026-02-27 12:40:39 +00:00
archicratie-bot
2bcea39558 anno: apply ticket #142 (archicrat-ia/chapitre-1#p-0-8d27a7f5 type/reference)
All checks were successful
CI / build-and-anchors (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 14s
2026-02-27 12:34:32 +00:00
af85970d4a Merge pull request 'chore/fix-build-annotations-index-shards' (#141) from chore/fix-build-annotations-index-shards into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m45s
Deploy staging+live (annotations) / deploy (push) Successful in 1m53s
SMOKE / smoke (push) Successful in 13s
Reviewed-on: #141
2026-02-27 13:21:43 +01:00
41b0517c6c Merge pull request 'ci: deploy hotpatch-only + full rebuild warmup' (#139) from chore/fix-deploy-hotpatch-stable into main
All checks were successful
CI / build-and-anchors (push) Successful in 1m50s
Deploy staging+live (annotations) / deploy (push) Successful in 2m12s
SMOKE / smoke (push) Successful in 19s
Reviewed-on: #139
2026-02-26 22:00:20 +01:00
d40f24e92d Merge pull request 'ci: fix hotpatch (yaml datetime -> json safe)' (#138) from chore/fix-hotpatch-json into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m54s
Deploy staging+live (annotations) / deploy (push) Failing after 5m35s
SMOKE / smoke (push) Successful in 16s
Reviewed-on: #138
2026-02-26 21:32:56 +01:00
a5d68d6a7e Merge pull request 'ci: fix deploy workflow (warmup + hotpatch)' (#137) from chore/fix-deploy-warmup into main
Some checks failed
CI / build-and-anchors (push) Successful in 1m38s
Deploy staging+live (annotations) / deploy (push) Failing after 8m16s
SMOKE / smoke (push) Successful in 15s
Reviewed-on: #137
2026-02-26 21:09:21 +01:00
25 changed files with 893 additions and 167 deletions

View File

@@ -16,9 +16,13 @@ defaults:
run:
shell: bash
concurrency:
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-approved:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -29,12 +33,11 @@ jobs:
git --version
node --version
npm --version
npm ping --registry=https://registry.npmjs.org
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
@@ -78,10 +81,12 @@ jobs:
throw new Error("No issue number in event.json or workflow_dispatch input");
}
const labelName =
ev?.label?.name ||
ev?.label ||
"workflow_dispatch";
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
else if (ev?.label?.name) labelName = ev.label.name;
const u = new URL(cloneUrl);
const origin = u.origin;
@@ -106,16 +111,129 @@ jobs:
echo "✅ context:"
sed -n '1,120p' /tmp/anno.env
- name: Gate on label state/approved
- name: Early gate (label event fast-skip, but tolerant)
run: |
set -euo pipefail
source /tmp/anno.env
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
echo " label=$LABEL_NAME => skip"
echo " event label = $LABEL_NAME"
# Fast skip on obvious non-approved label events (avoid noise),
# BUT do NOT skip if label payload is weird/unknown.
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo " label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/anno.env
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
exit 0
fi
echo "✅ proceed (issue=$ISSUE_NUMBER)"
echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"
- name: Fetch issue + hard gate on labels + Type
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/issue.json
node --input-type=module - <<'NODE' >> /tmp/anno.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
const title = String(issue.title || "");
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
const hasApproved = labels.includes("state/approved");
function pickLine(key) {
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = body.match(re);
return m ? m[1].trim() : "";
}
const typeRaw = pickLine("Type");
const type = String(typeRaw || "").trim().toLowerCase();
const allowed = new Set(["type/media","type/reference","type/comment"]);
const proposer = new Set(["type/correction","type/fact-check"]);
const out = [];
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
// HARD gate: must currently have state/approved (avoids depending on event payload)
if (!hasApproved) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
process.stdout.write(out.join("\n") + "\n");
process.exit(0);
}
if (!type) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
} else if (allowed.has(type)) {
// proceed
} else if (proposer.has(type)) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("proposer_type:"+type)}`);
} else {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("unsupported_type:"+type)}`);
}
process.stdout.write(out.join("\n") + "\n");
NODE
echo "✅ gating result:"
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
echo " skip reason=${SKIP_REASON} -> no comment"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == proposer_type:* ]]; then
MSG=" Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
elif [[ "$REASON" == unsupported_type:* ]]; then
MSG=" Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
else
MSG=" Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: type/media|type/reference|type/comment"
fi
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Checkout default branch
run: |
@@ -135,7 +253,7 @@ jobs:
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
npm ci
npm ci --no-audit --no-fund
- name: Check apply script exists
run: |
@@ -154,7 +272,7 @@ jobs:
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
npm run build:clean
npm run build
test -f dist/para-index.json || {
echo "❌ missing dist/para-index.json after build"
@@ -173,6 +291,7 @@ jobs:
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -d .git || { echo "❌ not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
@@ -220,7 +339,7 @@ jobs:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-0}"
@@ -229,30 +348,15 @@ jobs:
exit 0
fi
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
test -n "${FORGE_TOKEN:-}" || exit 0
if [[ -f /tmp/apply.log ]]; then
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
else
BODY="(no apply log found)"
fi
MSG="❌ apply-annotation-ticket a échoué (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
- name: Comment issue if no-op (already applied)
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "1" ]] || exit 0
MSG=" Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
@@ -267,11 +371,12 @@ jobs:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip push"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo " no-op -> skip push"; exit 0; }
test -d .git || { echo " no git repo -> skip push"; exit 0; }
AUTH_URL="$(node --input-type=module -e '
const [clone, tok] = process.argv.slice(1);
@@ -290,7 +395,7 @@ jobs:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/anno.env
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip PR"; exit 0; }
@@ -333,6 +438,7 @@ jobs:
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-0}"

View File

@@ -1,8 +1,13 @@
name: Anno Reject
name: Anno Reject (close issue)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to reject/close"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
@@ -11,14 +16,26 @@ defaults:
run:
shell: bash
concurrency:
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
reject:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Derive context
- name: Tools sanity
run: |
set -euo pipefail
node --version
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
@@ -29,58 +46,122 @@ jobs:
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository url");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
if ((!owner || !repo) && cloneUrl) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const issueNumber = ev?.issue?.number || ev?.issue?.index;
if (!issueNumber) throw new Error("No issue number");
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
const labelName = ev?.label?.name || ev?.label || "";
const u = new URL(cloneUrl);
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
throw new Error("No issue number in event.json or workflow_dispatch input");
}
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
let apiBase = "";
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
apiBase = String(process.env.FORGE_API).trim().replace(/\/+$/,"");
} else if (cloneUrl) {
apiBase = new URL(cloneUrl).origin;
} else {
apiBase = "";
}
function sh(s){ return JSON.stringify(String(s)); }
process.stdout.write([
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(u.origin)}`
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
- name: Gate on label state/rejected
echo "✅ context:"
sed -n '1,120p' /tmp/reject.env
- name: Early gate (fast-skip, tolerant)
run: |
set -euo pipefail
source /tmp/reject.env
if [[ "$LABEL_NAME" != "state/rejected" ]]; then
echo " label=$LABEL_NAME => skip"
echo " event label = $LABEL_NAME"
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo " label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/reject.env
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
exit 0
fi
echo "✅ reject issue=$ISSUE_NUMBER"
- name: Comment + close issue
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/reject.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/reject.issue.json
node --input-type=module - <<'NODE' > /tmp/reject.flags
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/reject.issue.json","utf8"));
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
const hasApproved = labels.includes("state/approved");
const hasRejected = labels.includes("state/rejected");
process.stdout.write(`HAS_APPROVED=${hasApproved ? "1":"0"}\nHAS_REJECTED=${hasRejected ? "1":"0"}\n`);
NODE
source /tmp/reject.flags
# Do nothing unless state/rejected is truly present now (anti payload weird)
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
echo " state/rejected not present -> skip"
exit 0
fi
if [[ "${HAS_APPROVED:-0}" == "1" && "${HAS_REJECTED:-0}" == "1" ]]; then
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡ Action manuelle requise : retirer l'un des deux labels avant relance."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
echo " conflict => stop"
exit 0
fi
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
@@ -95,4 +176,6 @@ jobs:
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
--data-binary '{"state":"closed"}'
--data-binary '{"state":"closed"}'
echo "✅ rejected+closed"

View File

@@ -4,22 +4,37 @@ on:
issues:
types: [opened, edited]
concurrency:
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Apply labels from Type/State/Category
env:
FORGE_BASE: ${{ vars.FORGE_API || vars.FORGE_BASE }}
# IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
REPO_FULL: ${{ gitea.repository }}
EVENT_PATH: ${{ github.event_path }}
NODE_OPTIONS: --dns-result-order=ipv4first
run: |
python3 - <<'PY'
import json, os, re, urllib.request, urllib.error
import json, os, re, time, urllib.request, urllib.error, socket
forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
if not forge:
raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")
token = os.environ.get("FORGE_TOKEN") or ""
if not token:
raise SystemExit("Missing secret FORGE_TOKEN")
forge = os.environ["FORGE_BASE"].rstrip("/")
token = os.environ["FORGE_TOKEN"]
owner, repo = os.environ["REPO_FULL"].split("/", 1)
event_path = os.environ["EVENT_PATH"]
@@ -46,12 +61,9 @@ jobs:
print("PARSED:", {"Type": t, "State": s, "Category": c})
# 1) explicite depuis le body
if t:
desired.add(t)
if s:
desired.add(s)
if c:
desired.add(c)
if t: desired.add(t)
if s: desired.add(s)
if c: desired.add(c)
# 2) fallback depuis le titre si Type absent
if not t:
@@ -76,42 +88,56 @@ jobs:
"Authorization": f"token {token}",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "archicratie-auto-label/1.0",
"User-Agent": "archicratie-auto-label/1.1",
}
def jreq(method, url, payload=None):
def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
data = None if payload is None else json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
last_err = None
for i in range(retries):
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
b = r.read()
return json.loads(b.decode("utf-8")) if b else None
except urllib.error.HTTPError as e:
b = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
last_err = e
# retry only on network/timeout
time.sleep(backoff * (i + 1))
raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")
# labels repo
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000") or []
labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
name_to_id = {x.get("name"): x.get("id") for x in labels}
missing = [x for x in desired if x not in name_to_id]
if missing:
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))
wanted_ids = [name_to_id[x] for x in desired]
wanted_ids = sorted({int(name_to_id[x]) for x in desired})
# labels actuels de l'issue
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels") or []
current_ids = {x.get("id") for x in current if x.get("id") is not None}
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}
final_ids = sorted(current_ids.union(wanted_ids))
# set labels = union (n'enlève rien)
# Replace labels = union (n'enlève rien)
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
try:
jreq("PUT", url, {"labels": final_ids})
except Exception:
jreq("PUT", url, final_ids)
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
# vérif post-apply (anti "timeout mais appliqué")
post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}
missing_ids = [i for i in wanted_ids if i not in post_ids]
if missing_ids:
raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")
print(f"OK labels #{number}: {sorted(desired)}")
PY

View File

@@ -3,7 +3,7 @@ name: CI
on:
push:
pull_request:
branches: [master]
branches: [main]
workflow_dispatch:
env:
@@ -15,7 +15,7 @@ defaults:
jobs:
build-and-anchors:
runs-on: ubuntu-latest
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm

View File

@@ -26,9 +26,9 @@ concurrency:
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: nas-deploy
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
steps:
- name: Tools sanity
@@ -47,53 +47,58 @@ jobs:
node --input-type=module <<'NODE'
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
const defaultBranch = repoObj?.default_branch || "main";
const sha =
// Push-range (most reliable for change detection)
const before = String(ev?.before || "").trim();
const after =
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
ev?.after ||
ev?.sha ||
ev?.head_commit?.id ||
ev?.pull_request?.head?.sha ||
"";
String(ev?.after || ev?.sha || ev?.head_commit?.id || ev?.pull_request?.head?.sha || "").trim();
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
fs.writeFileSync("/tmp/deploy.env", [
`REPO_URL=${shq(cloneUrl)}`,
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
`SHA=${shq(sha)}`
`BEFORE=${shq(before)}`,
`AFTER=${shq(after)}`
].join("\n") + "\n");
NODE
source /tmp/deploy.env
echo "Repo URL: $REPO_URL"
echo "Default branch: $DEFAULT_BRANCH"
echo "SHA: ${SHA:-<empty>}"
echo "BEFORE: ${BEFORE:-<empty>}"
echo "AFTER: ${AFTER:-<empty>}"
rm -rf .git
git init -q
git remote add origin "$REPO_URL"
if [[ -n "${SHA:-}" ]]; then
git fetch --depth 1 origin "$SHA"
# Checkout AFTER (or default branch if missing)
if [[ -n "${AFTER:-}" ]]; then
git fetch --depth 50 origin "$AFTER"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
else
git fetch --depth 1 origin "$DEFAULT_BRANCH"
git fetch --depth 50 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
SHA="$(git rev-parse HEAD)"
echo "SHA='$SHA'" >> /tmp/deploy.env
echo "Resolved SHA: $SHA"
AFTER="$(git rev-parse HEAD)"
echo "AFTER='$AFTER'" >> /tmp/deploy.env
echo "Resolved AFTER: $AFTER"
fi
git log -1 --oneline
- name: Gate — decide HOTPATCH vs FULL rebuild
- name: Gate — decide SKIP vs HOTPATCH vs FULL rebuild
env:
INPUT_FORCE: ${{ inputs.force }}
run: |
@@ -101,51 +106,80 @@ jobs:
source /tmp/deploy.env
FORCE="${INPUT_FORCE:-0}"
BEFORE="${BEFORE:-}"
AFTER="${AFTER:-}"
# helper: true si sha = 0000... (push delete / weird payload)
is_zeros() { [[ -n "${1:-}" && "$1" =~ ^0+$ ]]; }
CHANGED=""
if [[ "$FORCE" == "1" ]]; then
echo "GO=1" >> /tmp/deploy.env
echo "MODE='full'" >> /tmp/deploy.env
echo "Gate flags: HAS_FULL=1 HAS_HOTPATCH=0"
echo "✅ force=1 -> MODE=full (rebuild+restart)"
exit 0
fi
# ✅ Push range (robuste, merge-proof)
if [[ -n "$BEFORE" && -n "$AFTER" && ! is_zeros "$BEFORE" ]]; then
# On s'assure que BEFORE est présent localement (checkout a fetch AFTER)
git cat-file -e "$BEFORE^{commit}" 2>/dev/null || git fetch --depth 50 origin "$BEFORE" || true
CHANGED="$(git diff --name-only "$BEFORE" "$AFTER" | sed '/^$/d' || true)"
else
# Fallback (workflow_dispatch, etc.) : diff HEAD^..HEAD si possible
if git cat-file -e "${AFTER}^" 2>/dev/null; then
CHANGED="$(git diff --name-only "${AFTER}^" "$AFTER" | sed '/^$/d' || true)"
else
# Merge commit sans parents dispo / autre cas : force la version merge-aware
CHANGED="$(git show -m --first-parent --name-only --pretty="" "$AFTER" | sed '/^$/d' || true)"
fi
fi
# liste fichiers touchés (utile pour copier les médias)
CHANGED="$(git show --name-only --pretty="" "$SHA" | sed '/^$/d' || true)"
printf "%s\n" "$CHANGED" > /tmp/changed.txt
echo "== changed files =="
echo "$CHANGED" | sed -n '1,260p'
if [[ "$FORCE" == "1" ]]; then
echo "GO=1" >> /tmp/deploy.env
echo "MODE='full'" >> /tmp/deploy.env
echo "✅ force=1 -> MODE=full (rebuild+restart)"
exit 0
# --- Flags ---
HAS_HOTPATCH=0
echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)' && HAS_HOTPATCH=1
HAS_FULL=0
echo "$CHANGED" | grep -qE '^(src/(content|anchors|pages|components|layouts|styles|lib|plugins)/|scripts/|astro\.config\.mjs|package(-lock)?\.json|Dockerfile|nginx\.conf|docker-compose\.yml)' && HAS_FULL=1
# public/ hors media => FULL (sinon ce n'est jamais déployé par hotpatch)
if echo "$CHANGED" | grep -qE '^public/' && ! echo "$CHANGED" | grep -qE '^public/media/'; then
HAS_FULL=1
fi
# Auto mode: uniquement annotations/media => hotpatch only
if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then
echo "Gate flags: HAS_FULL=${HAS_FULL} HAS_HOTPATCH=${HAS_HOTPATCH}"
if [[ "$HAS_FULL" == "1" ]]; then
echo "GO=1" >> /tmp/deploy.env
echo "MODE='full'" >> /tmp/deploy.env
echo "✅ build-impacting change -> MODE=full (rebuild+restart)"
elif [[ "$HAS_HOTPATCH" == "1" ]]; then
echo "GO=1" >> /tmp/deploy.env
echo "MODE='hotpatch'" >> /tmp/deploy.env
echo "✅ annotations/media change -> MODE=hotpatch"
else
echo "GO=0" >> /tmp/deploy.env
echo "MODE='skip'" >> /tmp/deploy.env
echo " no annotations/media change -> skip deploy"
echo " no deploy-relevant change -> skip deploy"
fi
- name: Install docker client + docker compose plugin (v2) + python yaml
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
run: |
set -euo pipefail
source /tmp/deploy.env
[[ "${GO:-0}" == "1" ]] || { echo " skipped"; exit 0; }
apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update
apt-get install -y --no-install-recommends ca-certificates curl docker.io python3 python3-yaml
rm -rf /var/lib/apt/lists/*
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL \
"https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64" \
-o /usr/local/lib/docker/cli-plugins/docker-compose
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
# tools are prebaked in the image
git --version
docker version
docker compose version
python3 --version
python3 -c 'import yaml; print("PyYAML OK")'
# Reuse existing compose project name if containers already exist
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"

View File

@@ -0,0 +1,395 @@
name: Proposer Apply (PR)
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number to apply (Proposer: correction/fact-check)"
required: true
env:
NODE_OPTIONS: --dns-result-order=ipv4first
defaults:
run:
shell: bash
concurrency:
group: proposer-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-proposer:
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
steps:
- name: Tools sanity
run: |
set -euo pipefail
git --version
node --version
npm --version
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
node --input-type=module - <<'NODE' > /tmp/proposer.env
import fs from "node:fs";
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
const repoObj = ev?.repository || {};
const cloneUrl =
repoObj?.clone_url ||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
let owner =
repoObj?.owner?.login ||
repoObj?.owner?.username ||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
let repo =
repoObj?.name ||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
if (!owner || !repo) {
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
}
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
const defaultBranch = repoObj?.default_branch || "main";
const issueNumber =
ev?.issue?.number ||
ev?.issue?.index ||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
throw new Error("No issue number in event.json or workflow_dispatch input");
}
const labelName =
ev?.label?.name ||
ev?.label ||
"workflow_dispatch";
const u = new URL(cloneUrl);
const origin = u.origin;
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
: origin;
function sh(s){ return JSON.stringify(String(s)); }
process.stdout.write([
`CLONE_URL=${sh(cloneUrl)}`,
`OWNER=${sh(owner)}`,
`REPO=${sh(repo)}`,
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "✅ context:"
sed -n '1,120p' /tmp/proposer.env
- name: Gate on label state/approved
run: |
set -euo pipefail
source /tmp/proposer.env
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
echo " label=$LABEL_NAME => skip"
echo "SKIP=1" >> /tmp/proposer.env
exit 0
fi
echo "✅ proceed (issue=$ISSUE_NUMBER)"
- name: Fetch issue + API-hard gate on (state/approved present + proposer type)
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/issue.json
node --input-type=module - <<'NODE' >> /tmp/proposer.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
const title = String(issue.title || "");
const body = String(issue.body || "").replace(/\r\n/g, "\n");
const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name||"")).filter(Boolean) : [];
function pickLine(key) {
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
const m = body.match(re);
return m ? m[1].trim() : "";
}
const typeRaw = pickLine("Type");
const type = String(typeRaw || "").trim().toLowerCase();
const hasApproved = labels.includes("state/approved");
const proposer = new Set(["type/correction","type/fact-check"]);
const out = [];
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);
if (!hasApproved) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
} else if (!type) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
} else if (!proposer.has(type)) {
out.push(`SKIP=1`);
out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
}
process.stdout.write(out.join("\n") + "\n");
NODE
echo "✅ proposer gating:"
grep -E '^(ISSUE_TYPE|HAS_APPROVED|SKIP|SKIP_REASON)=' /tmp/proposer.env || true
- name: Comment issue if skipped
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" == "1" ]] || exit 0
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
REASON="${SKIP_REASON:-}"
TYPE="${ISSUE_TYPE:-}"
if [[ "$REASON" == "approved_not_present" ]]; then
MSG=" Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
elif [[ "$REASON" == "missing_type" ]]; then
MSG=" Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
else
MSG=" Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
fi
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD" || true
- name: Checkout default branch
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
rm -rf .git
git init -q
git remote add origin "$CLONE_URL"
git fetch --depth 1 origin "$DEFAULT_BRANCH"
git -c advice.detachedHead=false checkout -q FETCH_HEAD
git log -1 --oneline
echo "✅ workspace:"
ls -la | sed -n '1,120p'
- name: Detect app dir (repo-root vs ./site)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
APP_DIR="."
if [[ -d "site" && -f "site/package.json" ]]; then
APP_DIR="site"
fi
echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
echo "✅ APP_DIR=$APP_DIR"
ls -la "$APP_DIR" | sed -n '1,120p'
test -f "$APP_DIR/package.json" || { echo "❌ package.json missing in APP_DIR=$APP_DIR"; exit 1; }
test -d "$APP_DIR/scripts" || { echo "❌ scripts/ missing in APP_DIR=$APP_DIR"; exit 1; }
- name: NPM harden (reduce flakiness)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || exit 0
cd "$APP_DIR"
npm config set fetch-retries 5
npm config set fetch-retry-mintimeout 20000
npm config set fetch-retry-maxtimeout 120000
npm config set registry https://registry.npmjs.org
- name: Install deps (APP_DIR)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
cd "$APP_DIR"
npm ci --no-audit --no-fund
- name: Build dist baseline (APP_DIR)
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
cd "$APP_DIR"
npm run build
- name: Apply ticket (alias + commit) on bot branch
continue-on-error: true
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
run: |
set -euo pipefail
source /tmp/proposer.env
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
git config user.name "${BOT_GIT_NAME:-archicratie-bot}"
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
START_SHA="$(git rev-parse HEAD)"
TS="$(date -u +%Y%m%d-%H%M%S)"
BR="bot/proposer-${ISSUE_NUMBER}-${TS}"
echo "BRANCH=$BR" >> /tmp/proposer.env
git checkout -b "$BR"
export GITEA_OWNER="$OWNER"
export GITEA_REPO="$REPO"
export FORGE_BASE="$API_BASE"
LOG="/tmp/proposer-apply.log"
set +e
(cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
RC=$?
set -e
echo "APPLY_RC=$RC" >> /tmp/proposer.env
echo "== apply log (tail) =="
tail -n 200 "$LOG" || true
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=0" >> /tmp/proposer.env
exit 0
fi
if [[ "$START_SHA" == "$END_SHA" ]]; then
echo "NOOP=1" >> /tmp/proposer.env
else
echo "NOOP=0" >> /tmp/proposer.env
echo "END_SHA=$END_SHA" >> /tmp/proposer.env
fi
- name: Push bot branch
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip push"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo " no-op -> skip push"; exit 0; }
[[ -n "${BRANCH:-}" ]] || { echo " BRANCH unset -> skip push"; exit 0; }
AUTH_URL="$(node --input-type=module -e '
const [clone, tok] = process.argv.slice(1);
const u = new URL(clone);
u.username = "oauth2";
u.password = tok;
console.log(u.toString());
' "$CLONE_URL" "$FORGE_TOKEN")"
git remote set-url origin "$AUTH_URL"
git push -u origin "$BRANCH"
- name: Create PR + comment issue
if: ${{ always() }}
env:
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
[[ "${NOOP:-0}" == "0" ]] || exit 0
[[ -n "${BRANCH:-}" ]] || { echo " BRANCH unset -> skip PR"; exit 0; }
PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\n\nMerge si CI OK."
PR_PAYLOAD="$(node --input-type=module -e '
const [title, body, base, head] = process.argv.slice(1);
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
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 "$PR_PAYLOAD")"
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; }
MSG="✅ PR Proposer créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$C_PAYLOAD"
- name: Finalize (fail job if apply failed)
if: ${{ always() }}
run: |
set -euo pipefail
source /tmp/proposer.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
RC="${APPLY_RC:-0}"
if [[ "$RC" != "0" ]]; then
echo "❌ apply failed (rc=$RC)"
exit "$RC"
fi
echo "✅ apply ok"

View File

@@ -3,7 +3,7 @@ on: [push, workflow_dispatch]
jobs:
smoke:
runs-on: ubuntu-latest
runs-on: mac-ci
steps:
- run: node -v && npm -v
- run: echo "runner OK"

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -114,7 +114,6 @@ async function runMammoth(docxPath, assetsOutDirWebRoot) {
);
let html = result.value || "";
// Mammoth gives relative src="image-xx.png" ; we will prefix later
return html;
}
@@ -182,6 +181,25 @@ async function exists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
/**
* ✅ compat:
* - ancien : collection="archicratie" + slug="archicrat-ia/chapitre-3"
* - nouveau : collection="archicrat-ia" + slug="chapitre-3"
*
* But : toujours écrire dans src/content/archicrat-ia/<slugSansPrefix>.mdx
*/
function normalizeDest(collection, slug) {
let outCollection = String(collection || "").trim();
let outSlug = String(slug || "").trim().replace(/^\/+|\/+$/g, "");
if (outCollection === "archicratie" && outSlug.startsWith("archicrat-ia/")) {
outCollection = "archicrat-ia";
outSlug = outSlug.replace(/^archicrat-ia\//, "");
}
return { outCollection, outSlug };
}
async function main() {
const args = parseArgs(process.argv);
const manifestPath = path.resolve(args.manifest);
@@ -203,11 +221,14 @@ async function main() {
for (const it of selected) {
const docxPath = path.resolve(it.source);
const outFile = path.resolve("src/content", it.collection, `${it.slug}.mdx`);
const { outCollection, outSlug } = normalizeDest(it.collection, it.slug);
const outFile = path.resolve("src/content", outCollection, `${outSlug}.mdx`);
const outDir = path.dirname(outFile);
const assetsPublicDir = path.posix.join("/imported", it.collection, it.slug);
const assetsDiskDir = path.resolve("public", "imported", it.collection, it.slug);
const assetsPublicDir = path.posix.join("/imported", outCollection, outSlug);
const assetsDiskDir = path.resolve("public", "imported", outCollection, outSlug);
if (!(await exists(docxPath))) {
throw new Error(`Missing source docx: ${docxPath}`);
@@ -241,18 +262,20 @@ async function main() {
html = rewriteLocalImageLinks(html, assetsPublicDir);
body = html.trim() ? html : "<p>(Import vide)</p>";
}
const defaultVersion = process.env.PUBLIC_RELEASE || "0.1.0";
// ✅ IMPORTANT: archicrat-ia partage edition/status avec archicratie (pas de migration frontmatter)
const schemaDefaultsByCollection = {
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
ia: { edition: "ia", status: "cas_pratique", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
archicratie: { edition: "archicratie", status: "modele_sociopolitique", level: 1 },
"archicrat-ia": { edition: "archicrat-ia", status: "essai_these", level: 1 },
ia: { edition: "ia", status: "cas_pratique", level: 1 },
traite: { edition: "traite", status: "ontodynamique", level: 1 },
glossaire: { edition: "glossaire", status: "lexique", level: 1 },
atlas: { edition: "atlas", status: "atlas", level: 1 },
};
const defaults = schemaDefaultsByCollection[it.collection] || { edition: it.collection, status: "draft", level: 1 };
const defaults = schemaDefaultsByCollection[outCollection] || { edition: outCollection, status: "draft", level: 1 };
const fm = [
"---",
@@ -282,4 +305,4 @@ async function main() {
main().catch((e) => {
console.error("\nERROR:", e?.message || e);
process.exit(1);
});
});

View File

@@ -1,2 +1,5 @@
{}
{
"/archicrat-ia/chapitre-3/": {
"p-1-60c7ea48": "p-1-a21087b0"
}
}

View File

@@ -0,0 +1,10 @@
schema: 1
page: archicrat-ia/chapitre-1
paras:
p-0-8d27a7f5:
refs:
- url: https://auth.archicratie.trans-hands.synology.me/authenticated
label: Lien web
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-02-27T12:34:31.704Z
fromIssue: 142

View File

@@ -0,0 +1,9 @@
schema: 1
page: archicrat-ia/chapitre-1
paras:
p-1-8a6c18bf:
comments_editorial:
- text: Yeaha
status: new
ts: 2026-02-27T12:40:39.462Z
fromIssue: 143

View File

@@ -0,0 +1,18 @@
schema: 1
page: archicrat-ia/chapitre-3
paras:
p-0-ace27175:
media:
- type: image
src: /media/archicrat-ia/chapitre-3/p-0-ace27175/Capture_d_e_cran_2025-05-05_a_19.20.40.png
caption: "[Media] p-0-ace27175 — Chapitre 3 — Philosophies du pouvoir et
archicration"
credit: ""
ts: 2026-02-27T12:43:14.259Z
fromIssue: 144
refs:
- url: https://gitea.archicratie.trans-hands.synology.me
label: Gitea
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-03-02T19:53:21.252Z
fromIssue: 169

View File

@@ -0,0 +1,11 @@
schema: 1
page: archicrat-ia/chapitre-3
paras:
p-1-60c7ea48:
refs:
- url: https://gitea.archicratie.trans-hands.synology.me
label: Gitea
kind: (livre / article / vidéo / site / autre) Site
ts: 2026-03-02T20:01:55.858Z
fromIssue: 172
# testB: hotpatch-auto gate proof

View File

@@ -3,14 +3,11 @@ import { getCollection } from "astro:content";
const { currentSlug } = Astro.props;
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"))
// ✅ Après migration : TOC = collection "archicrat-ia"
const entries = (await getCollection("archicrat-ia"))
.sort((a, b) => (a.data.order ?? 0) - (b.data.order ?? 0));
// ✅ On route lEssai-thèse sur /archicrat-ia/<slug-sans-prefix>/
// (Astro trailingSlash = always → on garde le "/" final)
const strip = (s) => String(s || "").replace(/^archicrat-ia\//, "");
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
const href = (slug) => `/archicrat-ia/${slug}/`;
---
<nav class="toc-global" aria-label="Table des matières — ArchiCraT-IA">
@@ -163,4 +160,4 @@ const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
const active = document.querySelector(".toc-global .toc-item.is-active");
if (active) active.scrollIntoView({ block: "nearest" });
})();
</script>
</script>

View File

@@ -14,7 +14,7 @@ source:
---
Ce chapitre se tient à un point nodal de notre essai-thèse : il ouvre un espace dexploration systématique des formes conceptuelles et philosophiques à travers lesquelles le pouvoir se configure comme régime de régulation. Il ne sagit pas ici de revenir une nouvelle fois sur les fondements de lautorité, ni dinterroger la légitimité politique au sens classique du terme, ni même denquêter sur la genèse des institutions. Lambition est autre, structurelle, transversale, morphologique, elle tentera darpenter, à même les dispositifs, les pensées, les théorisations et les expériences, les modalités différentiées par lesquelles sinstaurent, séprouvent et se disputent les formes de régulation du vivre-ensemble.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point dorigine, ne prétend restituer aucune ontologie stable du politique. Ce quil donne à lire, cest une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières.
Dès lors, ce chapitre ne postule aucun fondement, ne cherche aucun point dorigine, ne prétend restituer aucune ontologie stable du politique. Ce quil donne à lire, cest une cartographie dynamique des régimes de régulation, traversée par des formes irréductibles, non homogènes, souvent conflictuelles, parfois incompatibles, mais toutes pensées comme des configurations singulières, et souvent complémentaires.
Ainsi, loin dêtre une galerie illustrative de théories politiques juxtaposées, le chapitre sagence comme une topologie critique, une plongée stratigraphique dans les scènes où sarticule la régulation — entendue ici non comme stabilisation externe ou ajustement technico-fonctionnel, mais comme dispositif instituant, tension structurante, scène traversée de conflictualité et dexigence normative. Car à nos yeux, la régulation nest pas ce qui vient après le pouvoir, elle en est la forme même constitutive — son architecture, son rythme, son épaisseur. Elle est ce par quoi le pouvoir ne se contente pas dêtre exercé, mais sinstitue, se justifie, se dispute, se recompose.

View File

@@ -2,7 +2,7 @@ import { defineCollection, z } from "astro:content";
const linkSchema = z.object({
type: z.enum(["definition", "appui", "transposition"]),
target: z.string().min(1), // URL interne (ex: /glossaire/archicratie/) ou slug
target: z.string().min(1),
note: z.string().optional()
});
@@ -12,7 +12,6 @@ const baseTextSchema = z.object({
version: z.string().min(1),
concepts: z.array(z.string().min(1)).default([]),
links: z.array(linkSchema).default([]),
// optionnels mais utiles dès maintenant
order: z.number().int().nonnegative().optional(),
summary: z.string().optional()
});
@@ -50,20 +49,31 @@ const atlas = defineCollection({
})
});
// ✅ NOUVELLE collection : archicrat-ia (Essai-thèse)
// NOTE : on accepte temporairement edition/status "archicratie/modele_sociopolitique"
// si tes MDX nont pas encore été normalisés.
// Quand tu voudras "strict", on passera à edition="archicrat-ia" status="essai_these"
// + update frontmatter des 7 fichiers.
const archicratIa = defineCollection({
type: "content",
schema: baseTextSchema.extend({
edition: z.union([z.literal("archicrat-ia"), z.literal("archicratie")]),
status: z.union([z.literal("essai_these"), z.literal("modele_sociopolitique")])
})
});
// Glossaire (référentiel terminologique)
const glossaire = defineCollection({
type: "content",
schema: z.object({
title: z.string().min(1), // Titre public (souvent identique au terme)
term: z.string().min(1), // Terme canonique
title: z.string().min(1),
term: z.string().min(1),
aliases: z.array(z.string().min(1)).default([]),
edition: z.literal("glossaire"),
status: z.literal("referentiel"),
version: z.string().min(1),
// Micro-définition affichable en popover (courte, stable)
definitionShort: z.string().min(1),
concepts: z.array(z.string().min(1)).default([]),
// Liens typés (vers ouvrages ou autres termes)
links: z.array(linkSchema).default([])
})
});
@@ -73,5 +83,8 @@ export const collections = {
archicratie,
ia,
glossaire,
atlas
};
atlas,
// ⚠️ clé avec tiret => doit être quotée
"archicrat-ia": archicratIa
};

View File

@@ -5,12 +5,11 @@ import EditionToc from "../../components/EditionToc.astro";
import LocalToc from "../../components/LocalToc.astro";
export async function getStaticPaths() {
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"));
// ✅ Après migration : plus de filtre par prefix, on prend toute la collection
const entries = await getCollection("archicrat-ia");
return entries.map((entry) => ({
// ✅ inline : jamais de helper externe (évite "stripPrefix is not defined")
params: { slug: entry.slug.replace(/^archicrat-ia\//, "") },
params: { slug: entry.slug },
props: { entry },
}));
}
@@ -35,4 +34,4 @@ const { Content, headings } = await entry.render();
<h1>{entry.data.title}</h1>
<Content />
</EditionLayout>
</EditionLayout>

View File

@@ -2,13 +2,12 @@
import SiteLayout from "../../layouts/SiteLayout.astro";
import { getCollection } from "astro:content";
const entries = (await getCollection("archicratie"))
.filter((e) => e.slug.startsWith("archicrat-ia/"));
// ✅ Après migration physique : collection = "archicrat-ia", slug = "chapitre-3" (sans prefix)
const entries = await getCollection("archicrat-ia");
entries.sort((a, b) => (a.data.order ?? 9999) - (b.data.order ?? 9999));
const strip = (slug) => slug.replace(/^archicrat-ia\//, "");
const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
const href = (slug) => `/archicrat-ia/${slug}/`;
---
<SiteLayout title="Essai-thèse — ArchiCraT-IA">
@@ -19,4 +18,4 @@ const href = (slug) => `/archicrat-ia/${strip(slug)}/`;
<li><a href={href(e.slug)}>{e.data.title}</a></li>
))}
</ul>
</SiteLayout>
</SiteLayout>