From b585159e57e2176e9016eac5a5470d2b816b4d46 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:49:46 +0200 Subject: [PATCH] Let the Discord bot drive the testing thread lifecycle The lifecycle workflow and its curl helper move into the services Discord bot, which receives the issue events through a GitHub App webhook. App webhooks also see labels set with GITHUB_TOKEN, so the explicit dispatch after the migration PR is no longer needed. Merge once the bot is deployed with GITHUB_WEBHOOK_SECRET set. --- .../workflows/discord-thread-lifecycle.yml | 433 ------------------ .github/workflows/move-to-main-repo.yaml | 13 - .github/workflows/scripts/discord-thread.sh | 214 --------- 3 files changed, 660 deletions(-) delete mode 100644 .github/workflows/discord-thread-lifecycle.yml delete mode 100644 .github/workflows/scripts/discord-thread.sh diff --git a/.github/workflows/discord-thread-lifecycle.yml b/.github/workflows/discord-thread-lifecycle.yml deleted file mode 100644 index 2019745612..0000000000 --- a/.github/workflows/discord-thread-lifecycle.yml +++ /dev/null @@ -1,433 +0,0 @@ -name: Discord thread lifecycle - -on: - issues: - types: - - labeled - - unlabeled - - closed - workflow_dispatch: - inputs: - issue_number: - description: Issue number to act on - required: true - type: string - mode: - description: Lifecycle step to run - required: true - type: choice - options: - - ready - - hold - - defer - - migrate - - delete - -permissions: - contents: read - issues: write - actions: write - -concurrency: - group: discord-thread-${{ github.event.issue.number || inputs.issue_number }} - cancel-in-progress: false - -jobs: - # --------------------------------------------------------------------------- - # Collapse the trigger into one lifecycle mode plus the issue facts every - # downstream step needs, so nothing below re-derives the label logic. - # --------------------------------------------------------------------------- - route: - runs-on: ubuntu-latest - if: github.repository == 'community-scripts/DevScripts' - outputs: - mode: ${{ steps.route.outputs.mode }} - issue_number: ${{ steps.route.outputs.issue_number }} - issue_title: ${{ steps.route.outputs.issue_title }} - issue_url: ${{ steps.route.outputs.issue_url }} - issue_locked: ${{ steps.route.outputs.issue_locked }} - thread_name: ${{ steps.route.outputs.thread_name }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - steps: - - name: Determine mode - id: route - env: - EVENT_NAME: ${{ github.event_name }} - EVENT_ACTION: ${{ github.event.action }} - EVENT_LABEL: ${{ github.event.label.name }} - ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, '|') }} - INPUT_MODE: ${{ inputs.mode }} - INPUT_ISSUE: ${{ inputs.issue_number }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_URL: ${{ github.event.issue.html_url }} - ISSUE_LOCKED: ${{ github.event.issue.locked }} - run: | - mode=noop - - if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - mode="$INPUT_MODE" - ISSUE_NUMBER="$INPUT_ISSUE" - META=$(gh api "/repos/$REPO/issues/$ISSUE_NUMBER") - ISSUE_TITLE=$(jq -r '.title' <<<"$META") - ISSUE_URL=$(jq -r '.html_url' <<<"$META") - ISSUE_LOCKED=$(jq -r '.locked' <<<"$META") - elif [ "$EVENT_ACTION" = "closed" ]; then - mode=delete - else - case "$EVENT_ACTION:$EVENT_LABEL" in - "labeled:Ready For Testing") mode=ready ;; - "unlabeled:Ready For Testing") mode=hold ;; - "labeled:deferred") mode=defer ;; - "labeled:Started Migration To ProxmoxVE") mode=migrate ;; - esac - - # Removing 'Ready For Testing' usually accompanies a deferral or a - # migration; those modes own the messaging, so drop the duplicate. - if [ "$mode" = "hold" ] && printf '%s' "|$ISSUE_LABELS|" \ - | grep -qE '\|(deferred|Migration To ProxmoxVE|Started Migration To ProxmoxVE)\|'; then - mode=noop - fi - fi - - echo "Mode: $mode (event=$EVENT_NAME action=$EVENT_ACTION label=$EVENT_LABEL issue=$ISSUE_NUMBER)" - - { - echo "mode=$mode" - echo "issue_number=$ISSUE_NUMBER" - echo "issue_url=$ISSUE_URL" - echo "issue_locked=${ISSUE_LOCKED:-false}" - printf 'issue_title<> "$GITHUB_OUTPUT" - - lifecycle: - runs-on: ubuntu-latest - needs: route - if: needs.route.outputs.mode != 'noop' - env: - DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} - DISCORD_GUILD_ID: ${{ vars.DISCORD_GUILD_ID }} - DISCORD_CHANNEL_ID: ${{ vars.DISCORD_CHANNEL_ID }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - MODE: ${{ needs.route.outputs.mode }} - THREAD_NAME: ${{ needs.route.outputs.thread_name }} - ISSUE_NUMBER: ${{ needs.route.outputs.issue_number }} - ISSUE_TITLE: ${{ needs.route.outputs.issue_title }} - ISSUE_URL: ${{ needs.route.outputs.issue_url }} - TAG_TESTING: Testing - TAG_HOLD: On Hold - TAG_DEFERRED: Deferred - TAG_MIGRATING: Migrating - HELPER: .github/workflows/scripts/discord-thread.sh - steps: - - name: Checkout helper script - uses: actions/checkout@v5 - with: - sparse-checkout: .github/workflows/scripts - sparse-checkout-cone-mode: false - - - name: Resolve Discord thread - id: thread - run: | - chmod +x "$HELPER" - id=$("$HELPER" resolve "$ISSUE_NUMBER" "$THREAD_NAME") - echo "id=$id" >> "$GITHUB_OUTPUT" - if [ -n "$id" ]; then - echo "Resolved thread: $id" - else - echo "No existing thread for: $THREAD_NAME" - fi - - # ----------------------------------------------------------------------- - # Ready For Testing - # ----------------------------------------------------------------------- - - name: Resolve the script to test - if: env.MODE == 'ready' - run: | - ISSUE_BODY=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json body --jq '.body // ""') - RAW=https://raw.githubusercontent.com/community-scripts/DevScripts/main - - # First answered line under an issue form heading. - field() { - printf '%s\n' "$ISSUE_BODY" | tr -d '\r' \ - | sed -n "/^###[[:space:]]*$1/,/^###/p" \ - | sed '1d;/^###/d;/^[[:space:]]*$/d;/^_No response_$/d' | head -n1 - } - - NAME_RAW=$(field 'Name of the Script') - if [ -z "$NAME_RAW" ]; then - NAME_RAW="$ISSUE_TITLE" - fi - SLUG=$(printf '%s' "$NAME_RAW" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | sed 's/[[:space:]]\+/-/g') - - case "$(field 'Script Type')" in - CT*) SCRIPT_TYPE="ct" ;; - VM*) SCRIPT_TYPE="vm" ;; - Addon*) SCRIPT_TYPE="addon" ;; - "PVE Tool"*) SCRIPT_TYPE="pve" ;; - Other*) SCRIPT_TYPE="other" ;; - *) if [[ "$SLUG" == *"-vm"* ]]; then SCRIPT_TYPE="vm"; else SCRIPT_TYPE="ct"; fi ;; - esac - - SCRIPT_PATH=$(field 'Script path' | tr -d '[:space:]' | sed 's|^/||') - if [[ ! "$SCRIPT_PATH" =~ ^[A-Za-z0-9._/-]+\.sh$ || "$SCRIPT_PATH" == *..* ]]; then - SCRIPT_PATH="" - fi - if [ -z "$SCRIPT_PATH" ] && [[ "$SLUG" =~ ^[a-z0-9._-]+$ ]]; then - case "$SCRIPT_TYPE" in - ct) SCRIPT_PATH="ct/${SLUG}.sh" ;; - vm) SCRIPT_PATH="vm/${SLUG}.sh" - curl -fsI "$RAW/$SCRIPT_PATH" >/dev/null 2>&1 || SCRIPT_PATH="vm/${SLUG%-vm}-vm.sh" ;; - addon) SCRIPT_PATH="tools/addon/${SLUG}.sh" ;; - pve) SCRIPT_PATH="tools/pve/${SLUG}.sh" ;; - esac - fi - - if [ -z "$SCRIPT_PATH" ] || ! curl -fsSL --retry 3 -o script.sh "$RAW/$SCRIPT_PATH"; then - gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body \ - "⚠️ \`${SCRIPT_PATH:-no script path}\` is not on \`main\`, so there is no test command to post. Fill in **Script path** in the issue or fix the name, then add \`Ready For Testing\` again." - exit 1 - fi - - APPLIANCE=$(basename "$SCRIPT_PATH" | grep -oE '(^|-)(pbs|pmg|pdm)([0-9-]|\.sh$)' | grep -oE 'pbs|pmg|pdm' | head -n1 || true) - if [[ "$SCRIPT_TYPE" == ct || "$SCRIPT_TYPE" == vm ]]; then - RUNS_ON="pve" - else - case "$(field 'Where does it run?')" in - "Proxmox VE host") RUNS_ON="pve" ;; - "Inside an LXC or VM") RUNS_ON="guest" ;; - "Proxmox Backup Server") RUNS_ON="pbs" ;; - "Proxmox Mail Gateway") RUNS_ON="pmg" ;; - "Proxmox Datacenter Manager") RUNS_ON="pdm" ;; - *) - if [[ "$SCRIPT_PATH" == tools/pve/* ]]; then - RUNS_ON="${APPLIANCE:-pve}" - elif sed -e 's/^[[:space:]]*#.*//' -e 's/command -v [^[:space:]]*//g' script.sh \ - | grep -qE '(^|[[:space:];&|(`$])(pct|qm|pveam|pvesm|pvesh|pveversion)[[:space:]]'; then - RUNS_ON="pve" - elif [ -n "$APPLIANCE" ]; then - RUNS_ON="$APPLIANCE" - elif [[ "$SCRIPT_PATH" == tools/addon/* ]]; then - RUNS_ON="guest" - else - RUNS_ON="unknown" - fi ;; - esac - fi - - { - echo "SLUG=$SLUG" - echo "SCRIPT_TYPE=$SCRIPT_TYPE" - echo "SCRIPT_PATH=$SCRIPT_PATH" - echo "RUNS_ON=$RUNS_ON" - } >> "$GITHUB_ENV" - echo "Resolved $SCRIPT_TYPE script $SCRIPT_PATH, runs on: $RUNS_ON" - - - name: Build testing message - if: env.MODE == 'ready' - run: | - case "$RUNS_ON:$SCRIPT_TYPE" in - pve:ct) WHERE="in the Proxmox VE shell (or on an Incus host) to create a container" ;; - pve:vm) WHERE="in the Proxmox VE shell to create a VM" ;; - pve:*) WHERE="in the Proxmox VE shell" ;; - guest:*) WHERE="inside the LXC or VM that should get it" ;; - pbs:*) WHERE="in the Proxmox Backup Server shell" ;; - pmg:*) WHERE="in the Proxmox Mail Gateway shell" ;; - pdm:*) WHERE="in the Proxmox Datacenter Manager shell" ;; - *) WHERE="as described in the issue" ;; - esac - - { - printf 'The %s script is ready for testing. Run it %s:\n' "$ISSUE_TITLE" "$WHERE" - printf '```\nbash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/DevScripts/main/%s)"\n```\n' "$SCRIPT_PATH" - } > testing.md - - if [ "$SCRIPT_TYPE" != "pve" ]; then - JSON=$(curl -fsSL "https://github.com/community-scripts/DevScripts/raw/main/json/${SLUG}.json" 2>/dev/null || echo '{}') - - username=$(printf '%s' "$JSON" | jq -r '.default_credentials.username // empty' 2>/dev/null || true) - password=$(printf '%s' "$JSON" | jq -r '.default_credentials.password // empty' 2>/dev/null || true) - if [ -n "$username" ] || [ -n "$password" ]; then - printf '\nDefault credentials:\n' >> testing.md - if [ -n "$username" ]; then - printf 'Username: %s\n' "$username" >> testing.md - fi - if [ -n "$password" ]; then - printf 'Password: %s\n' "$password" >> testing.md - fi - fi - - NOTES=$(printf '%s' "$JSON" | jq -r '.notes[]?.text // empty' 2>/dev/null || true) - if [ -n "$NOTES" ]; then - printf '\n%s\n' "$NOTES" >> testing.md - fi - fi - - if [[ "$SCRIPT_TYPE" == ct || "$SCRIPT_TYPE" == vm ]]; then - printf '\nNote: This is not in the official repo yet—it'"'"'s just a dev version! After merging into ProxmoxVE, it will need to be recreated.\n' >> testing.md - else - printf '\nNote: This is not in the official repo yet—it'"'"'s just a dev version!\n' >> testing.md - fi - - cp testing.md discord.md - printf '\nDiscussion & issue tracking:\n%s\n' "$ISSUE_URL" >> discord.md - - echo "--- Discord message ---" - cat discord.md - - - name: Create Discord thread - id: create - if: env.MODE == 'ready' && steps.thread.outputs.id == '' - run: | - id=$("$HELPER" create "$THREAD_NAME" discord.md "$TAG_TESTING") - if [ -z "$id" ] || [ "$id" = "null" ]; then - echo "::error::Discord thread creation returned no id" - exit 1 - fi - echo "id=$id" >> "$GITHUB_OUTPUT" - echo "Created thread: $id" - - - name: Comment testing instructions on issue - if: steps.create.outputs.id != '' - env: - THREAD_ID: ${{ steps.create.outputs.id }} - run: | - { - cat testing.md - printf '\nDiscord testing thread: https://discord.com/channels/%s/%s\n' "$DISCORD_GUILD_ID" "$THREAD_ID" - printf '\n\n' "$THREAD_ID" - } > comment.md - gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file comment.md - - - name: Reopen existing thread for testing - if: env.MODE == 'ready' && steps.thread.outputs.id != '' - env: - THREAD_ID: ${{ steps.thread.outputs.id }} - run: | - { - printf '▶️ **Testing has resumed** — the `Ready For Testing` label is back on the issue.\n\n' - cat testing.md - printf '\n🔗 %s\n' "$ISSUE_URL" - } > notice.md - "$HELPER" post "$THREAD_ID" notice.md - "$HELPER" state "$THREAD_ID" false false "$TAG_TESTING" - - # Also re-anchors the thread id on issues whose thread predates the marker. - { - printf '▶️ Testing resumed. Discord thread: https://discord.com/channels/%s/%s\n' "$DISCORD_GUILD_ID" "$THREAD_ID" - printf '\n\n' "$THREAD_ID" - } > comment.md - gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file comment.md - - - name: Unlock issue - if: env.MODE == 'ready' && needs.route.outputs.issue_locked == 'true' - run: | - gh api --method DELETE "/repos/$REPO/issues/$ISSUE_NUMBER/lock" - - - name: Push script JSON to PocketBase - if: env.MODE == 'ready' && env.SCRIPT_TYPE != 'pve' && env.SCRIPT_TYPE != 'other' - run: | - gh workflow run push_json_to_pocketbase.yml --repo "$REPO" -f script_slug="$SLUG" - - # ----------------------------------------------------------------------- - # Hold / defer / migrate - # ----------------------------------------------------------------------- - - name: Warn about missing thread - if: steps.thread.outputs.id == '' && env.MODE != 'ready' - run: echo "::warning::No Discord thread found for '$THREAD_NAME' — nothing to update." - - - name: Collect latest issue update - if: env.MODE == 'hold' || env.MODE == 'defer' - run: | - gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json comments --jq ' - .comments - | map(select((.author.login // "") | test("\\[bot\\]$|^github-actions$") | not)) - | last | .body // ""' > last_update.txt - - if [ ! -s last_update.txt ]; then - gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json body --jq '.body // ""' > last_update.txt - fi - - if [ "$(wc -c < last_update.txt)" -gt 800 ]; then - head -c 800 last_update.txt > trimmed.txt - printf '\n…\n' >> trimmed.txt - mv trimmed.txt last_update.txt - fi - - - name: Pause testing in Discord thread - if: env.MODE == 'hold' && steps.thread.outputs.id != '' - env: - THREAD_ID: ${{ steps.thread.outputs.id }} - run: | - { - printf '⚠️ **Testing paused** — the `Ready For Testing` label was removed because there may be problems with this script.\n\n' - printf 'The thread stays open, but please hold off on testing the version above until it is marked ready again.\n\n' - printf 'Latest update from the issue:\n\n' - cat last_update.txt - printf '\n\n🔗 %s\n' "$ISSUE_URL" - } > notice.md - "$HELPER" post "$THREAD_ID" notice.md - "$HELPER" state "$THREAD_ID" false false "$TAG_HOLD" - - - name: Defer testing in Discord thread - if: env.MODE == 'defer' && steps.thread.outputs.id != '' - env: - THREAD_ID: ${{ steps.thread.outputs.id }} - run: | - { - printf '⏸️ **This script has been deferred** and is postponed indefinitely.\n\n' - printf 'This thread is locked. It will reopen if the `Ready For Testing` label is added to the issue again.\n\n' - printf 'Latest update from the issue:\n\n' - cat last_update.txt - printf '\n\n🔗 %s\n' "$ISSUE_URL" - } > notice.md - "$HELPER" post "$THREAD_ID" notice.md - "$HELPER" state "$THREAD_ID" true true "$TAG_DEFERRED" - - - name: Comment and lock deferred issue - if: env.MODE == 'defer' - run: | - gh issue comment "$ISSUE_NUMBER" --repo "$REPO" \ - --body "⏸️ This script has been **deferred**. The Discord testing thread has been locked. Re-add the \`Ready For Testing\` label to resume testing." - gh api --method PUT "/repos/$REPO/issues/$ISSUE_NUMBER/lock" -f lock_reason=resolved - - - name: Announce migration in Discord thread - if: env.MODE == 'migrate' && steps.thread.outputs.id != '' - env: - THREAD_ID: ${{ steps.thread.outputs.id }} - run: | - # The manual label and the dispatch from move-to-main-repo can both land; - # a locked thread means the announcement already went out. - if [ "$("$HELPER" locked "$THREAD_ID")" = "true" ]; then - echo "Thread already locked — migration was announced before." - exit 0 - fi - - PR=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json comments \ - --jq '[.comments[].body // "" | scan("community-scripts/ProxmoxVE#([0-9]+)")[]] | last // empty') - - { - printf '🚚 **Migration to ProxmoxVE has started.**\n\n' - if [ -n "$PR" ]; then - printf 'A pull request is open in the main repository: https://github.com/community-scripts/ProxmoxVE/pull/%s\n\n' "$PR" - fi - printf 'Testing in DevScripts is finished — this thread is locked and will be removed once the migration is complete. Please continue the discussion on the pull request.\n\n' - printf '🔗 %s\n' "$ISSUE_URL" - } > notice.md - "$HELPER" post "$THREAD_ID" notice.md - "$HELPER" state "$THREAD_ID" true true "$TAG_MIGRATING" - - # ----------------------------------------------------------------------- - # Issue closed -> the thread has served its purpose - # ----------------------------------------------------------------------- - - name: Delete Discord thread - if: env.MODE == 'delete' && steps.thread.outputs.id != '' - env: - THREAD_ID: ${{ steps.thread.outputs.id }} - run: | - "$HELPER" delete "$THREAD_ID" - echo "Deleted thread: $THREAD_ID" diff --git a/.github/workflows/move-to-main-repo.yaml b/.github/workflows/move-to-main-repo.yaml index feaa7e323b..714ccfcd98 100644 --- a/.github/workflows/move-to-main-repo.yaml +++ b/.github/workflows/move-to-main-repo.yaml @@ -10,7 +10,6 @@ permissions: contents: write issues: write pull-requests: write - actions: write jobs: move-to-main-repo: @@ -330,15 +329,3 @@ jobs: run: | gh issue comment "$issue_nr" --body "A PR has been created for ${script_name}: community-scripts/ProxmoxVE#${pr_number}" gh issue edit "$issue_nr" --remove-label "Migration To ProxmoxVE" --add-label "Started Migration To ProxmoxVE" - - - name: Announce migration in the Discord thread - if: steps.create_pull_request.outputs.pr_number - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # The label above is set with GITHUB_TOKEN and therefore raises no - # 'labeled' event, so the Discord workflow is dispatched explicitly. - gh workflow run discord-thread-lifecycle.yml \ - --repo ${{ github.repository }} \ - -f issue_number=${{ steps.list_issues.outputs.issue_nr }} \ - -f mode=migrate diff --git a/.github/workflows/scripts/discord-thread.sh b/.github/workflows/scripts/discord-thread.sh deleted file mode 100644 index 777fc815fb..0000000000 --- a/.github/workflows/scripts/discord-thread.sh +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -API="https://discord.com/api/v10" -NAME_LIMIT=100 -CONTENT_LIMIT=1990 - -RESPONSE="" -HTTP_CODE="" - -: "${DISCORD_BOT_TOKEN:?DISCORD_BOT_TOKEN is required}" -: "${DISCORD_CHANNEL_ID:?DISCORD_CHANNEL_ID is required}" - -request() { - local method=$1 path=$2 body=${3:-} - local attempt out wait - local args=( - --silent --show-error - --write-out $'\n%{http_code}' - --request "$method" "$API$path" - --header "Authorization: Bot $DISCORD_BOT_TOKEN" - --header "Content-Type: application/json" - ) - if [[ -n $body ]]; then - args+=(--data "$body") - fi - - for attempt in 1 2 3; do - if ! out=$(curl "${args[@]}"); then - HTTP_CODE="000" - RESPONSE="" - continue - fi - HTTP_CODE=${out##*$'\n'} - RESPONSE=${out%$'\n'*} - - if [[ $HTTP_CODE == 2* ]]; then - return 0 - fi - if [[ $HTTP_CODE != 429 && $HTTP_CODE != 5* ]]; then - break - fi - wait=$(jq -r '.retry_after // empty' <<<"$RESPONSE" 2>/dev/null || true) - wait=${wait%.*} - if ! [[ $wait =~ ^[0-9]+$ ]]; then - wait=2 - fi - sleep "$wait" - done - - if [[ ${QUIET:-0} != 1 ]]; then - echo "::error::Discord $method $path -> HTTP $HTTP_CODE: $RESPONSE" >&2 - fi - return 1 -} - -# Probe variant for lookups where a miss is an expected answer, not a failure. -try_request() { - local QUIET=1 - request "$@" -} - -tag_id() { - local name=$1 - if [[ -z $name ]]; then - return 0 - fi - if ! try_request GET "/channels/$DISCORD_CHANNEL_ID"; then - return 0 - fi - jq -r --arg n "$name" \ - 'first(.available_tags[]? | select((.name | ascii_downcase) == ($n | ascii_downcase)) | .id) // empty' \ - <<<"$RESPONSE" -} - -with_tag() { - local payload=$1 name=$2 id - id=$(tag_id "$name") - if [[ -z $id ]]; then - if [[ -n $name ]]; then - echo "::notice::Forum tag '$name' not found, leaving tags untouched" >&2 - fi - printf '%s' "$payload" - return 0 - fi - jq --arg id "$id" '.applied_tags = [$id]' <<<"$payload" -} - -content_json() { - local file=$1 - jq -n --rawfile raw "$file" --argjson limit "$CONTENT_LIMIT" \ - '$raw | if (length > $limit) then .[0:$limit] + "\n…" else . end' -} - -thread_exists() { - try_request GET "/channels/$1" >/dev/null -} - -find_active() { - local name=$1 - if [[ -z ${DISCORD_GUILD_ID:-} ]]; then - return 0 - fi - if ! try_request GET "/guilds/$DISCORD_GUILD_ID/threads/active"; then - return 0 - fi - jq -r --arg n "$name" --arg p "$DISCORD_CHANNEL_ID" \ - 'first(.threads[]? | select(.parent_id == $p and .name == $n) | .id) // empty' \ - <<<"$RESPONSE" -} - -find_archived() { - local name=$1 before="" path id page - for page in 1 2 3 4 5; do - path="/channels/$DISCORD_CHANNEL_ID/threads/archived/public?limit=100" - if [[ -n $before ]]; then - path="$path&before=${before//+/%2B}" - fi - if ! try_request GET "$path"; then - return 0 - fi - id=$(jq -r --arg n "$name" 'first(.threads[]? | select(.name == $n) | .id) // empty' <<<"$RESPONSE") - if [[ -n $id ]]; then - printf '%s' "$id" - return 0 - fi - if [[ $(jq -r '.has_more // false' <<<"$RESPONSE") != true ]]; then - return 0 - fi - before=$(jq -r '.threads[-1].thread_metadata.archive_timestamp // empty' <<<"$RESPONSE") - if [[ -z $before ]]; then - return 0 - fi - done -} - -find_marker() { - local issue=$1 - gh issue view "$issue" --repo "$GITHUB_REPOSITORY" --json comments \ - --jq '[.comments[].body // "" | scan("discord-thread-id: ([0-9]+)")[]] | last // empty' 2>/dev/null || true -} - -# resolve -# The marker comment is authoritative and survives renames and archiving; the -# name lookups only exist for threads created before markers were written. -cmd_resolve() { - local issue=$1 name=$2 id - id=$(find_marker "$issue") - if [[ -n $id ]] && thread_exists "$id"; then - printf '%s' "$id" - return 0 - fi - id=$(find_active "$name") - if [[ -n $id ]]; then - printf '%s' "$id" - return 0 - fi - find_archived "$name" -} - -# create [tag-name] -cmd_create() { - local name=$1 file=$2 tag=${3:-} payload - payload=$(jq -n --arg name "${name:0:$NAME_LIMIT}" --argjson content "$(content_json "$file")" \ - '{name: $name, message: {content: $content}, applied_tags: []}') - payload=$(with_tag "$payload" "$tag") - request POST "/channels/$DISCORD_CHANNEL_ID/threads" "$payload" - jq -r '.id' <<<"$RESPONSE" -} - -# post -# A forum post that auto-archived rejects new messages, so unarchive first and -# let the caller re-apply the target state afterwards. -cmd_post() { - local thread=$1 file=$2 payload - request PATCH "/channels/$thread" '{"archived": false, "locked": false}' >/dev/null - payload=$(jq -n --argjson content "$(content_json "$file")" '{content: $content}') - request POST "/channels/$thread/messages" "$payload" >/dev/null -} - -# state [tag-name] -cmd_state() { - local thread=$1 locked=$2 archived=$3 tag=${4:-} payload - payload=$(jq -n --argjson locked "$locked" --argjson archived "$archived" \ - '{locked: $locked, archived: $archived}') - payload=$(with_tag "$payload" "$tag") - request PATCH "/channels/$thread" "$payload" >/dev/null -} - -# locked -cmd_locked() { - if try_request GET "/channels/$1"; then - jq -r '.thread_metadata.locked // false' <<<"$RESPONSE" - else - printf 'false' - fi -} - -# delete -cmd_delete() { - request DELETE "/channels/$1" >/dev/null -} - -command=${1:?usage: discord-thread.sh ...} -shift -case "$command" in - resolve) cmd_resolve "$@" ;; - create) cmd_create "$@" ;; - post) cmd_post "$@" ;; - state) cmd_state "$@" ;; - locked) cmd_locked "$@" ;; - delete) cmd_delete "$@" ;; - *) echo "::error::unknown command: $command" >&2; exit 1 ;; -esac