Compare commits
12 Commits
hotfix/fix
...
hotfix/pro
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d3473d66c | |||
| 513ae72e85 | |||
| 4c4dd1c515 | |||
| 46b15ed6ab | |||
| a015e72f7c | |||
|
|
d5df7d77a0 | ||
| ec3ceee862 | |||
| 867475c3ff | |||
| b024c5557c | |||
| 93306f360d | |||
| 52847d999d | |||
| 06482a9f8d |
@@ -122,21 +122,23 @@ jobs:
|
|||||||
echo "Context:"
|
echo "Context:"
|
||||||
sed -n '1,200p' /tmp/proposer.env
|
sed -n '1,200p' /tmp/proposer.env
|
||||||
|
|
||||||
- name: Early gate
|
- name: Early gate (tolerant on empty issue label payload)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/proposer.env
|
source /tmp/proposer.env
|
||||||
|
|
||||||
|
echo "event=$EVENT_NAME label=${LABEL_NAME:-<empty>}"
|
||||||
|
|
||||||
if [[ "$EVENT_NAME" == "issues" ]]; then
|
if [[ "$EVENT_NAME" == "issues" ]]; then
|
||||||
if [[ "$LABEL_NAME" != "state/approved" ]]; then
|
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
|
||||||
echo "issues/labeled but label=$LABEL_NAME -> skip"
|
echo "issues/labeled with explicit non-approved label=$LABEL_NAME -> skip"
|
||||||
echo 'SKIP=1' >> /tmp/proposer.env
|
echo 'SKIP=1' >> /tmp/proposer.env
|
||||||
echo 'SKIP_REASON="label_not_state_approved"' >> /tmp/proposer.env
|
echo 'SKIP_REASON="label_not_state_approved_event"' >> /tmp/proposer.env
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Proceed"
|
echo "Proceed to API-based selection/gating"
|
||||||
|
|
||||||
- name: Checkout default branch
|
- name: Checkout default branch
|
||||||
run: |
|
run: |
|
||||||
@@ -214,6 +216,43 @@ jobs:
|
|||||||
echo "Target batch:"
|
echo "Target batch:"
|
||||||
grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env
|
grep -E '^(TARGET_PRIMARY_ISSUE|TARGET_ISSUES|TARGET_COUNT|TARGET_CHEMIN)=' /tmp/proposer.env
|
||||||
|
|
||||||
|
- name: Derive deterministic batch identity
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
|
||||||
|
|
||||||
|
export TARGET_ISSUES TARGET_CHEMIN
|
||||||
|
|
||||||
|
node --input-type=module - <<'NODE'
|
||||||
|
import fs from "node:fs";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
const issues = String(process.env.TARGET_ISSUES || "")
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => Number(a) - Number(b));
|
||||||
|
|
||||||
|
const chemin = String(process.env.TARGET_CHEMIN || "").trim();
|
||||||
|
const keySource = `${chemin}::${issues.join(",")}`;
|
||||||
|
const hash = crypto.createHash("sha1").update(keySource).digest("hex").slice(0, 12);
|
||||||
|
const primary = issues[0] || "0";
|
||||||
|
const batchBranch = `bot/proposer-${primary}-${hash}`;
|
||||||
|
|
||||||
|
fs.appendFileSync(
|
||||||
|
"/tmp/proposer.env",
|
||||||
|
[
|
||||||
|
`BATCH_KEY=${JSON.stringify(keySource)}`,
|
||||||
|
`BATCH_HASH=${JSON.stringify(hash)}`,
|
||||||
|
`BATCH_BRANCH=${JSON.stringify(batchBranch)}`
|
||||||
|
].join("\n") + "\n"
|
||||||
|
);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
echo "Batch identity:"
|
||||||
|
grep -E '^(BATCH_KEY|BATCH_HASH|BATCH_BRANCH)=' /tmp/proposer.env
|
||||||
|
|
||||||
- name: Inspect open proposer PRs
|
- name: Inspect open proposer PRs
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
@@ -229,6 +268,8 @@ jobs:
|
|||||||
-o /tmp/open_pulls.json
|
-o /tmp/open_pulls.json
|
||||||
|
|
||||||
export TARGET_ISSUES="${TARGET_ISSUES:-}"
|
export TARGET_ISSUES="${TARGET_ISSUES:-}"
|
||||||
|
export BATCH_BRANCH="${BATCH_BRANCH:-}"
|
||||||
|
export BATCH_KEY="${BATCH_KEY:-}"
|
||||||
|
|
||||||
node --input-type=module - <<'NODE' >> /tmp/proposer.env
|
node --input-type=module - <<'NODE' >> /tmp/proposer.env
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
@@ -239,15 +280,21 @@ jobs:
|
|||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const batchBranch = String(process.env.BATCH_BRANCH || "");
|
||||||
|
const batchKey = String(process.env.BATCH_KEY || "");
|
||||||
|
|
||||||
const proposerOpen = Array.isArray(pulls)
|
const proposerOpen = Array.isArray(pulls)
|
||||||
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
|
? pulls.filter((pr) => String(pr?.head?.ref || "").startsWith("bot/proposer-"))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const current = proposerOpen.find((pr) => {
|
const sameBatch = proposerOpen.find((pr) => {
|
||||||
const ref = String(pr?.head?.ref || "");
|
const ref = String(pr?.head?.ref || "");
|
||||||
const title = String(pr?.title || "");
|
const title = String(pr?.title || "");
|
||||||
const body = String(pr?.body || "");
|
const body = String(pr?.body || "");
|
||||||
|
|
||||||
|
if (batchBranch && ref === batchBranch) return true;
|
||||||
|
if (batchKey && body.includes(`Batch-Key: ${batchKey}`)) return true;
|
||||||
|
|
||||||
return issues.some((n) =>
|
return issues.some((n) =>
|
||||||
ref.startsWith(`bot/proposer-${n}-`) ||
|
ref.startsWith(`bot/proposer-${n}-`) ||
|
||||||
title.includes(`#${n}`) ||
|
title.includes(`#${n}`) ||
|
||||||
@@ -258,10 +305,11 @@ jobs:
|
|||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
|
|
||||||
if (current) {
|
if (sameBatch) {
|
||||||
out.push("SKIP=1");
|
out.push("SKIP=1");
|
||||||
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
|
out.push(`SKIP_REASON=${JSON.stringify("issue_already_has_open_pr")}`);
|
||||||
out.push(`OPEN_PR_URL=${JSON.stringify(String(current.html_url || current.url || ""))}`);
|
out.push(`OPEN_PR_URL=${JSON.stringify(String(sameBatch.html_url || sameBatch.url || ""))}`);
|
||||||
|
out.push(`OPEN_PR_BRANCH=${JSON.stringify(String(sameBatch?.head?.ref || ""))}`);
|
||||||
} else if (proposerOpen.length > 0) {
|
} else if (proposerOpen.length > 0) {
|
||||||
const first = proposerOpen[0];
|
const first = proposerOpen[0];
|
||||||
out.push("SKIP=1");
|
out.push("SKIP=1");
|
||||||
@@ -273,6 +321,22 @@ jobs:
|
|||||||
process.stdout.write(out.join("\n") + (out.length ? "\n" : ""));
|
process.stdout.write(out.join("\n") + (out.length ? "\n" : ""));
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
|
- name: Guard on remote batch branch before heavy work
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "Skipped"; exit 0; }
|
||||||
|
|
||||||
|
if git ls-remote --exit-code --heads origin "$BATCH_BRANCH" >/dev/null 2>&1; then
|
||||||
|
echo 'SKIP=1' >> /tmp/proposer.env
|
||||||
|
echo 'SKIP_REASON="batch_branch_exists_without_pr"' >> /tmp/proposer.env
|
||||||
|
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
|
||||||
|
echo "Remote batch branch already exists -> skip duplicate materialization"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Remote batch branch is free"
|
||||||
|
|
||||||
- name: Comment issue if queued / skipped
|
- name: Comment issue if queued / skipped
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
@@ -284,6 +348,11 @@ jobs:
|
|||||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||||
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
|
[[ "${EVENT_NAME:-}" != "push" ]] || exit 0
|
||||||
|
|
||||||
|
if [[ "${SKIP_REASON:-}" == "label_not_state_approved_event" || "${SKIP_REASON:-}" == "label_not_state_approved" ]]; then
|
||||||
|
echo "Skip reason=${SKIP_REASON} -> no comment"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || exit 0
|
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||||
|
|
||||||
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
|
ISSUE_TO_COMMENT="${ISSUE_NUMBER:-0}"
|
||||||
@@ -297,7 +366,13 @@ jobs:
|
|||||||
MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main."
|
MSG="Ticket queued in proposer queue. An open proposer PR already exists: ${OPEN_PR_URL:-"(URL unavailable)"}. The workflow will resume after merge on main."
|
||||||
;;
|
;;
|
||||||
issue_already_has_open_pr)
|
issue_already_has_open_pr)
|
||||||
MSG="This ticket already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
|
MSG="This batch already has an open proposer PR: ${OPEN_PR_URL:-"(URL unavailable)"}"
|
||||||
|
;;
|
||||||
|
batch_branch_exists_without_pr)
|
||||||
|
MSG="This batch already has a remote batch branch (${OPEN_PR_BRANCH:-"(unknown branch)"}). Manual inspection is required before any new proposer PR is created."
|
||||||
|
;;
|
||||||
|
batch_branch_already_materialized)
|
||||||
|
MSG="This batch was already materialized by another run on branch ${OPEN_PR_BRANCH:-"(unknown branch)"}. No duplicate PR was created."
|
||||||
;;
|
;;
|
||||||
explicit_issue_missing_chemin)
|
explicit_issue_missing_chemin)
|
||||||
MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable."
|
MSG="Proposer Apply: cannot process this ticket automatically because field Chemin is missing or unreadable."
|
||||||
@@ -319,6 +394,7 @@ jobs:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
export MSG
|
||||||
node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json
|
node --input-type=module - <<'NODE' > /tmp/proposer.skip.comment.json
|
||||||
const msg = process.env.MSG || "";
|
const msg = process.env.MSG || "";
|
||||||
process.stdout.write(JSON.stringify({ body: msg }));
|
process.stdout.write(JSON.stringify({ body: msg }));
|
||||||
@@ -375,8 +451,7 @@ jobs:
|
|||||||
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"
|
||||||
|
|
||||||
START_SHA="$(git rev-parse HEAD)"
|
START_SHA="$(git rev-parse HEAD)"
|
||||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
BR="$BATCH_BRANCH"
|
||||||
BR="bot/proposer-${TARGET_PRIMARY_ISSUE}-${TS}"
|
|
||||||
echo "BRANCH=$BR" >> /tmp/proposer.env
|
echo "BRANCH=$BR" >> /tmp/proposer.env
|
||||||
git checkout -b "$BR"
|
git checkout -b "$BR"
|
||||||
|
|
||||||
@@ -509,6 +584,27 @@ jobs:
|
|||||||
--data-binary @/tmp/proposer.failure.comment.json || true
|
--data-binary @/tmp/proposer.failure.comment.json || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Late guard against duplicate batch materialization
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env || true
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
|
||||||
|
[[ "${REBASE_RC:-0}" == "0" ]] || exit 0
|
||||||
|
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||||
|
|
||||||
|
REMOTE_SHA="$(git ls-remote --heads origin "$BATCH_BRANCH" | awk 'NR==1 {print $1}')"
|
||||||
|
|
||||||
|
if [[ -n "${REMOTE_SHA:-}" && "${REMOTE_SHA}" != "${END_SHA:-}" ]]; then
|
||||||
|
echo 'SKIP=1' >> /tmp/proposer.env
|
||||||
|
echo 'SKIP_REASON="batch_branch_already_materialized"' >> /tmp/proposer.env
|
||||||
|
echo "OPEN_PR_BRANCH=${BATCH_BRANCH}" >> /tmp/proposer.env
|
||||||
|
echo "Remote batch branch already exists at $REMOTE_SHA -> skip duplicate push/PR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Late guard OK"
|
||||||
|
|
||||||
- name: Push bot branch
|
- name: Push bot branch
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
@@ -546,46 +642,74 @@ jobs:
|
|||||||
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
[[ "${NOOP:-0}" == "0" ]] || exit 0
|
||||||
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
|
[[ -n "${BRANCH:-}" ]] || { echo "BRANCH unset -> skip PR"; exit 0; }
|
||||||
|
|
||||||
|
test -n "${FORGE_TOKEN:-}" || { echo "Missing FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
|
OPEN_PRS_JSON="$(curl -fsS \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls?state=open&limit=100")"
|
||||||
|
|
||||||
|
export OPEN_PRS_JSON BATCH_BRANCH BATCH_KEY
|
||||||
|
|
||||||
|
EXISTING_PR_URL="$(node --input-type=module -e '
|
||||||
|
const pulls = JSON.parse(process.env.OPEN_PRS_JSON || "[]");
|
||||||
|
const branch = String(process.env.BATCH_BRANCH || "");
|
||||||
|
const key = String(process.env.BATCH_KEY || "");
|
||||||
|
const current = Array.isArray(pulls)
|
||||||
|
? pulls.find((pr) => {
|
||||||
|
const ref = String(pr?.head?.ref || "");
|
||||||
|
const body = String(pr?.body || "");
|
||||||
|
return (branch && ref === branch) || (key && body.includes(`Batch-Key: ${key}`));
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
process.stdout.write(current ? String(current.html_url || current.url || "") : "");
|
||||||
|
')"
|
||||||
|
|
||||||
|
if [[ -n "${EXISTING_PR_URL:-}" ]]; then
|
||||||
|
echo "PR already exists for this batch: $EXISTING_PR_URL"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
|
if [[ "${TARGET_COUNT:-0}" == "1" ]]; then
|
||||||
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
|
PR_TITLE="proposer: apply ticket #${TARGET_PRIMARY_ISSUE}"
|
||||||
else
|
else
|
||||||
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
|
PR_TITLE="proposer: apply ${TARGET_COUNT} tickets on ${TARGET_CHEMIN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export TITLE="$PR_TITLE"
|
export PR_TITLE TARGET_CHEMIN TARGET_ISSUES BRANCH END_SHA DEFAULT_BRANCH OWNER BATCH_KEY
|
||||||
export CHEMIN="$TARGET_CHEMIN"
|
|
||||||
export ISSUES="$TARGET_ISSUES"
|
|
||||||
export BRANCH="$BRANCH"
|
|
||||||
export END_SHA="${END_SHA:-unknown}"
|
|
||||||
export DEFAULT_BRANCH="$DEFAULT_BRANCH"
|
|
||||||
export OWNER="$OWNER"
|
|
||||||
|
|
||||||
node --input-type=module - <<'NODE' > /tmp/proposer.pr.json
|
node --input-type=module -e '
|
||||||
const issues = String(process.env.ISSUES || "")
|
import fs from "node:fs";
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
const body = [
|
const issues = String(process.env.TARGET_ISSUES || "")
|
||||||
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
|
.trim()
|
||||||
"",
|
.split(/\s+/)
|
||||||
`- Chemin: ${process.env.CHEMIN || "(inconnu)"}`,
|
.filter(Boolean);
|
||||||
"- Tickets:",
|
|
||||||
...issues.map((n) => ` - #${n}`),
|
|
||||||
`- Branche: ${process.env.BRANCH || ""}`,
|
|
||||||
`- Commit: ${process.env.END_SHA || "unknown"}`,
|
|
||||||
"",
|
|
||||||
"Merge si CI OK."
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify({
|
const body = [
|
||||||
title: process.env.TITLE || "proposer: apply tickets",
|
`PR auto depuis ticket${issues.length > 1 ? "s" : ""} ${issues.map((n) => `#${n}`).join(", ")} (state/approved).`,
|
||||||
body,
|
"",
|
||||||
base: process.env.DEFAULT_BRANCH || "main",
|
`- Chemin: ${process.env.TARGET_CHEMIN || "(inconnu)"}`,
|
||||||
head: `${process.env.OWNER}:${process.env.BRANCH}`,
|
"- Tickets:",
|
||||||
allow_maintainer_edit: true
|
...issues.map((n) => ` - #${n}`),
|
||||||
}));
|
`- Branche: ${process.env.BRANCH || ""}`,
|
||||||
NODE
|
`- Commit: ${process.env.END_SHA || "unknown"}`,
|
||||||
|
`- Batch-Key: ${process.env.BATCH_KEY || ""}`,
|
||||||
|
"",
|
||||||
|
"Merge si CI OK."
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
"/tmp/proposer.pr.json",
|
||||||
|
JSON.stringify({
|
||||||
|
title: process.env.PR_TITLE || "proposer: apply tickets",
|
||||||
|
body,
|
||||||
|
base: process.env.DEFAULT_BRANCH || "main",
|
||||||
|
head: `${process.env.OWNER}:${process.env.BRANCH}`,
|
||||||
|
allow_maintainer_edit: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
'
|
||||||
|
|
||||||
PR_JSON="$(curl -fsS -X POST \
|
PR_JSON="$(curl -fsS -X POST \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
@@ -593,10 +717,7 @@ jobs:
|
|||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
|
||||||
--data-binary @/tmp/proposer.pr.json)"
|
--data-binary @/tmp/proposer.pr.json)"
|
||||||
|
|
||||||
PR_URL="$(node --input-type=module -e '
|
PR_URL="$(node --input-type=module -e 'const pr = JSON.parse(process.argv[1] || "{}"); console.log(pr.html_url || pr.url || "");' "$PR_JSON")"
|
||||||
const pr = JSON.parse(process.argv[1] || "{}");
|
|
||||||
console.log(pr.html_url || pr.url || "");
|
|
||||||
' "$PR_JSON")"
|
|
||||||
|
|
||||||
test -n "$PR_URL" || {
|
test -n "$PR_URL" || {
|
||||||
echo "PR URL missing. Raw: $PR_JSON"
|
echo "PR URL missing. Raw: $PR_JSON"
|
||||||
@@ -605,15 +726,21 @@ jobs:
|
|||||||
|
|
||||||
for ISSUE in $TARGET_ISSUES; do
|
for ISSUE in $TARGET_ISSUES; do
|
||||||
export ISSUE PR_URL
|
export ISSUE PR_URL
|
||||||
node --input-type=module - <<'NODE' > /tmp/proposer.issue.close.comment.json
|
|
||||||
const issue = process.env.ISSUE || "";
|
|
||||||
const url = process.env.PR_URL || "";
|
|
||||||
const msg =
|
|
||||||
`PR proposer created for ticket #${issue}: ${url}\n\n` +
|
|
||||||
`The ticket is closed automatically. Discussion can continue in the PR.`;
|
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify({ body: msg }));
|
node --input-type=module -e '
|
||||||
NODE
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const issue = process.env.ISSUE || "";
|
||||||
|
const url = process.env.PR_URL || "";
|
||||||
|
const msg =
|
||||||
|
`PR proposer creee pour le ticket #${issue} : ${url}\n\n` +
|
||||||
|
`Le ticket est cloture automatiquement ; la discussion peut se poursuivre dans la PR.`;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
"/tmp/proposer.issue.close.comment.json",
|
||||||
|
JSON.stringify({ body: msg })
|
||||||
|
);
|
||||||
|
'
|
||||||
|
|
||||||
curl -fsS -X POST \
|
curl -fsS -X POST \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
@@ -626,6 +753,17 @@ jobs:
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" \
|
||||||
--data-binary '{"state":"closed"}'
|
--data-binary '{"state":"closed"}'
|
||||||
|
|
||||||
|
ISSUE_STATE="$(curl -fsS \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE" | \
|
||||||
|
node --input-type=module -e 'let s=""; process.stdin.on("data", d => s += d); process.stdin.on("end", () => { const j = JSON.parse(s || "{}"); process.stdout.write(String(j.state || "")); });')"
|
||||||
|
|
||||||
|
[[ "$ISSUE_STATE" == "closed" ]] || {
|
||||||
|
echo "Issue #$ISSUE is still not closed after PATCH"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "PR: $PR_URL"
|
echo "PR: $PR_URL"
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { spawnSync } from "node:child_process";
|
|||||||
*
|
*
|
||||||
* Conçu pour:
|
* Conçu pour:
|
||||||
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
|
* - prendre un ticket [Correction]/[Fact-check] (issue) avec Chemin + Ancre + Proposition
|
||||||
* - retrouver le bon paragraphe dans le .mdx
|
* - retrouver le bon paragraphe dans le .mdx/.md
|
||||||
* - remplacer proprement
|
* - remplacer proprement
|
||||||
|
* - ne JAMAIS toucher au frontmatter
|
||||||
* - optionnel: écrire un alias d’ancre old->new (build-time) dans src/anchors/anchor-aliases.json
|
* - optionnel: écrire un alias d’ancre old->new (build-time) dans src/anchors/anchor-aliases.json
|
||||||
* - optionnel: committer automatiquement
|
* - optionnel: committer automatiquement
|
||||||
* - optionnel: fermer le ticket (après commit)
|
* - optionnel: fermer le ticket (après commit)
|
||||||
@@ -137,28 +138,17 @@ function scoreText(candidate, targetText) {
|
|||||||
let hit = 0;
|
let hit = 0;
|
||||||
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
||||||
|
|
||||||
// Bonus si un long préfixe ressemble
|
|
||||||
const tgtNorm = normalizeText(stripMd(targetText));
|
const tgtNorm = normalizeText(stripMd(targetText));
|
||||||
const blkNorm = normalizeText(stripMd(candidate));
|
const blkNorm = normalizeText(stripMd(candidate));
|
||||||
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
||||||
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
||||||
|
|
||||||
// Ratio bonus (0..100)
|
|
||||||
const ratio = hit / Math.max(1, tgtSet.size);
|
const ratio = hit / Math.max(1, tgtSet.size);
|
||||||
const ratioBonus = Math.round(ratio * 100);
|
const ratioBonus = Math.round(ratio * 100);
|
||||||
|
|
||||||
return prefixBonus + hit + ratioBonus;
|
return prefixBonus + hit + ratioBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bestBlockMatchIndex(blocks, targetText) {
|
|
||||||
let best = { i: -1, score: -1 };
|
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
|
||||||
const sc = scoreText(blocks[i], targetText);
|
|
||||||
if (sc > best.score) best = { i, score: sc };
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rankedBlockMatches(blocks, targetText, limit = 5) {
|
function rankedBlockMatches(blocks, targetText, limit = 5) {
|
||||||
return blocks
|
return blocks
|
||||||
.map((b, i) => ({
|
.map((b, i) => ({
|
||||||
@@ -170,11 +160,6 @@ function rankedBlockMatches(blocks, targetText, limit = 5) {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitParagraphBlocks(mdxText) {
|
|
||||||
const raw = String(mdxText ?? "").replace(/\r\n/g, "\n");
|
|
||||||
return raw.split(/\n{2,}/);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLikelyExcerpt(s) {
|
function isLikelyExcerpt(s) {
|
||||||
const t = String(s || "").trim();
|
const t = String(s || "").trim();
|
||||||
if (!t) return true;
|
if (!t) return true;
|
||||||
@@ -184,6 +169,89 @@ function isLikelyExcerpt(s) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------- frontmatter / structure ------------------------ */
|
||||||
|
|
||||||
|
function normalizeNewlines(s) {
|
||||||
|
return String(s ?? "").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMdxFrontmatter(src) {
|
||||||
|
const text = normalizeNewlines(src);
|
||||||
|
const m = text.match(/^---\n[\s\S]*?\n---\n?/);
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
return {
|
||||||
|
hasFrontmatter: false,
|
||||||
|
frontmatter: "",
|
||||||
|
body: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter = m[0];
|
||||||
|
const body = text.slice(frontmatter.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasFrontmatter: true,
|
||||||
|
frontmatter,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinMdxFrontmatter(frontmatter, body) {
|
||||||
|
if (!frontmatter) return String(body ?? "");
|
||||||
|
return String(frontmatter) + String(body ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFrontmatterIntegrity({ hadFrontmatter, originalFrontmatter, finalText, filePath }) {
|
||||||
|
if (!hadFrontmatter) return;
|
||||||
|
|
||||||
|
const text = normalizeNewlines(finalText);
|
||||||
|
|
||||||
|
if (!text.startsWith("---\n")) {
|
||||||
|
throw new Error(`Frontmatter perdu pendant la mise à jour de ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text.startsWith(originalFrontmatter)) {
|
||||||
|
throw new Error(`Frontmatter altéré pendant la mise à jour de ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitParagraphBlocksPreserve(bodyText) {
|
||||||
|
const text = normalizeNewlines(bodyText);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return { blocks: [], separators: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = [];
|
||||||
|
const separators = [];
|
||||||
|
|
||||||
|
const re = /(\n{2,})/g;
|
||||||
|
let last = 0;
|
||||||
|
let m;
|
||||||
|
|
||||||
|
while ((m = re.exec(text))) {
|
||||||
|
blocks.push(text.slice(last, m.index));
|
||||||
|
separators.push(m[1]);
|
||||||
|
last = m.index + m[1].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(text.slice(last));
|
||||||
|
|
||||||
|
return { blocks, separators };
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinParagraphBlocksPreserve(blocks, separators) {
|
||||||
|
if (!Array.isArray(blocks) || blocks.length === 0) return "";
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
out += blocks[i];
|
||||||
|
if (i < separators.length) out += separators[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------ utils système ------------------------------ */
|
/* ------------------------------ utils système ------------------------------ */
|
||||||
|
|
||||||
function run(cmd, args, opts = {}) {
|
function run(cmd, args, opts = {}) {
|
||||||
@@ -263,7 +331,9 @@ function pickSection(body, markers) {
|
|||||||
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
|
.map((m) => ({ m, i: text.toLowerCase().indexOf(m.toLowerCase()) }))
|
||||||
.filter((x) => x.i >= 0)
|
.filter((x) => x.i >= 0)
|
||||||
.sort((a, b) => a.i - b.i)[0];
|
.sort((a, b) => a.i - b.i)[0];
|
||||||
|
|
||||||
if (!idx) return "";
|
if (!idx) return "";
|
||||||
|
|
||||||
const start = idx.i + idx.m.length;
|
const start = idx.i + idx.m.length;
|
||||||
const tail = text.slice(start);
|
const tail = text.slice(start);
|
||||||
|
|
||||||
@@ -278,11 +348,13 @@ function pickSection(body, markers) {
|
|||||||
"\n## Proposition",
|
"\n## Proposition",
|
||||||
"\n## Problème",
|
"\n## Problème",
|
||||||
];
|
];
|
||||||
|
|
||||||
let end = tail.length;
|
let end = tail.length;
|
||||||
for (const s of stops) {
|
for (const s of stops) {
|
||||||
const j = tail.toLowerCase().indexOf(s.toLowerCase());
|
const j = tail.toLowerCase().indexOf(s.toLowerCase());
|
||||||
if (j >= 0 && j < end) end = j;
|
if (j >= 0 && j < end) end = j;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tail.slice(0, end).trim();
|
return tail.slice(0, end).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,8 +382,6 @@ function extractAnchorIdAnywhere(text) {
|
|||||||
|
|
||||||
function extractCheminFromAnyUrl(text) {
|
function extractCheminFromAnyUrl(text) {
|
||||||
const s = String(text || "");
|
const s = String(text || "");
|
||||||
// Exemple: http://localhost:4321/archicratie/prologue/#p-3-xxxx
|
|
||||||
// ou: /archicratie/prologue/#p-3-xxxx
|
|
||||||
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
|
const m = s.match(/(\/[a-z0-9\-]+\/[a-z0-9\-\/]+\/)#p-\d+-[0-9a-f]{8}/i);
|
||||||
return m ? m[1] : "";
|
return m ? m[1] : "";
|
||||||
}
|
}
|
||||||
@@ -412,7 +482,7 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -428,7 +498,7 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
|||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": "archicratie-apply-ticket/2.0",
|
"User-Agent": "archicratie-apply-ticket/2.1",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (comment) {
|
if (comment) {
|
||||||
@@ -437,7 +507,11 @@ async function closeIssue({ forgeApiBase, owner, repo, token, issueNum, comment
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
|
const url = `${base}/api/v1/repos/${owner}/${repo}/issues/${issueNum}`;
|
||||||
const res = await fetch(url, { method: "PATCH", headers, body: JSON.stringify({ state: "closed" }) });
|
const res = await fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ state: "closed" }),
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const t = await res.text().catch(() => "");
|
const t = await res.text().catch(() => "");
|
||||||
@@ -541,10 +615,9 @@ async function main() {
|
|||||||
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
|
console.log(`🔎 Fetch ticket #${issueNum} from ${owner}/${repo} …`);
|
||||||
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
|
const issue = await fetchIssue({ forgeApiBase, owner, repo, token, issueNum });
|
||||||
|
|
||||||
// Guard PR (Pull Request = "Demande d'ajout" = pas un ticket éditorial)
|
|
||||||
if (issue?.pull_request) {
|
if (issue?.pull_request) {
|
||||||
console.error(`❌ #${issueNum} est une Pull Request (demande d’ajout), pas un ticket éditorial.`);
|
console.error(`❌ #${issueNum} est une Pull Request (demande d’ajout), pas un ticket éditorial.`);
|
||||||
console.error(`➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.`);
|
console.error("➡️ Ouvre un ticket [Correction]/[Fact-check] depuis le site (Proposer), puis relance apply-ticket sur ce numéro.");
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +638,6 @@ async function main() {
|
|||||||
ancre = (ancre || "").trim();
|
ancre = (ancre || "").trim();
|
||||||
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
||||||
|
|
||||||
// fallback si ticket mal formé
|
|
||||||
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
|
if (!ancre) ancre = extractAnchorIdAnywhere(title) || extractAnchorIdAnywhere(body);
|
||||||
|
|
||||||
chemin = normalizeChemin(chemin);
|
chemin = normalizeChemin(chemin);
|
||||||
@@ -604,7 +676,6 @@ async function main() {
|
|||||||
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
|
const distHtmlPath = path.join(DIST_ROOT, chemin.replace(/^\/+|\/+$/g, ""), "index.html");
|
||||||
await ensureBuildIfNeeded(distHtmlPath);
|
await ensureBuildIfNeeded(distHtmlPath);
|
||||||
|
|
||||||
// Texte cible: préférence au texte complet (ticket), sinon dist si extrait probable
|
|
||||||
let targetText = texteActuel;
|
let targetText = texteActuel;
|
||||||
let distText = "";
|
let distText = "";
|
||||||
|
|
||||||
@@ -621,18 +692,24 @@ async function main() {
|
|||||||
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
|
throw new Error("Impossible de reconstruire le texte du paragraphe (ni texte actuel, ni dist html).");
|
||||||
}
|
}
|
||||||
|
|
||||||
const original = await fs.readFile(contentFile, "utf-8");
|
const originalRaw = await fs.readFile(contentFile, "utf-8");
|
||||||
const blocks = splitParagraphBlocks(original);
|
const { hasFrontmatter, frontmatter, body: originalBody } = splitMdxFrontmatter(originalRaw);
|
||||||
|
|
||||||
|
const split = splitParagraphBlocksPreserve(originalBody);
|
||||||
|
const blocks = split.blocks;
|
||||||
|
const separators = split.separators;
|
||||||
|
|
||||||
|
if (!blocks.length) {
|
||||||
|
throw new Error(`Aucun bloc éditorial exploitable dans ${path.relative(CWD, contentFile)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const ranked = rankedBlockMatches(blocks, targetText, 5);
|
const ranked = rankedBlockMatches(blocks, targetText, 5);
|
||||||
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
|
const best = ranked[0] || { i: -1, score: -1, excerpt: "" };
|
||||||
const runnerUp = ranked[1] || null;
|
const runnerUp = ranked[1] || null;
|
||||||
|
|
||||||
// seuil absolu
|
|
||||||
if (best.i < 0 || best.score < 40) {
|
if (best.i < 0 || best.score < 40) {
|
||||||
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
||||||
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
||||||
|
|
||||||
console.error("Top candidates:");
|
console.error("Top candidates:");
|
||||||
for (const r of ranked) {
|
for (const r of ranked) {
|
||||||
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
|
console.error(` #${r.i + 1} score=${r.score} ${r.excerpt}${r.excerpt.length >= 140 ? "…" : ""}`);
|
||||||
@@ -640,7 +717,6 @@ async function main() {
|
|||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// seuil relatif : si le 2e est trop proche du 1er, on refuse aussi
|
|
||||||
if (runnerUp) {
|
if (runnerUp) {
|
||||||
const ambiguityGap = best.score - runnerUp.score;
|
const ambiguityGap = best.score - runnerUp.score;
|
||||||
if (ambiguityGap < 15) {
|
if (ambiguityGap < 15) {
|
||||||
@@ -659,7 +735,16 @@ async function main() {
|
|||||||
|
|
||||||
const nextBlocks = blocks.slice();
|
const nextBlocks = blocks.slice();
|
||||||
nextBlocks[best.i] = afterBlock;
|
nextBlocks[best.i] = afterBlock;
|
||||||
const updated = nextBlocks.join("\n\n");
|
|
||||||
|
const updatedBody = joinParagraphBlocksPreserve(nextBlocks, separators);
|
||||||
|
const updatedRaw = joinMdxFrontmatter(frontmatter, updatedBody);
|
||||||
|
|
||||||
|
assertFrontmatterIntegrity({
|
||||||
|
hadFrontmatter: hasFrontmatter,
|
||||||
|
originalFrontmatter: frontmatter,
|
||||||
|
finalText: updatedRaw,
|
||||||
|
filePath: path.relative(CWD, contentFile),
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
|
console.log(`🧩 Matched block #${best.i + 1}/${blocks.length} score=${best.score}`);
|
||||||
|
|
||||||
@@ -673,16 +758,15 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// backup uniquement si on écrit
|
|
||||||
const relContentFile = path.relative(CWD, contentFile);
|
const relContentFile = path.relative(CWD, contentFile);
|
||||||
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
|
const bakPath = path.join(BACKUP_ROOT, `${relContentFile}.bak.issue-${issueNum}`);
|
||||||
await fs.mkdir(path.dirname(bakPath), { recursive: true });
|
await fs.mkdir(path.dirname(bakPath), { recursive: true });
|
||||||
|
|
||||||
if (!(await fileExists(bakPath))) {
|
if (!(await fileExists(bakPath))) {
|
||||||
await fs.writeFile(bakPath, original, "utf-8");
|
await fs.writeFile(bakPath, originalRaw, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(contentFile, updated, "utf-8");
|
await fs.writeFile(contentFile, updatedRaw, "utf-8");
|
||||||
console.log("✅ Applied.");
|
console.log("✅ Applied.");
|
||||||
|
|
||||||
let aliasChanged = false;
|
let aliasChanged = false;
|
||||||
@@ -703,13 +787,11 @@ async function main() {
|
|||||||
|
|
||||||
if (aliasChanged) {
|
if (aliasChanged) {
|
||||||
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
||||||
// MàJ dist sans rebuild complet (inject seulement)
|
|
||||||
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
||||||
} else {
|
} else {
|
||||||
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// garde-fous rapides
|
|
||||||
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
|
run("node", ["scripts/check-anchor-aliases.mjs"], { cwd: CWD });
|
||||||
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
|
run("node", ["scripts/verify-anchor-aliases-in-dist.mjs"], { cwd: CWD });
|
||||||
run("npm", ["run", "test:anchors"], { cwd: CWD });
|
run("npm", ["run", "test:anchors"], { cwd: CWD });
|
||||||
@@ -741,7 +823,6 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mode manuel
|
|
||||||
console.log("Next (manuel) :");
|
console.log("Next (manuel) :");
|
||||||
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
||||||
console.log(
|
console.log(
|
||||||
@@ -758,4 +839,4 @@ async function main() {
|
|||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
console.error("💥", e?.message || e);
|
console.error("💥", e?.message || e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@@ -1 +1,5 @@
|
|||||||
{}
|
{
|
||||||
|
"/archicrat-ia/prologue/": {
|
||||||
|
"p-0-d7974f88": "p-0-e729df02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ source:
|
|||||||
kind: docx
|
kind: docx
|
||||||
path: "sources/docx/archicrat-ia/Prologue—Archicratie-fondation_et_finalite_sociopolitique_et_historique-version_officielle.docx"
|
path: "sources/docx/archicrat-ia/Prologue—Archicratie-fondation_et_finalite_sociopolitique_et_historique-version_officielle.docx"
|
||||||
---
|
---
|
||||||
Nous vivons dans une époque saturée de diagnostics sur les formes de domination, les mutations du pouvoir, les détournements de la souveraineté. Depuis une vingtaine d’années, les appellations s’accumulent : *démocratie illibérale*, *ploutocratie*, *happycratie*, *gouvernement algorithmique*, *démocrature*… À travers ces tentatives de nommer le désordre du présent, un fait se répète, de manière sourde : la scène politique semble désorientée. Les catégories héritées — *État*, *pouvoir*, *représentation*, *volonté générale*, *contrat social* — apparaissent de moins en moins capables de décrire ce qui nous gouverne effectivement.
|
Nous vivons une époque saturée de diagnostics sur les formes de domination, les mutations du pouvoir, les détournements de la souveraineté. Depuis une vingtaine d’années, les appellations s’accumulent : démocratie illibérale, ploutocratie, happycratie, gouvernement algorithmique, démocrature… À travers ces tentatives de nommer le désordre du présent, un fait se répète, de manière sourde : la scène politique semble désorientée. Les catégories héritées — État, pouvoir, représentation, volonté générale, contrat social — apparaissent de moins en moins capables de décrire ce qui nous gouverne effectivement.
|
||||||
|
|
||||||
C’est cette perte de prise sur le réel que ce livre souhaite prendre au sérieux. Non pour lui ajouter un terme de plus au lexique fatigué des contre-pouvoirs ou des impuissances, mais pour repartir d’un point plus fondamental, presque en-deçà de la question politique classique. Ce point, c’est celui de la *tenue d’un monde commun* — c’est-à-dire la possibilité, pour des êtres dissemblables, vulnérables, inégaux, traversés de contradictions et situés dans des temporalités hétérogènes, de coexister sans s’annihiler.
|
C’est cette perte de prise sur le réel que ce livre souhaite prendre au sérieux. Non pour lui ajouter un terme de plus au lexique fatigué des contre-pouvoirs ou des impuissances, mais pour repartir d’un point plus fondamental, presque en-deçà de la question politique classique. Ce point, c’est celui de la *tenue d’un monde commun* — c’est-à-dire la possibilité, pour des êtres dissemblables, vulnérables, inégaux, traversés de contradictions et situés dans des temporalités hétérogènes, de coexister sans s’annihiler.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user