ci: fix anno apply/reject JSON parsing + hard gates
This commit is contained in:
@@ -22,13 +22,6 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
apply-approved:
|
apply-approved:
|
||||||
# ✅ Job ne se lance QUE si le label ajouté est state/approved, ou si workflow_dispatch
|
|
||||||
if: >-
|
|
||||||
${{
|
|
||||||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'state/approved')
|
|
||||||
|| github.event_name == 'workflow_dispatch'
|
|
||||||
}}
|
|
||||||
|
|
||||||
runs-on: mac-ci
|
runs-on: mac-ci
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||||
@@ -42,7 +35,6 @@ jobs:
|
|||||||
npm --version
|
npm --version
|
||||||
|
|
||||||
- name: Derive context (event.json / workflow_dispatch)
|
- name: Derive context (event.json / workflow_dispatch)
|
||||||
id: ctx
|
|
||||||
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 }}
|
||||||
@@ -60,18 +52,15 @@ jobs:
|
|||||||
const cloneUrl =
|
const cloneUrl =
|
||||||
repoObj?.clone_url ||
|
repoObj?.clone_url ||
|
||||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||||
|
|
||||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||||
|
|
||||||
let owner =
|
let owner =
|
||||||
repoObj?.owner?.login ||
|
repoObj?.owner?.login ||
|
||||||
repoObj?.owner?.username ||
|
repoObj?.owner?.username ||
|
||||||
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
(repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");
|
||||||
|
|
||||||
let repo =
|
let repo =
|
||||||
repoObj?.name ||
|
repoObj?.name ||
|
||||||
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
(repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");
|
||||||
|
|
||||||
if (!owner || !repo) {
|
if (!owner || !repo) {
|
||||||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||||||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||||||
@@ -84,17 +73,15 @@ jobs:
|
|||||||
ev?.issue?.number ||
|
ev?.issue?.number ||
|
||||||
ev?.issue?.index ||
|
ev?.issue?.index ||
|
||||||
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
(process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);
|
||||||
|
|
||||||
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ici, vu job-level if, on sait déjà que c'est state/approved en issues:labeled.
|
// label triggered (best effort; may be missing depending on Gitea payload)
|
||||||
// Mais on garde pour logs / workflow_dispatch.
|
|
||||||
const labelName =
|
const labelName =
|
||||||
ev?.label?.name ||
|
ev?.label?.name ||
|
||||||
ev?.label ||
|
(typeof ev?.label === "string" ? ev.label : "") ||
|
||||||
"workflow_dispatch";
|
"";
|
||||||
|
|
||||||
const u = new URL(cloneUrl);
|
const u = new URL(cloneUrl);
|
||||||
const origin = u.origin;
|
const origin = u.origin;
|
||||||
@@ -103,7 +90,7 @@ jobs:
|
|||||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||||
: origin;
|
: origin;
|
||||||
|
|
||||||
function sh(s){ return JSON.stringify(String(s)); }
|
function sh(s){ return JSON.stringify(String(s ?? "")); }
|
||||||
|
|
||||||
process.stdout.write([
|
process.stdout.write([
|
||||||
`CLONE_URL=${sh(cloneUrl)}`,
|
`CLONE_URL=${sh(cloneUrl)}`,
|
||||||
@@ -113,37 +100,73 @@ jobs:
|
|||||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||||
`LABEL_NAME=${sh(labelName)}`,
|
`LABEL_NAME=${sh(labelName)}`,
|
||||||
`API_BASE=${sh(apiBase)}`,
|
`API_BASE=${sh(apiBase)}`,
|
||||||
`SKIP=0`
|
|
||||||
|
// init safe defaults (avoid "unbound variable" cascades)
|
||||||
|
`SKIP=0`,
|
||||||
|
`SKIP_REASON=${sh("")}`,
|
||||||
|
`ISSUE_TYPE=${sh("")}`,
|
||||||
|
`ISSUE_TITLE=${sh("")}`,
|
||||||
|
`APPLY_RC=${sh("")}`,
|
||||||
|
`NOOP=0`,
|
||||||
|
`BRANCH=${sh("")}`,
|
||||||
|
`END_SHA=${sh("")}`
|
||||||
].join("\n") + "\n");
|
].join("\n") + "\n");
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
echo "✅ context:"
|
echo "✅ context:"
|
||||||
sed -n '1,160p' /tmp/anno.env
|
sed -n '1,160p' /tmp/anno.env
|
||||||
|
|
||||||
- name: Fetch issue + gate on Type (skip Proposer)
|
- name: Gate fast (only if label is state/approved or workflow_dispatch)
|
||||||
id: typegate
|
env:
|
||||||
|
INPUT_ISSUE: ${{ inputs.issue }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/anno.env
|
||||||
|
|
||||||
|
# workflow_dispatch => allow
|
||||||
|
if [[ -n "${INPUT_ISSUE:-}" ]]; then
|
||||||
|
echo "✅ workflow_dispatch => proceed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if payload provides the triggering label, we can skip without API call
|
||||||
|
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/approved" ]]; then
|
||||||
|
echo "ℹ️ triggering label='$LABEL_NAME' (not state/approved) => skip"
|
||||||
|
echo "SKIP=1" >> /tmp/anno.env
|
||||||
|
echo "SKIP_REASON=\"trigger_label_not_approved\"" >> /tmp/anno.env
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ℹ️ label unknown or approved => continue to API gating"
|
||||||
|
|
||||||
|
- name: Fetch issue + gate on state/approved + gate on Type (skip Proposer)
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env
|
source /tmp/anno.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
|
|
||||||
# ✅ récupérer issue JSON dans un fichier (zéro JSON dans argv)
|
|
||||||
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
|
> /tmp/issue.json
|
||||||
|
|
||||||
node --input-type=module <<'NODE' >> /tmp/anno.env
|
node --input-type=module - <<'NODE' >> /tmp/anno.env
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
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);
|
||||||
@@ -160,7 +183,11 @@ 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)}`);
|
||||||
|
|
||||||
if (!type) {
|
// main gate: only act if state/approved is actually present on the issue
|
||||||
|
if (!hasApproved) {
|
||||||
|
out.push(`SKIP=1`);
|
||||||
|
out.push(`SKIP_REASON=${JSON.stringify("no_state_approved")}`);
|
||||||
|
} else 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")}`);
|
||||||
} else if (allowed.has(type)) {
|
} else if (allowed.has(type)) {
|
||||||
@@ -176,10 +203,10 @@ jobs:
|
|||||||
process.stdout.write(out.join("\n") + "\n");
|
process.stdout.write(out.join("\n") + "\n");
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
echo "✅ issue type gating:"
|
echo "✅ issue gating:"
|
||||||
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 (only when state/approved was present)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
@@ -188,7 +215,9 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
|
|
||||||
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
[[ "${SKIP:-0}" == "1" ]] || exit 0
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
[[ "${SKIP_REASON:-}" != "no_state_approved" ]] || exit 0 # do not comment on normal label churn
|
||||||
|
|
||||||
|
test -n "${FORGE_TOKEN:-}" || exit 0
|
||||||
|
|
||||||
REASON="${SKIP_REASON:-}"
|
REASON="${SKIP_REASON:-}"
|
||||||
TYPE="${ISSUE_TYPE:-}"
|
TYPE="${ISSUE_TYPE:-}"
|
||||||
@@ -198,7 +227,7 @@ jobs:
|
|||||||
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.\n✅ Action : traitement manuel si nécessaire."
|
||||||
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\n✅ Action : corriger le ticket (Type: type/media|type/reference|type/comment) ou traiter manuellement."
|
||||||
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")"
|
||||||
@@ -210,10 +239,9 @@ jobs:
|
|||||||
--data-binary "$PAYLOAD"
|
--data-binary "$PAYLOAD"
|
||||||
|
|
||||||
- name: Checkout default branch
|
- name: Checkout default branch
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env || true
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
rm -rf .git
|
rm -rf .git
|
||||||
@@ -224,18 +252,16 @@ jobs:
|
|||||||
git log -1 --oneline
|
git log -1 --oneline
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env || true
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
npm ci --no-audit --no-fund
|
npm ci --no-audit --no-fund
|
||||||
|
|
||||||
- name: Check apply script exists
|
- name: Check apply script exists
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env || true
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
test -f scripts/apply-annotation-ticket.mjs || {
|
test -f scripts/apply-annotation-ticket.mjs || {
|
||||||
echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
|
echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
|
||||||
@@ -244,10 +270,9 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Build dist (needed for --verify)
|
- name: Build dist (needed for --verify)
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env || true
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
npm run build
|
npm run build
|
||||||
@@ -260,7 +285,6 @@ jobs:
|
|||||||
echo "✅ dist/para-index.json present"
|
echo "✅ dist/para-index.json present"
|
||||||
|
|
||||||
- name: Apply ticket on bot branch (strict+verify, commit)
|
- name: Apply ticket on bot branch (strict+verify, commit)
|
||||||
if: ${{ always() }}
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
@@ -268,7 +292,7 @@ jobs:
|
|||||||
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
|
BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/anno.env || true
|
source /tmp/anno.env
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||||||
@@ -279,7 +303,7 @@ jobs:
|
|||||||
START_SHA="$(git rev-parse HEAD)"
|
START_SHA="$(git rev-parse HEAD)"
|
||||||
TS="$(date -u +%Y%m%d-%H%M%S)"
|
TS="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
BR="bot/anno-${ISSUE_NUMBER}-${TS}"
|
BR="bot/anno-${ISSUE_NUMBER}-${TS}"
|
||||||
echo "BRANCH=$BR" >> /tmp/anno.env
|
echo "BRANCH=\"$BR\"" >> /tmp/anno.env
|
||||||
git checkout -b "$BR"
|
git checkout -b "$BR"
|
||||||
|
|
||||||
export FORGE_API="$API_BASE"
|
export FORGE_API="$API_BASE"
|
||||||
@@ -292,22 +316,23 @@ jobs:
|
|||||||
RC=$?
|
RC=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "APPLY_RC=$RC" >> /tmp/anno.env
|
echo "APPLY_RC=\"$RC\"" >> /tmp/anno.env
|
||||||
|
|
||||||
echo "== apply log (tail) =="
|
echo "== apply log (tail) =="
|
||||||
tail -n 180 "$LOG" || true
|
tail -n 180 "$LOG" || true
|
||||||
|
|
||||||
|
END_SHA="$(git rev-parse HEAD)"
|
||||||
|
echo "END_SHA=\"$END_SHA\"" >> /tmp/anno.env
|
||||||
|
|
||||||
if [[ "$RC" -ne 0 ]]; then
|
if [[ "$RC" -ne 0 ]]; then
|
||||||
echo "NOOP=0" >> /tmp/anno.env
|
echo "NOOP=0" >> /tmp/anno.env
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
END_SHA="$(git rev-parse HEAD)"
|
|
||||||
if [[ "$START_SHA" == "$END_SHA" ]]; then
|
if [[ "$START_SHA" == "$END_SHA" ]]; then
|
||||||
echo "NOOP=1" >> /tmp/anno.env
|
echo "NOOP=1" >> /tmp/anno.env
|
||||||
else
|
else
|
||||||
echo "NOOP=0" >> /tmp/anno.env
|
echo "NOOP=0" >> /tmp/anno.env
|
||||||
echo "END_SHA=$END_SHA" >> /tmp/anno.env
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Comment issue on failure (strict/verify/etc)
|
- name: Comment issue on failure (strict/verify/etc)
|
||||||
@@ -319,13 +344,9 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
RC="${APPLY_RC:-0}"
|
RC="${APPLY_RC:-}"
|
||||||
if [[ "$RC" == "0" ]]; then
|
[[ -n "$RC" ]] || { echo "ℹ️ apply not executed"; exit 0; }
|
||||||
echo "ℹ️ no failure detected"
|
[[ "$RC" != "0" ]] || { echo "ℹ️ no failure detected"; exit 0; }
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
|
||||||
|
|
||||||
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$//')"
|
||||||
@@ -351,12 +372,10 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
|
||||||
# ✅ si apply a réussi ET NOOP=1 => commenter
|
RC="${APPLY_RC:-}"
|
||||||
[[ "${APPLY_RC:-0}" == "0" ]] || exit 0
|
[[ "$RC" == "0" ]] || exit 0
|
||||||
[[ "${NOOP:-0}" == "1" ]] || exit 0
|
[[ "${NOOP:-0}" == "1" ]] || exit 0
|
||||||
|
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
|
||||||
|
|
||||||
MSG="ℹ️ Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)."
|
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")"
|
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||||||
|
|
||||||
@@ -375,11 +394,9 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
|
||||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
|
[[ "${APPLY_RC:-}" == "0" ]] || { echo "ℹ️ apply not ok -> skip push"; exit 0; }
|
||||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
|
||||||
|
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ missing BRANCH -> skip push"; exit 0; }
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
|
||||||
test -n "${BRANCH:-}" || { echo "❌ BRANCH missing"; exit 1; }
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -401,12 +418,10 @@ jobs:
|
|||||||
source /tmp/anno.env || true
|
source /tmp/anno.env || true
|
||||||
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
[[ "${SKIP:-0}" != "1" ]] || exit 0
|
||||||
|
|
||||||
[[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip PR"; exit 0; }
|
[[ "${APPLY_RC:-}" == "0" ]] || { echo "ℹ️ apply not ok -> skip PR"; exit 0; }
|
||||||
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip PR"; exit 0; }
|
[[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip PR"; exit 0; }
|
||||||
|
[[ -n "${BRANCH:-}" ]] || { echo "ℹ️ missing BRANCH -> skip PR"; exit 0; }
|
||||||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
[[ -n "${END_SHA:-}" ]] || { echo "ℹ️ missing END_SHA -> skip PR"; exit 0; }
|
||||||
test -n "${BRANCH:-}" || { echo "❌ BRANCH missing"; exit 1; }
|
|
||||||
test -n "${END_SHA:-}" || { echo "❌ END_SHA missing"; exit 1; }
|
|
||||||
|
|
||||||
PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
|
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."
|
PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."
|
||||||
@@ -448,7 +463,9 @@ jobs:
|
|||||||
|
|
||||||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
RC="${APPLY_RC:-0}"
|
RC="${APPLY_RC:-}"
|
||||||
|
[[ -n "$RC" ]] || { echo "❌ apply did not run"; exit 2; }
|
||||||
|
|
||||||
if [[ "$RC" != "0" ]]; then
|
if [[ "$RC" != "0" ]]; then
|
||||||
echo "❌ apply failed (rc=$RC)"
|
echo "❌ apply failed (rc=$RC)"
|
||||||
exit "$RC"
|
exit "$RC"
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
reject:
|
reject:
|
||||||
# ✅ Job ne se lance QUE si le label ajouté est state/rejected, ou si workflow_dispatch
|
|
||||||
if: >-
|
|
||||||
${{
|
|
||||||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'state/rejected')
|
|
||||||
|| github.event_name == 'workflow_dispatch'
|
|
||||||
}}
|
|
||||||
|
|
||||||
runs-on: mac-ci
|
runs-on: mac-ci
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||||||
@@ -84,8 +77,8 @@ jobs:
|
|||||||
|
|
||||||
const labelName =
|
const labelName =
|
||||||
ev?.label?.name ||
|
ev?.label?.name ||
|
||||||
ev?.label ||
|
(typeof ev?.label === "string" ? ev.label : "") ||
|
||||||
"workflow_dispatch";
|
"";
|
||||||
|
|
||||||
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()) {
|
||||||
@@ -96,7 +89,7 @@ jobs:
|
|||||||
apiBase = "";
|
apiBase = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sh(s){ return JSON.stringify(String(s)); }
|
function sh(s){ return JSON.stringify(String(s ?? "")); }
|
||||||
|
|
||||||
process.stdout.write([
|
process.stdout.write([
|
||||||
`OWNER=${sh(owner)}`,
|
`OWNER=${sh(owner)}`,
|
||||||
@@ -109,14 +102,35 @@ jobs:
|
|||||||
NODE
|
NODE
|
||||||
|
|
||||||
echo "✅ context:"
|
echo "✅ context:"
|
||||||
sed -n '1,120p' /tmp/reject.env
|
sed -n '1,160p' /tmp/reject.env
|
||||||
|
|
||||||
- name: Comment + close (only if not conflicting with state/approved)
|
- name: Gate fast (only if label is state/rejected or workflow_dispatch)
|
||||||
|
env:
|
||||||
|
INPUT_ISSUE: ${{ inputs.issue }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source /tmp/reject.env
|
||||||
|
|
||||||
|
if [[ -n "${INPUT_ISSUE:-}" ]]; then
|
||||||
|
echo "✅ workflow_dispatch => proceed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/rejected" ]]; then
|
||||||
|
echo "ℹ️ triggering label='$LABEL_NAME' (not state/rejected) => skip"
|
||||||
|
echo "SKIP=1" >> /tmp/reject.env
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ℹ️ label unknown or rejected => continue to API gating"
|
||||||
|
|
||||||
|
- name: Comment + close (only if issue has state/rejected; conflict-guard approved+rejected)
|
||||||
env:
|
env:
|
||||||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source /tmp/reject.env
|
source /tmp/reject.env
|
||||||
|
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||||
|
|
||||||
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; }
|
||||||
@@ -125,10 +139,9 @@ jobs:
|
|||||||
-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
|
> /tmp/issue.json
|
||||||
|
|
||||||
# conflict guard: approved + rejected => do nothing, comment warning
|
node --input-type=module - <<'NODE' > /tmp/reject.flags
|
||||||
node --input-type=module <<'NODE' > /tmp/reject.flags
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
const issue = JSON.parse(fs.readFileSync("/tmp/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) : [];
|
||||||
@@ -139,6 +152,12 @@ jobs:
|
|||||||
|
|
||||||
source /tmp/reject.flags
|
source /tmp/reject.flags
|
||||||
|
|
||||||
|
# If issue does not actually have state/rejected -> do nothing (normal label churn)
|
||||||
|
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
|
||||||
|
echo "ℹ️ issue has no state/rejected => 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")"
|
||||||
@@ -151,7 +170,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")"
|
||||||
|
|
||||||
@@ -161,7 +179,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" \
|
||||||
|
|||||||
Reference in New Issue
Block a user