Compare commits

...

24 Commits

Author SHA1 Message Date
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
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
20 changed files with 693 additions and 173 deletions

View File

@@ -17,13 +17,11 @@ defaults:
shell: bash
concurrency:
group: anno-apply-${{ github.event.issue.number || inputs.issue || 'manual' }}
group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
apply-approved:
# ✅ Job ne démarre QUE si state/approved (ou workflow_dispatch)
if: ${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'state/approved' }}
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -35,12 +33,11 @@ jobs:
git --version
node --version
npm --version
curl --version | head -n 1
- 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"
@@ -84,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;
@@ -98,7 +97,6 @@ jobs:
function sh(s){ return JSON.stringify(String(s)); }
// ✅ defaults antifragiles (empêchent les steps "always" de faire n'importe quoi)
process.stdout.write([
`CLONE_URL=${sh(cloneUrl)}`,
`OWNER=${sh(owner)}`,
@@ -106,39 +104,57 @@ jobs:
`DEFAULT_BRANCH=${sh(defaultBranch)}`,
`ISSUE_NUMBER=${sh(issueNumber)}`,
`LABEL_NAME=${sh(labelName)}`,
`API_BASE=${sh(apiBase)}`,
`SKIP=${sh("0")}`,
`SKIP_REASON=${sh("")}`,
`APPLY_RC=${sh("999")}`,
`NOOP=${sh("1")}`
`API_BASE=${sh(apiBase)}`
].join("\n") + "\n");
NODE
echo "✅ context:"
sed -n '1,160p' /tmp/anno.env
sed -n '1,120p' /tmp/anno.env
- name: Fetch issue + gate on Type (skip Proposer)
- name: Early gate (label event fast-skip, but tolerant)
run: |
set -euo pipefail
source /tmp/anno.env
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 "✅ 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; }
# ✅ on écrit le JSON dans un fichier (FINI JSON.parse('-'))
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
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 - /tmp/issue.json >> /tmp/anno.env <<'NODE'
node --input-type=module - <<'NODE' >> /tmp/anno.env
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
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);
@@ -155,6 +171,14 @@ jobs:
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")}`);
@@ -171,7 +195,7 @@ jobs:
process.stdout.write(out.join("\n") + "\n");
NODE
echo "✅ issue type gating:"
echo "✅ gating result:"
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
@@ -184,24 +208,28 @@ jobs:
[[ "${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
test -n "${API_BASE:-}" || 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** (correction/fact-check + cat/*).\n- Le bot n'applique **jamais** Proposer.\n\n✅ Action : traitement éditorial manuel."
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.\n✅ Action : traitement manuel si nécessaire."
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\n✅ Action : corriger le ticket (Type: type/media|type/reference|type/comment) ou traiter manuellement."
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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
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" \
@@ -245,6 +273,7 @@ jobs:
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
npm run build
test -f dist/para-index.json || {
echo "❌ missing dist/para-index.json after build"
ls -la dist | sed -n '1,200p' || true
@@ -262,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; }
@@ -292,7 +322,7 @@ jobs:
END_SHA="$(git rev-parse HEAD)"
if [[ "$RC" -ne 0 ]]; then
echo "NOOP=1" >> /tmp/anno.env
echo "NOOP=0" >> /tmp/anno.env
exit 0
fi
@@ -310,13 +340,15 @@ jobs:
run: |
set -euo pipefail
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-999}"
[[ "$RC" != "0" ]] || { echo " no failure detected"; exit 0; }
RC="${APPLY_RC:-0}"
if [[ "$RC" == "0" ]]; then
echo " no failure detected"
exit 0
fi
test -n "${FORGE_TOKEN:-}" || exit 0
test -n "${API_BASE:-}" || exit 0
if [[ -f /tmp/apply.log ]]; then
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
@@ -327,33 +359,7 @@ jobs:
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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-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 || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-999}" == "0" ]] || exit 0
[[ "${NOOP:-1}" == "1" ]] || exit 0
test -n "${FORGE_TOKEN:-}" || exit 0
test -n "${API_BASE:-}" || 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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
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" \
@@ -368,9 +374,9 @@ jobs:
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-999}" == "0" ]] || { echo " apply not ok -> skip push"; exit 0; }
[[ "${NOOP:-1}" == "0" ]] || { echo " no-op -> skip push"; exit 0; }
test -n "${BRANCH:-}" || { echo " no BRANCH -> skip push"; 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);
@@ -392,10 +398,8 @@ jobs:
source /tmp/anno.env || true
[[ "${SKIP:-0}" != "1" ]] || exit 0
[[ "${APPLY_RC:-999}" == "0" ]] || { echo " apply not ok -> skip PR"; exit 0; }
[[ "${NOOP:-1}" == "0" ]] || { echo " no-op -> skip PR"; exit 0; }
test -n "${BRANCH:-}" || { echo " no BRANCH -> skip PR"; exit 0; }
test -n "${END_SHA:-}" || { echo " no END_SHA -> skip PR"; exit 0; }
[[ "${APPLY_RC:-0}" == "0" ]] || { echo " apply failed -> skip PR"; exit 0; }
[[ "${NOOP:-0}" == "0" ]] || { echo " no-op -> skip PR"; exit 0; }
PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."
@@ -405,8 +409,7 @@ jobs:
console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"
PR_JSON="$(curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
PR_JSON="$(curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
@@ -422,13 +425,14 @@ jobs:
MSG="✅ PR 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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
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"
echo "✅ PR: $PR_URL"
- name: Finalize (fail job if apply failed)
if: ${{ always() }}
run: |
@@ -437,7 +441,7 @@ jobs:
[[ "${SKIP:-0}" != "1" ]] || { echo " skipped"; exit 0; }
RC="${APPLY_RC:-999}"
RC="${APPLY_RC:-0}"
if [[ "$RC" != "0" ]]; then
echo "❌ apply failed (rc=$RC)"
exit "$RC"

View File

@@ -17,13 +17,11 @@ defaults:
shell: bash
concurrency:
group: anno-reject-${{ github.event.issue.number || inputs.issue || 'manual' }}
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
reject:
# ✅ Job ne démarre QUE si state/rejected (ou workflow_dispatch)
if: ${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'state/rejected' }}
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -33,12 +31,11 @@ jobs:
run: |
set -euo pipefail
node --version
curl --version | head -n 1
- 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,14 +75,20 @@ 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;
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
: (cloneUrl ? new URL(cloneUrl).origin : "");
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)); }
@@ -101,26 +104,39 @@ jobs:
echo "✅ context:"
sed -n '1,120p' /tmp/reject.env
- name: Comment + close (only if not conflicting with state/approved)
- name: Early gate (fast-skip, tolerant)
run: |
set -euo pipefail
source /tmp/reject.env
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
- 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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
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
# conflict guard: approved + rejected => do nothing, comment warning
node --input-type=module - /tmp/reject.issue.json > /tmp/reject.flags <<'NODE'
node --input-type=module - <<'NODE' > /tmp/reject.flags
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
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");
@@ -129,11 +145,16 @@ jobs:
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 --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
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" \
@@ -142,20 +163,16 @@ jobs:
exit 0
fi
# comment reject
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
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"
# close issue
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X PATCH \
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \

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: 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

@@ -93,7 +93,7 @@ jobs:
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: |
@@ -109,24 +109,49 @@ jobs:
echo "== changed files =="
echo "$CHANGED" | sed -n '1,260p'
# 0) Forçage manuel
if [[ "$FORCE" == "1" ]]; then
echo "GO=1" >> /tmp/deploy.env
echo "GO=1" >> /tmp/deploy.env
echo "MODE='full'" >> /tmp/deploy.env
echo "✅ force=1 -> MODE=full (rebuild+restart)"
exit 0
fi
# Auto mode: uniquement annotations/media => hotpatch only
# 1) Détection des classes de changements
HAS_FULL=0
HAS_HOTPATCH=0
# FULL si build-impacting (zéro surprise)
if echo "$CHANGED" | grep -qE '^(src/content/|src/anchors/|src/pages/|scripts/)'; then
HAS_FULL=1
fi
# HOTPATCH si annotations/media
if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then
HAS_HOTPATCH=1
fi
echo "Gate flags: HAS_FULL=$HAS_FULL HAS_HOTPATCH=$HAS_HOTPATCH"
# 2) Décision (priorité au FULL)
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)"
exit 0
fi
if [[ "$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"
exit 0
fi
echo "GO=0" >> /tmp/deploy.env
echo "MODE='skip'" >> /tmp/deploy.env
echo " no deploy-relevant change -> skip deploy"
- name: Toolchain sanity + resolve COMPOSE_PROJECT_NAME
run: |
set -euo pipefail

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

@@ -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

@@ -10,3 +10,9 @@ paras:
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

@@ -11,10 +11,12 @@ summary: ""
source:
kind: docx
path: "sources/docx/archicrat-ia/Chapitre_3—Philosophies_du_pouvoir_et_Archicration-pour_une_topologie_differenciee_des_regimes_regulateurs-version_officielle.docx"
<!-- testA: full-auto gate proof -->
---
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>