Compare commits
18 Commits
chore/depl
...
chore/prop
| Author | SHA1 | Date | |
|---|---|---|---|
| bfbdc7b688 | |||
| 8fd53dd4d2 | |||
|
|
c8bbee4f74 | ||
| 04cdf54eb7 | |||
|
|
d6bf645ae9 | ||
| 1ca6bcbd81 | |||
| dec5f8eba7 | |||
| 716c887045 | |||
| 9b1789a164 | |||
| 17fa39c7ff | |||
| 8132e315f4 | |||
| 8d993915d7 | |||
| 497bddd05d | |||
| 7c8e49c1a9 | |||
| 901d28b89b | |||
| 43e2862c89 | |||
| 73fb38c4d1 | |||
| 9801ea3cea |
@@ -17,7 +17,7 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
concurrency:
|
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
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Derive context (event.json / workflow_dispatch)
|
- name: Derive context (event.json / workflow_dispatch)
|
||||||
env:
|
env:
|
||||||
INPUT_ISSUE: ${{ inputs.issue }}
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||||
@@ -81,10 +81,12 @@ jobs:
|
|||||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelName =
|
// label name: best-effort (non-bloquant)
|
||||||
ev?.label?.name ||
|
let labelName = "workflow_dispatch";
|
||||||
ev?.label ||
|
const lab = ev?.label;
|
||||||
"workflow_dispatch";
|
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 u = new URL(cloneUrl);
|
||||||
const origin = u.origin;
|
const origin = u.origin;
|
||||||
@@ -109,19 +111,25 @@ jobs:
|
|||||||
echo "✅ context:"
|
echo "✅ context:"
|
||||||
sed -n '1,120p' /tmp/anno.env
|
sed -n '1,120p' /tmp/anno.env
|
||||||
|
|
||||||
- name: Gate on label state/approved
|
- name: Early gate (label event fast-skip, but tolerant)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env
|
source /tmp/anno.env
|
||||||
|
|
||||||
if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
echo "ℹ️ event label = $LABEL_NAME"
|
||||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
|
||||||
|
# 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=1" >> /tmp/anno.env
|
||||||
|
echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
|
||||||
|
|
||||||
- name: Fetch issue + gate on Type (skip Proposer)
|
echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"
|
||||||
|
|
||||||
|
- name: Fetch issue + hard gate on labels + Type
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -131,16 +139,22 @@ jobs:
|
|||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
ISSUE_JSON="$(curl -fsS \
|
curl -fsS \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")"
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||||
|
-o /tmp/issue.json
|
||||||
|
|
||||||
node --input-type=module - <<'NODE' "$ISSUE_JSON" >> /tmp/anno.env
|
node --input-type=module - <<'NODE' >> /tmp/anno.env
|
||||||
const issue = JSON.parse(process.argv[1] || "{}");
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||||
const title = String(issue.title || "");
|
const title = String(issue.title || "");
|
||||||
const body = String(issue.body || "").replace(/\r\n/g, "\n");
|
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) {
|
function pickLine(key) {
|
||||||
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
|
||||||
const m = body.match(re);
|
const m = body.match(re);
|
||||||
@@ -157,6 +171,14 @@ jobs:
|
|||||||
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
|
||||||
out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);
|
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) {
|
if (!type) {
|
||||||
out.push(`SKIP=1`);
|
out.push(`SKIP=1`);
|
||||||
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
|
||||||
@@ -173,7 +195,7 @@ jobs:
|
|||||||
process.stdout.write(out.join("\n") + "\n");
|
process.stdout.write(out.join("\n") + "\n");
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
echo "✅ issue type gating:"
|
echo "✅ gating result:"
|
||||||
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
|
grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true
|
||||||
|
|
||||||
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
|
- name: Comment issue if skipped (Proposer / unsupported / missing Type)
|
||||||
@@ -185,19 +207,24 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
|
|
||||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||||
[[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0
|
|
||||||
|
|
||||||
# message différent si Proposer
|
# IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
|
||||||
|
if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
|
||||||
|
echo "ℹ️ skip reason=${SKIP_REASON} -> no comment"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||||
|
|
||||||
REASON="${SKIP_REASON:-}"
|
REASON="${SKIP_REASON:-}"
|
||||||
TYPE="${ISSUE_TYPE:-}"
|
TYPE="${ISSUE_TYPE:-}"
|
||||||
TITLE="${ISSUE_TITLE:-}"
|
|
||||||
|
|
||||||
if [[ "$REASON" == proposer_type:* ]]; then
|
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 et n'ajoute **jamais** state/approved automatiquement.\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
|
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
|
else
|
||||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\n✅ Action : corriger le ticket (ajouter '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
|
fi
|
||||||
|
|
||||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
@@ -264,6 +291,7 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${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; }
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
@@ -320,6 +348,8 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||||
|
|
||||||
if [[ -f /tmp/apply.log ]]; then
|
if [[ -f /tmp/apply.log ]]; then
|
||||||
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
|
BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
|
||||||
else
|
else
|
||||||
@@ -335,27 +365,6 @@ jobs:
|
|||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||||
--data-binary "$PAYLOAD"
|
--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:-0}" == "0" ]] || exit 0
|
|
||||||
[[ "${NOOP:-0}" == "1" ]] || exit 0
|
|
||||||
|
|
||||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
|
|
||||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
|
||||||
|
|
||||||
curl -fsS -X POST \
|
|
||||||
-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: Push bot branch
|
- name: Push bot branch
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
@@ -367,6 +376,7 @@ jobs:
|
|||||||
|
|
||||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> 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; }
|
[[ "${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 '
|
AUTH_URL="$(node --input-type=module -e '
|
||||||
const [clone, tok] = process.argv.slice(1);
|
const [clone, tok] = process.argv.slice(1);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
concurrency:
|
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
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Derive context (event.json / workflow_dispatch)
|
- name: Derive context (event.json / workflow_dispatch)
|
||||||
env:
|
env:
|
||||||
INPUT_ISSUE: ${{ inputs.issue }}
|
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||||
@@ -75,10 +75,11 @@ jobs:
|
|||||||
throw new Error("No issue number in event.json or workflow_dispatch input");
|
throw new Error("No issue number in event.json or workflow_dispatch input");
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelName =
|
// label name: best-effort (non-bloquant)
|
||||||
ev?.label?.name ||
|
let labelName = "workflow_dispatch";
|
||||||
ev?.label ||
|
const lab = ev?.label;
|
||||||
"workflow_dispatch";
|
if (typeof lab === "string") labelName = lab;
|
||||||
|
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
|
||||||
|
|
||||||
let apiBase = "";
|
let apiBase = "";
|
||||||
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
|
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
|
||||||
@@ -103,19 +104,20 @@ jobs:
|
|||||||
echo "✅ context:"
|
echo "✅ context:"
|
||||||
sed -n '1,120p' /tmp/reject.env
|
sed -n '1,120p' /tmp/reject.env
|
||||||
|
|
||||||
- name: Gate on label state/rejected only
|
- name: Early gate (fast-skip, tolerant)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/reject.env
|
source /tmp/reject.env
|
||||||
|
echo "ℹ️ event label = $LABEL_NAME"
|
||||||
|
|
||||||
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
|
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
|
||||||
echo "ℹ️ label=$LABEL_NAME => skip"
|
echo "ℹ️ label=$LABEL_NAME => skip early"
|
||||||
echo "SKIP=1" >> /tmp/reject.env
|
echo "SKIP=1" >> /tmp/reject.env
|
||||||
|
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "✅ proceed (issue=$ISSUE_NUMBER)"
|
|
||||||
|
|
||||||
- name: Comment + close (only if not conflicting with state/approved)
|
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -126,14 +128,15 @@ jobs:
|
|||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
|
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
|
||||||
|
|
||||||
ISSUE_JSON="$(curl -fsS \
|
curl -fsS \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")"
|
"$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 - <<'NODE' > /tmp/reject.flags
|
||||||
node --input-type=module - <<'NODE' "$ISSUE_JSON" > /tmp/reject.flags
|
import fs from "node:fs";
|
||||||
const issue = JSON.parse(process.argv[1] || "{}");
|
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 labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
|
||||||
const hasApproved = labels.includes("state/approved");
|
const hasApproved = labels.includes("state/approved");
|
||||||
const hasRejected = labels.includes("state/rejected");
|
const hasRejected = labels.includes("state/rejected");
|
||||||
@@ -142,6 +145,12 @@ jobs:
|
|||||||
|
|
||||||
source /tmp/reject.flags
|
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
|
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."
|
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")"
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
@@ -154,7 +163,6 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# comment reject
|
|
||||||
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
|
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
|
||||||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
|
|
||||||
@@ -164,7 +172,6 @@ jobs:
|
|||||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||||
--data-binary "$PAYLOAD"
|
--data-binary "$PAYLOAD"
|
||||||
|
|
||||||
# close issue
|
|
||||||
curl -fsS -X PATCH \
|
curl -fsS -X PATCH \
|
||||||
-H "Authorization: token $FORGE_TOKEN" \
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -4,22 +4,37 @@ on:
|
|||||||
issues:
|
issues:
|
||||||
types: [opened, edited]
|
types: [opened, edited]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
runs-on: mac-ci
|
runs-on: mac-ci
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Apply labels from Type/State/Category
|
- name: Apply labels from Type/State/Category
|
||||||
env:
|
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 }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
REPO_FULL: ${{ gitea.repository }}
|
REPO_FULL: ${{ gitea.repository }}
|
||||||
EVENT_PATH: ${{ github.event_path }}
|
EVENT_PATH: ${{ github.event_path }}
|
||||||
|
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||||
run: |
|
run: |
|
||||||
python3 - <<'PY'
|
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)
|
owner, repo = os.environ["REPO_FULL"].split("/", 1)
|
||||||
event_path = os.environ["EVENT_PATH"]
|
event_path = os.environ["EVENT_PATH"]
|
||||||
|
|
||||||
@@ -46,12 +61,9 @@ jobs:
|
|||||||
print("PARSED:", {"Type": t, "State": s, "Category": c})
|
print("PARSED:", {"Type": t, "State": s, "Category": c})
|
||||||
|
|
||||||
# 1) explicite depuis le body
|
# 1) explicite depuis le body
|
||||||
if t:
|
if t: desired.add(t)
|
||||||
desired.add(t)
|
if s: desired.add(s)
|
||||||
if s:
|
if c: desired.add(c)
|
||||||
desired.add(s)
|
|
||||||
if c:
|
|
||||||
desired.add(c)
|
|
||||||
|
|
||||||
# 2) fallback depuis le titre si Type absent
|
# 2) fallback depuis le titre si Type absent
|
||||||
if not t:
|
if not t:
|
||||||
@@ -76,42 +88,56 @@ jobs:
|
|||||||
"Authorization": f"token {token}",
|
"Authorization": f"token {token}",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Content-Type": "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")
|
data = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
last_err = None
|
||||||
try:
|
for i in range(retries):
|
||||||
with urllib.request.urlopen(req, timeout=20) as r:
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
b = r.read()
|
try:
|
||||||
return json.loads(b.decode("utf-8")) if b else None
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
except urllib.error.HTTPError as e:
|
b = r.read()
|
||||||
b = e.read().decode("utf-8", errors="replace")
|
return json.loads(b.decode("utf-8")) if b else None
|
||||||
raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
|
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 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}
|
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]
|
missing = [x for x in desired if x not in name_to_id]
|
||||||
if missing:
|
if missing:
|
||||||
raise SystemExit("Missing labels in repo: " + ", ".join(sorted(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
|
# labels actuels de l'issue
|
||||||
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels") or []
|
current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
|
||||||
current_ids = {x.get("id") for x in current if x.get("id") is not None}
|
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))
|
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"
|
url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"
|
||||||
try:
|
|
||||||
jreq("PUT", url, {"labels": final_ids})
|
# IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
|
||||||
except Exception:
|
jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)
|
||||||
jreq("PUT", url, final_ids)
|
|
||||||
|
# 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)}")
|
print(f"OK labels #{number}: {sorted(desired)}")
|
||||||
PY
|
PY
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: nas-deploy
|
runs-on: nas-deploy
|
||||||
container:
|
container:
|
||||||
image: localhost:5000/archicratie/nas-deploy-node22:2026-02-28-1
|
image: localhost:5000/archicratie/nas-deploy-node22@sha256:fefa8bb307005cebec07796661ab25528dc319c33a8f1e480e1d66f90cd5cff6
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Tools sanity
|
- name: Tools sanity
|
||||||
|
|||||||
408
.gitea/workflows/proposer-apply-pr.yml
Normal file
408
.gitea/workflows/proposer-apply-pr.yml
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
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
|
||||||
|
python3 --version || true
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Install deps (site/)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
cd site
|
||||||
|
npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
- name: Build dist baseline (site/)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
cd site
|
||||||
|
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"
|
||||||
|
|
||||||
|
LOG="/tmp/proposer-apply.log"
|
||||||
|
set +e
|
||||||
|
(cd site && 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: Comment issue on failure
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/proposer.env || true
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
|
||||||
|
RC="${APPLY_RC:-0}"
|
||||||
|
if [[ "$RC" == "0" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY="(no log)"
|
||||||
|
[[ -f /tmp/proposer-apply.log ]] && BODY="$(tail -n 160 /tmp/proposer-apply.log | sed 's/\r$//')"
|
||||||
|
|
||||||
|
MSG="❌ Proposer Apply échoué (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
|
||||||
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
|
|
||||||
|
curl -fsS -X POST \
|
||||||
|
-H "Authorization: token $FORGE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||||
|
--data-binary "$PAYLOAD"
|
||||||
|
|
||||||
|
- name: Comment issue if no-op
|
||||||
|
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}" == "1" ]] || exit 0
|
||||||
|
|
||||||
|
MSG="ℹ️ Proposer Apply: rien à appliquer (déjà présent / dédupliqué)."
|
||||||
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
|
|
||||||
|
curl -fsS -X POST \
|
||||||
|
-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: 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; }
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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}\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"
|
||||||
@@ -10,3 +10,9 @@ paras:
|
|||||||
credit: ""
|
credit: ""
|
||||||
ts: 2026-02-27T12:43:14.259Z
|
ts: 2026-02-27T12:43:14.259Z
|
||||||
fromIssue: 144
|
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
|
||||||
|
|||||||
10
src/annotations/archicrat-ia/chapitre-3/p-1-60c7ea48.yml
Normal file
10
src/annotations/archicrat-ia/chapitre-3/p-1-60c7ea48.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
Reference in New Issue
Block a user