Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 272 additions & 40 deletions .github/workflows/harness-quickstart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,135 @@ on:
channel:
description: Installer channel to validate
type: choice
options: [main, next]
default: main
options: [latest, next]
default: latest

permissions:
contents: read

concurrency:
group: harness-quickstart-${{ inputs.channel || 'main' }}
group: harness-quickstart-${{ inputs.channel || github.event_name }}
cancel-in-progress: true

jobs:
validate:
name: harness quickstart / latest registry (${{ inputs.channel || 'main' }})
name: harness quickstart / published registry (${{ matrix.channel }})
strategy:
fail-fast: false
matrix:
channel: >-
${{
github.event_name == 'schedule'
&& fromJSON('["latest","next"]')
|| fromJSON(format('["{0}"]', inputs.channel))
}}
runs-on: ubuntu-latest
timeout-minutes: 25
# Covers registry timeouts plus terminal rendering and finalization.
timeout-minutes: 35
steps:
- name: Notify Slack that validation started
id: slack_start
continue-on-error: true
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL: worker-releases
III_CHANNEL: ${{ matrix.channel }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
text="⏳ *Harness quickstart* started · \`${III_CHANNEL}\` · <${RUN_URL}|view run>"
payload=$(jq -n \
--arg channel "$SLACK_CHANNEL" \
--arg text "$text" \
'{channel: $channel, text: $text}')
response=$(curl -sS --retry 3 --retry-all-errors \
-X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$payload")
Comment on lines +42 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Slack chat.postMessage still support channel name instead of channel ID?

💡 Result:

Yes, the Slack chat.postMessage API method still supports using a channel name instead of a channel ID [1][2]. According to the official Slack API documentation, you can pass either the channel name (e.g., #general) or the encoded channel ID to the channel parameter [1]. However, while this is supported, it is generally discouraged in production environments. Channel names can change over time, whereas channel IDs remain permanent identifiers [3]. Furthermore, most other Slack API methods that require a channel identifier do not support names and specifically require the channel ID [3]. Using the channel ID is considered the best practice to ensure reliability and consistency across your integration [3].

Citations:


🌐 Web query:

Slack chat.postMessage channel parameter "channel" accept bare channel name without # Slack API

💡 Result:

Yes, the Slack API chat.postMessage channel parameter accepts a bare channel name (without the # prefix) [1][2][3][4]. According to official Slack API documentation, the channel argument can be an encoded channel ID (e.g., C123456) or the channel's name [1][2]. When passing the name, you do not need to include the hash (#) symbol [1][2]. While the API allows using channel names, it is generally recommended to use the channel's unique ID instead [5]. Channel names can be renamed, whereas channel IDs are persistent, ensuring your application remains reliable if a channel's name is changed [5][6]. You can retrieve the ID associated with a channel name by using the conversations.list API method [1][5][7].

Citations:


🌐 Web query:

Slack API channel names chat.postMessage channel parameter no # examples

💡 Result:

The Slack API chat.postMessage method's channel parameter accepts either an encoded channel ID (e.g., C12345678) or a channel name [1][2]. When using a channel name, you do not need to include the hash (#) prefix [2]. You can pass the name as a plain string (e.g., "general") [2]. While the API documentation has historically permitted channel names [2], using the channel ID is strongly recommended because channel names are not globally unique across workspaces and can be changed by users, whereas the channel ID is immutable [1]. To obtain the correct channel ID, you can use the conversations.list API method [1][3].

Citations:


🌐 Web query:

Slack API channel_not_name channel_param_is_not_valid chat.postMessage channel name

💡 Result:

The channel_not_found error in the Slack API's chat.postMessage method indicates that the Slack platform cannot locate the specified channel or that your application does not have the necessary permission to post into it [1][2][3]. Common causes and solutions include: 1. App Membership: For private channels, your Slack app or bot must be explicitly added to the channel as a member [2][4]. You can invite your bot by typing /invite @YourBotName in the channel [3][4]. 2. Incorrect Identifier: Ensure the value passed for the channel argument is correct [5]. While Slack often supports both channel names (e.g., #general) and channel IDs (e.g., C024BE91L), using the unique channel ID is more reliable and recommended to avoid ambiguity [6][5][4]. 3. Permission Scopes: Verify that your application has the required scopes for the requested action, such as chat:write or chat:write.public (for public channels where the bot is not a member) [7][4]. 4. Token Context: Ensure you are using the correct token associated with the intended workspace. Errors can occur if an OAuth token is used for a workspace where the app was not installed, or if there is a mismatch between the token and the channel access [8][5]. 5. Formatting: If making manual API requests, ensure parameters are correctly encoded in the request body (e.g., application/x-www-form-urlencoded) rather than incorrectly placed in headers or improperly formatted [8][9][10].

Citations:


🌐 Web query:

Slack chat.postMessage channel parameter "channel" accepts channel name or encoded channel ID

💡 Result:

The channel parameter in the Slack chat.postMessage method accepts either a Slack-encoded channel ID (e.g., C123456) or a channel name (e.g., #general) [1][2][3]. While it is possible to use a channel name, it is generally recommended to use the channel ID [4]. Channel names are mutable and can be changed by workspace members, whereas channel IDs are persistent and unique identifiers [4]. Furthermore, most other Slack API methods that require a channel reference only accept the channel ID, making the use of IDs a more consistent practice across your integration [4]. If you use a channel name, the API will resolve it to the corresponding channel, but relying on IDs ensures greater stability for your application [1][4].

Citations:


🌐 Web query:

curl --retry-all-errors retry POST request duplicate submission non-idempotent documentation

💡 Result:

Using the --retry-all-errors option in curl is considered a "sledgehammer" approach to retrying transfers and should be used with extreme caution, particularly with non-idempotent HTTP methods like POST [1][2]. Key Considerations: 1. Risk of Duplicate Submissions: When you use --retry-all-errors, curl will attempt to retry the request for any error it encounters [3][4]. If the initial request was a POST or another non-idempotent method that partially reached the server—or was processed before a connection error occurred—the retry will trigger a duplicate submission on the server side [1][2][5]. 2. Scope of Option: This option must be used in conjunction with --retry [1][4]. By default, curl only retries on certain connection errors and transient HTTP 5xx server errors [5]. Enabling --retry-all-errors forces retries on nearly all failures, including many 4xx client errors that would otherwise be considered final [1][5]. 3. Data Handling: The documentation warns against using this option with redirected input or output (e.g., using pipes | or >), as curl cannot automatically "reset" these streams before a retry, which can lead to receiving or sending duplicate data [1]. 4. Best Practices: * Avoid --retry-all-errors for any request that is not explicitly safe to repeat [2][5]. * Prioritize idempotent methods (like GET or HEAD) when configuring automatic retry logic [2][5]. * For POST/PUT requests, handle retries at the application level where you can verify the state of the previous submission before attempting another [2][5]. * If you must use it, ensure the command is part of a controlled, well-understood environment rather than a default configuration [1][2]. Official documentation emphasizes that you are generally better off handling specific, expected failure scenarios within a script rather than relying on this broad, indiscriminate retry mechanism [1].

Citations:


Use the Slack channel ID and limit retries for non-idempotent Slack posts.

chat.postMessage accepts channel names, but names are mutable and can fail with channel_not_found; channel_id is preferred for reliability. Also, --retry-all-errors will retry the POST submission more broadly than --retry 3, which risks duplicate Slack posts if the first request was processed before a failure was reported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/harness-quickstart.yml around lines 42 - 56, Update the
Slack post step around the payload and curl invocation to use the configured
Slack channel ID rather than the mutable channel name, while preserving the
existing message content. Remove --retry-all-errors and retain only bounded
retries appropriate for the non-idempotent chat.postMessage request.

if ! jq -e '.ok == true' <<<"$response" >/dev/null 2>&1; then
error=$(jq -r '.error // "invalid_response"' <<<"$response" 2>/dev/null || printf 'invalid_response')
echo "::warning::Slack start notification failed: $error"
exit 1
fi
message_ts=$(jq -er '.ts' <<<"$response")
channel_id=$(jq -er '.channel' <<<"$response")
echo "ts=$message_ts" >>"$GITHUB_OUTPUT"
echo "channel_id=$channel_id" >>"$GITHUB_OUTPUT"

- uses: actions/checkout@v5

- name: Install VHS for terminal recording
continue-on-error: true
env:
VHS_VERSION: "0.11.0"
TTYD_VERSION: "1.7.7"
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends ffmpeg
curl -fsSL --retry 3 \
"https://github.com/charmbracelet/vhs/releases/download/v${VHS_VERSION}/vhs_${VHS_VERSION}_Linux_x86_64.tar.gz" \
-o /tmp/vhs.tar.gz
tar -xzf /tmp/vhs.tar.gz -C /tmp
sudo install -m 755 "/tmp/vhs_${VHS_VERSION}_Linux_x86_64/vhs" /usr/local/bin/vhs
curl -fsSL --retry 3 \
"https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \
-o /tmp/ttyd
sudo install -m 755 /tmp/ttyd /usr/local/bin/ttyd
vhs --version
ttyd --version
ffprobe -v quiet -version | sed -n '1p'
vhs validate harness/tests/quickstart/quickstart.tape

- name: Run quickstart validator
id: quickstart
env:
HARNESS_QUICKSTART_ARTIFACTS_DIR: ${{ github.workspace }}/target/harness-quickstart
III_CHANNEL: ${{ inputs.channel || 'main' }}
HARNESS_QUICKSTART_TRACE: "1"
III_CHANNEL: ${{ matrix.channel }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
run: harness/tests/quickstart/run-ci.sh
run: |
set -euo pipefail

# VHS records the real validator operations. The tape waits for a
# unique shell prompt instead of matching transient terminal output.
# Pass/fail is read from result.json because VHS cannot propagate the
# typed command's exit code.
if command -v vhs >/dev/null 2>&1; then
if vhs harness/tests/quickstart/quickstart.tape; then
if ! ffprobe -v error \
-select_streams v:0 \
-show_entries stream=codec_type \
-of csv=p=0 \
target/harness-quickstart/quickstart.mp4 |
grep -qx video; then
echo "::warning::VHS produced an invalid terminal recording"
rm -f target/harness-quickstart/quickstart.mp4
fi
else
echo "::warning::VHS recording failed"
rm -f target/harness-quickstart/quickstart.mp4
if [[ ! -f target/harness-quickstart/result.json ]]; then
echo "::warning::No validator result was produced; rerunning without recording"
harness/tests/quickstart/run-ci.sh
fi
fi
else
echo "::warning::VHS is unavailable; running without recording"
harness/tests/quickstart/run-ci.sh
fi

jq -e '.status == "passed"' target/harness-quickstart/result.json >/dev/null || {
reason=$(jq -r '.failure_reason // empty' target/harness-quickstart/result.json 2>/dev/null || true)
echo "::error::quickstart validator failed${reason:+: $reason}"
exit 1
}

- name: Show quickstart logs
if: always()
run: |
shopt -s nullglob
if [[ -f target/harness-quickstart/EVIDENCE.md ]]; then
echo "::group::EVIDENCE.md"
cat target/harness-quickstart/EVIDENCE.md
echo "::endgroup::"
fi
logs=(target/harness-quickstart/logs/*.log)
if ((${#logs[@]} == 0)); then
echo "No quickstart logs were produced."
Expand All @@ -57,48 +152,185 @@ jobs:
if: always()
run: |
{
if [[ -f target/harness-quickstart/EVIDENCE.md ]]; then
cat target/harness-quickstart/EVIDENCE.md
elif [[ -f target/harness-quickstart/result.json ]]; then
if [[ -f target/harness-quickstart/result.json ]]; then
echo "### Harness quickstart"
echo
jq -r '
"| Status | CLI | Duration |",
"| --- | --- | ---: |",
"| \(.status) | \(.cli_version) | \(.elapsed_ms) ms |",
(if .failure_reason != "" then "", "Failure: \(.failure_reason)" else empty end)
"| Status | Channel | CLI | GLM canary | Duration |",
"| --- | --- | --- | --- | ---: |",
"| \(.status) | \(.channel) | \(.cli_version) | \(.message_check) | \(.elapsed_ms) ms |",
(if .failure_reason != "" then "", "**Failure:** \(.failure_reason)" else empty end)
' target/harness-quickstart/result.json
else
echo "### Harness quickstart"
echo
echo "No quickstart result was produced."
fi
if [[ -f target/harness-quickstart/quickstart.mp4 ]]; then
echo
echo "Terminal recording: \`quickstart.mp4\` in the \`harness-quickstart-${{ matrix.channel }}\` artifact."
fi
Comment on lines 158 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

failure_reason renders as null when the key is absent.

.failure_reason != "" is true for null, so a missing key emits **Failure:** null in the job summary. Use the alternative operator first.

Separately, moving ${{ matrix.channel }} (Line 171) into an env: var silences the zizmor template-injection finding; the value itself is safe here since it comes from an enumerated choice input.

🐛 Proposed fix
-                (if .failure_reason != "" then "", "**Failure:** \(.failure_reason)" else empty end)
+                (if (.failure_reason // "") != "" then "", "**Failure:** \(.failure_reason)" else empty end)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jq -r '
"| Status | CLI | Duration |",
"| --- | --- | ---: |",
"| \(.status) | \(.cli_version) | \(.elapsed_ms) ms |",
(if .failure_reason != "" then "", "Failure: \(.failure_reason)" else empty end)
"| Status | Channel | CLI | GLM canary | Duration |",
"| --- | --- | --- | --- | ---: |",
"| \(.status) | \(.channel) | \(.cli_version) | \(.message_check) | \(.elapsed_ms) ms |",
(if .failure_reason != "" then "", "**Failure:** \(.failure_reason)" else empty end)
' target/harness-quickstart/result.json
else
echo "### Harness quickstart"
echo
echo "No quickstart result was produced."
fi
if [[ -f target/harness-quickstart/quickstart.mp4 ]]; then
echo
echo "Terminal recording: \`quickstart.mp4\` in the \`harness-quickstart-${{ matrix.channel }}\` artifact."
fi
jq -r '
"| Status | Channel | CLI | GLM canary | Duration |",
"| --- | --- | --- | --- | ---: |",
"| \(.status) | \(.channel) | \(.cli_version) | \(.message_check) | \(.elapsed_ms) ms |",
(if (.failure_reason // "") != "" then "", "**Failure:** \(.failure_reason)" else empty end)
' target/harness-quickstart/result.json
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 171-171: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/harness-quickstart.yml around lines 158 - 172, Update the
jq failure condition in the result-summary block to apply the alternative
operator to failure_reason before comparing it with an empty string, preventing
missing values from rendering as null. Also expose matrix.channel through an env
variable and reference that variable in the terminal-recording message to
resolve the template-injection finding.

Source: Linters/SAST tools

} >> "$GITHUB_STEP_SUMMARY"

- name: Upload quickstart result
- name: Upload quickstart artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: harness-quickstart-result
path: |
target/harness-quickstart/result.json
target/harness-quickstart/EVIDENCE.md
target/harness-quickstart/timings.tsv
target/harness-quickstart/cli-version.txt
target/harness-quickstart/console-send.json
target/harness-quickstart/console-status.json
target/harness-quickstart/config.yaml
target/harness-quickstart/iii.lock
name: harness-quickstart-${{ matrix.channel }}
path: target/harness-quickstart/
retention-days: 7
if-no-files-found: ignore

- name: Upload quickstart logs
if: always()
uses: actions/upload-artifact@v6
with:
name: harness-quickstart-logs
path: |
target/harness-quickstart/logs/
target/harness-quickstart/console.html
retention-days: 7
if-no-files-found: ignore
- name: Finalize Slack notification
if: ${{ always() && steps.slack_start.outputs.ts != '' && steps.slack_start.outputs.channel_id != '' }}
continue-on-error: true
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ steps.slack_start.outputs.channel_id }}
SLACK_MESSAGE_TS: ${{ steps.slack_start.outputs.ts }}
III_CHANNEL: ${{ matrix.channel }}
QUICKSTART_OUTCOME: ${{ steps.quickstart.outcome }}
JOB_STATUS: ${{ job.status }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
COMMIT_SHA: ${{ github.sha }}
run: |
set -uo pipefail

slack_call() {
local method=$1 payload=$2 response error
if ! response=$(curl -sS --retry 3 --retry-all-errors \
-X POST "https://slack.com/api/${method}" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$payload"); then
echo "::warning::Slack ${method} request failed"
return 1
fi
if ! jq -e '.ok == true' <<<"$response" >/dev/null 2>&1; then
error=$(jq -r '.error // "invalid_response"' <<<"$response" 2>/dev/null || printf 'invalid_response')
echo "::warning::Slack ${method} failed: $error"
return 1
fi
}

result_file=target/harness-quickstart/result.json
if [[ -f "$result_file" ]]; then
result_status=$(jq -r '.status // "failed"' "$result_file")
cli_version=$(jq -r '.cli_version // "unknown"' "$result_file")
duration=$(jq -r '((.elapsed_ms // 0) / 1000 | floor | tostring) + "s"' "$result_file")
failure_reason=$(jq -r '.failure_reason // ""' "$result_file")
message_check=$(jq -r '.message_check // "unknown"' "$result_file")
else
result_status=failed
cli_version=unknown
duration=unknown
message_check=unknown
failure_reason="No result.json was produced (validator outcome: ${QUICKSTART_OUTCOME})."
fi

short_sha=${COMMIT_SHA:0:7}
if [[ "$JOB_STATUS" == "success" && "$result_status" == "passed" ]]; then
icon=✅
label=succeeded
detail=$(printf "*Status:* succeeded\n*Installer channel:* \`%s\`\n*CLI:* \`%s\`\n*GLM canary:* \`%s\`\n*Duration:* %s\n*Commit:* <%s|\`%s\`>\n<%s|Open GitHub Actions run>" \
"$III_CHANNEL" "$cli_version" "$message_check" "$duration" \
"$COMMIT_URL" "$short_sha" "$RUN_URL")
else
icon=❌
label=failed
if [[ -z "$failure_reason" ]]; then
failure_reason="Workflow status: ${JOB_STATUS} (validator outcome: ${QUICKSTART_OUTCOME})."
fi
detail=$(printf "*Status:* failed\n*Installer channel:* \`%s\`\n*CLI:* \`%s\`\n*GLM canary:* \`%s\`\n*Duration:* %s\n*Failure:* %s\n*Commit:* <%s|\`%s\`>\n<%s|Open GitHub Actions run>" \
"$III_CHANNEL" "$cli_version" "$message_check" "$duration" \
"${failure_reason:-unknown failure}" \
"$COMMIT_URL" "$short_sha" "$RUN_URL")
fi

root_text="${icon} *Harness quickstart* ${label} · \`${III_CHANNEL}\` · GLM ${message_check} · ${duration} · <${RUN_URL}|view run>"
update_payload=$(
# shellcheck disable=SC2016
jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg ts "$SLACK_MESSAGE_TS" \
--arg text "$root_text" \
'{channel: $channel, ts: $ts, text: $text}'
)
thread_payload=$(
# shellcheck disable=SC2016
jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg ts "$SLACK_MESSAGE_TS" \
--arg text "$detail" \
'{channel: $channel, thread_ts: $ts, text: $text}'
)

failed=0
slack_call chat.update "$update_payload" || failed=1
slack_call chat.postMessage "$thread_payload" || failed=1
exit "$failed"

- name: Upload terminal recording to Slack thread
if: ${{ always() && steps.slack_start.outputs.ts != '' && steps.slack_start.outputs.channel_id != '' }}
continue-on-error: true
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ steps.slack_start.outputs.channel_id }}
SLACK_MESSAGE_TS: ${{ steps.slack_start.outputs.ts }}
run: |
set -uo pipefail

recording=target/harness-quickstart/quickstart.mp4
if [[ ! -s "$recording" ]]; then
echo "No terminal recording was produced; skipping the Slack upload."
exit 0
fi
filename=$(basename "$recording")
length=$(wc -c <"$recording" | tr -d '[:space:]')

# Slack external upload flow: reserve a URL, send the bytes, then
# attach the file to the run's thread. Needs the files:write scope.
ticket=$(curl -sS --retry 3 --retry-all-errors \
-X POST https://slack.com/api/files.getUploadURLExternal \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
--data-urlencode "filename=${filename}" \
--data-urlencode "length=${length}")
if ! jq -e '.ok == true' <<<"$ticket" >/dev/null 2>&1; then
error=$(jq -r '.error // "invalid_response"' <<<"$ticket" 2>/dev/null || printf 'invalid_response')
echo "::warning::Slack files.getUploadURLExternal failed: $error (is the files:write scope granted?)"
exit 1
fi
upload_url=$(jq -er '.upload_url' <<<"$ticket")
file_id=$(jq -er '.file_id' <<<"$ticket")

if ! curl -sS --fail --retry 3 --retry-all-errors \
-X POST "$upload_url" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$recording" >/dev/null; then
echo "::warning::Slack recording upload failed"
exit 1
fi

complete_payload=$(jq -n \
--arg channel "$SLACK_CHANNEL_ID" \
--arg ts "$SLACK_MESSAGE_TS" \
--arg id "$file_id" \
--arg title "Harness quickstart terminal recording.mp4" \
'{
channel_id: $channel,
thread_ts: $ts,
initial_comment: "🎬 Terminal recording of this run",
files: [{id: $id, title: $title}]
}')
response=$(curl -sS --retry 3 --retry-all-errors \
-X POST https://slack.com/api/files.completeUploadExternal \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$complete_payload")
if ! jq -e '.ok == true' <<<"$response" >/dev/null 2>&1; then
error=$(jq -r '.error // "invalid_response"' <<<"$response" 2>/dev/null || printf 'invalid_response')
echo "::warning::Slack files.completeUploadExternal failed: $error"
exit 1
fi
echo "Uploaded ${filename} (${length} bytes) to the Slack thread."
Loading
Loading