188 lines
6.8 KiB
YAML
188 lines
6.8 KiB
YAML
name: Anno Reject (close issue)
|
||
|
||
on:
|
||
issues:
|
||
types: [labeled]
|
||
workflow_dispatch:
|
||
inputs:
|
||
issue:
|
||
description: "Issue number to reject/close"
|
||
required: true
|
||
|
||
env:
|
||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||
|
||
defaults:
|
||
run:
|
||
shell: bash
|
||
|
||
concurrency:
|
||
group: anno-reject-${{ github.event.issue.number || inputs.issue || 'manual' }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
reject:
|
||
runs-on: mac-ci
|
||
container:
|
||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||
|
||
steps:
|
||
- name: Tools sanity
|
||
run: |
|
||
set -euo pipefail
|
||
node --version
|
||
|
||
- name: Derive context (event.json / workflow_dispatch)
|
||
env:
|
||
INPUT_ISSUE: ${{ inputs.issue }}
|
||
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
|
||
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/reject.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") : "");
|
||
|
||
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) && cloneUrl) {
|
||
const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
|
||
if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
|
||
}
|
||
if (!owner || !repo) throw new Error("Cannot infer owner/repo");
|
||
|
||
const issueNumber =
|
||
ev?.issue?.number ||
|
||
ev?.issue?.index ||
|
||
(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 ||
|
||
(typeof ev?.label === "string" ? ev.label : "") ||
|
||
"";
|
||
|
||
let apiBase = "";
|
||
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
|
||
apiBase = String(process.env.FORGE_API).trim().replace(/\/+$/,"");
|
||
} else if (cloneUrl) {
|
||
apiBase = new URL(cloneUrl).origin;
|
||
} else {
|
||
apiBase = "";
|
||
}
|
||
|
||
function sh(s){ return JSON.stringify(String(s ?? "")); }
|
||
|
||
process.stdout.write([
|
||
`OWNER=${sh(owner)}`,
|
||
`REPO=${sh(repo)}`,
|
||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||
`LABEL_NAME=${sh(labelName)}`,
|
||
`API_BASE=${sh(apiBase)}`,
|
||
`SKIP=0`
|
||
].join("\n") + "\n");
|
||
NODE
|
||
|
||
echo "✅ context:"
|
||
sed -n '1,160p' /tmp/reject.env
|
||
|
||
- 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:
|
||
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
source /tmp/reject.env
|
||
[[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||
|
||
test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }
|
||
test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; }
|
||
|
||
curl -fsS \
|
||
-H "Authorization: token $FORGE_TOKEN" \
|
||
-H "Accept: application/json" \
|
||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||
> /tmp/issue.json
|
||
|
||
node --input-type=module - <<'NODE' > /tmp/reject.flags
|
||
import fs from "node:fs";
|
||
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 hasApproved = labels.includes("state/approved");
|
||
const hasRejected = labels.includes("state/rejected");
|
||
process.stdout.write(`HAS_APPROVED=${hasApproved ? "1":"0"}\nHAS_REJECTED=${hasRejected ? "1":"0"}\n`);
|
||
NODE
|
||
|
||
source /tmp/reject.flags
|
||
|
||
# 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
|
||
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡️ Action manuelle requise : retirer l'un des deux labels avant relance."
|
||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||
curl -fsS -X POST \
|
||
-H "Authorization: token $FORGE_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||
--data-binary "$PAYLOAD"
|
||
echo "ℹ️ conflict => stop"
|
||
exit 0
|
||
fi
|
||
|
||
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
|
||
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
|
||
|
||
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"
|
||
|
||
curl -fsS -X PATCH \
|
||
-H "Authorization: token $FORGE_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||
--data-binary '{"state":"closed"}'
|
||
|
||
echo "✅ rejected+closed" |