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
12 changes: 12 additions & 0 deletions .github/workflows/harness-quickstart.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
name: Harness quickstart

on:
workflow_call:
inputs:
channel:
description: Installer channel to validate
required: false
type: string
default: latest
secrets:
SLACK_BOT_TOKEN:
required: false
ZAI_API_KEY:
required: true
schedule:
- cron: "17 5 * * *"
workflow_dispatch:
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
experimental: ${{ steps.meta.outputs.experimental }}
web_bundle: ${{ steps.web.outputs.needed }}
interface_smoke: ${{ steps.smoke.outputs.interface_smoke }}
harness_smoke: ${{ steps.harness_smoke.outputs.enabled }}
steps:
- uses: actions/checkout@v5
with:
Expand Down Expand Up @@ -150,6 +151,29 @@ jobs:
echo "::notice::$WORKER opts out of interface collection; registry publish will be skipped"
fi

# The published Harness quickstart also covers every worker declared as
# a mandatory Harness dependency. Keep this derived from the manifest so
# the post-deploy smoke follows dependency changes automatically.
- name: Detect Harness quickstart smoke target
id: harness_smoke
env:
WORKER: ${{ steps.meta.outputs.worker }}
run: |
set -euo pipefail
enabled=$(WORKER="$WORKER" python3 - <<'PY'
import os
from pathlib import Path
import yaml

worker = os.environ["WORKER"]
manifest = yaml.safe_load(Path("harness/iii.worker.yaml").read_text())
dependencies = manifest.get("dependencies", {}) or {}
print("true" if worker == "harness" or worker in dependencies else "false")
PY
)
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
echo "::notice::Harness quickstart smoke target=$enabled worker=$WORKER"

# ──────────────────────────────────────────────────────────────
# Create the GitHub Release shell (skipped on dry runs).
# ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -241,6 +265,26 @@ jobs:
experimental: ${{ needs.setup.outputs.experimental }}
secrets: inherit

# Run the published Harness quickstart immediately after the registry
# deployment of Harness or one of its mandatory dependencies. The validator
# supports the stable release channels only.
quickstart:
name: Harness quickstart
needs: [setup, publish]
if: >-
${{
!failure() &&
!cancelled() &&
needs.setup.outputs.harness_smoke == 'true' &&
needs.setup.outputs.dry_run != 'true' &&
needs.publish.result == 'success' &&
(needs.setup.outputs.registry_tag == 'latest' || needs.setup.outputs.registry_tag == 'next')
}}
uses: ./.github/workflows/harness-quickstart.yml
with:
channel: ${{ needs.setup.outputs.registry_tag }}
secrets: inherit
Comment on lines +283 to +286

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass only the declared secrets to the reusable workflow.

secrets: inherit exposes every secret available to the release job. The quickstart contract requires only SLACK_BOT_TOKEN and ZAI_API_KEY. Pass those names explicitly.

Proposed fix
     with:
       channel: ${{ needs.setup.outputs.registry_tag }}
-    secrets: inherit
+    secrets:
+      SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+      ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
📝 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
uses: ./.github/workflows/harness-quickstart.yml
with:
channel: ${{ needs.setup.outputs.registry_tag }}
secrets: inherit
uses: ./.github/workflows/harness-quickstart.yml
with:
channel: ${{ needs.setup.outputs.registry_tag }}
secrets:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 283-283: secrets unconditionally inherited by called workflow (secrets-inherit): this reusable workflow

(secrets-inherit)

🤖 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/release.yml around lines 283 - 286, Update the reusable
workflow invocation of harness-quickstart.yml to replace secrets: inherit with
an explicit secrets mapping containing only SLACK_BOT_TOKEN and ZAI_API_KEY,
preserving the existing channel input.

Source: Linters/SAST tools


# ──────────────────────────────────────────────────────────────
# Announce the release in Slack (#worker-releases). Terminal job:
# nothing depends on it, but a bad token or channel fails red
Expand Down
10 changes: 5 additions & 5 deletions harness/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions harness/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[package]
name = "harness"
version = "1.6.4"
version = "1.6.7"
edition = "2021"
publish = false

Expand All @@ -17,12 +17,14 @@ name = "harness"
path = "src/lib.rs"

[dependencies]
iii-sdk = "=0.21.6"
# 0.21.8 adds the reconnect reattach handshake. Older SDKs can lose this
# worker's registrations when the engine reloads another worker.
iii-sdk = "=0.21.8"
# Re-exports the same `opentelemetry` the SDK uses, so we can carry the active
# OTel context across `tokio::spawn` boundaries (shared `Context` global).
# 0.21.5 adds the live span-start push (`LiveSpanStartProcessor`), so the
# console renders `harness::turn step` while it runs instead of on close.
iii-helpers = "=0.21.6"
iii-helpers = "=0.21.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
2 changes: 1 addition & 1 deletion harness/iii.worker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: Thin durable turn loop that wires session-manager, context-manager,

dependencies:
state: "^0.21.3"
queue: "^0.2.0"
queue: "^0.21.2"
cron: "^0.21.0"
configuration: "^0.21.6"
iii-observability: "^0.21.6"
Expand Down
62 changes: 61 additions & 1 deletion harness/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const DEFINE_RETRY_BACKOFF_MS: u64 = 250;
/// Exhaustion is fatal to harness startup: accepting sends without this queue
/// would acknowledge work that cannot run durably.
pub async fn ensure_turn_queue(iii: &IIIClient) -> Result<(), String> {
let payload = turn_queue_definition();
let mut payload = turn_queue_definition();
let mut restart_redelivery_requested = true;
let mut last_error = String::new();

for attempt in 1..=DEFINE_ATTEMPTS {
Expand All @@ -42,6 +43,16 @@ pub async fn ensure_turn_queue(iii: &IIIClient) -> Result<(), String> {
}
Err(error) => {
last_error = error.to_string();
if restart_redelivery_requested && is_legacy_queue_schema_error(&last_error) {
tracing::warn!(
queue = TURN_QUEUE,
error = %last_error,
"queue worker does not support restart redelivery; retrying with the legacy queue schema"
);
payload = legacy_turn_queue_definition();
restart_redelivery_requested = false;
continue;
}
Comment on lines +46 to +55

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 | 🟠 Major | ⚡ Quick win

Allow the fallback to consume a retry after the primary budget is exhausted.

If the unsupported-field error occurs on the final DEFINE_ATTEMPTS iteration, this branch changes payload and executes continue, but the bounded loop has no next iteration. ensure_turn_queue then returns the schema error without sending legacy_turn_queue_definition().

The supplied harness/src/main.rs flow waits for queue provisioning before it announces readiness. Give the legacy fallback its own retry budget, and add a regression test that returns the schema error on the final primary attempt.

Also applies to: 132-162

🤖 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 `@harness/src/queue.rs` around lines 46 - 55, Update ensure_turn_queue around
the bounded primary retry loop so a legacy schema fallback triggered by
is_legacy_queue_schema_error can perform its own retry even when the primary
DEFINE_ATTEMPTS budget is exhausted. Ensure legacy_turn_queue_definition() is
actually submitted before returning the error, and add a regression test
covering the schema error on the final primary attempt and successful fallback
provisioning.

if attempt < DEFINE_ATTEMPTS {
tracing::warn!(
queue = TURN_QUEUE,
Expand All @@ -60,6 +71,10 @@ pub async fn ensure_turn_queue(iii: &IIIClient) -> Result<(), String> {
))
}

fn is_legacy_queue_schema_error(error: &str) -> bool {
error.contains("unknown field `redeliver_on_engine_restart`")
}

fn turn_queue_definition() -> Value {
json!({
"queue": TURN_QUEUE,
Expand All @@ -75,6 +90,20 @@ fn turn_queue_definition() -> Value {
})
}

fn legacy_turn_queue_definition() -> Value {
json!({
"queue": TURN_QUEUE,
"config": {
"type": "fifo",
"message_group_field": "session_id",
"concurrency": 10,
"max_retries": 3,
"backoff_ms": 1_000,
"poll_interval_ms": 100
}
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -100,4 +129,35 @@ mod tests {
.get("timeout_ms")
.is_none());
}

#[test]
fn legacy_turn_queue_definition_omits_restart_redelivery() {
assert_eq!(
legacy_turn_queue_definition(),
json!({
"queue": "harness-turn",
"config": {
"type": "fifo",
"message_group_field": "session_id",
"concurrency": 10,
"max_retries": 3,
"backoff_ms": 1_000,
"poll_interval_ms": 100
}
})
);
assert!(legacy_turn_queue_definition()["config"]
.get("redeliver_on_engine_restart")
.is_none());
}

#[test]
fn only_the_unsupported_restart_redelivery_field_triggers_fallback() {
assert!(is_legacy_queue_schema_error(
"serialization error: unknown field `redeliver_on_engine_restart`"
));
assert!(!is_legacy_queue_schema_error(
"serialization error: unknown field `message_group_field`"
));
}
}
2 changes: 1 addition & 1 deletion harness/tests/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ publish = false

[dependencies]
harness = { path = "../.." }
iii-sdk = "=0.21.6"
iii-sdk = "=0.21.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
2 changes: 1 addition & 1 deletion harness/tests/integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ name = "harness_integration"
path = "src/lib.rs"

[dependencies]
iii-sdk = "=0.21.6"
iii-sdk = "=0.21.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "signal", "net", "io-util"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
2 changes: 1 addition & 1 deletion harness/tests/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fn worker_manifest_uses_the_standalone_queue_worker() {

assert_eq!(
dependencies.get(serde_yaml::Value::String("queue".into())),
Some(&serde_yaml::Value::String("^0.2.0".into()))
Some(&serde_yaml::Value::String("^0.21.2".into()))
);
assert!(!dependencies.contains_key(serde_yaml::Value::String("iii-queue".into())));
}
Expand Down
Loading