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
24 changes: 24 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ inputs:
description: 'Session inactivity timeout in seconds — auto-stops after this duration of inactivity'
required: false
default: '0'
stop-on-run-finished:
description: 'Stop the session automatically when the agent finishes its run'
required: false
default: 'false'
model:
description: 'Model override (e.g. claude-sonnet-4-20250514)'
required: false
Expand Down Expand Up @@ -75,6 +79,9 @@ outputs:
session-uid:
description: 'Created session UID'
value: ${{ steps.run.outputs.session-uid }}
session-url:
description: 'URL to the session in the Ambient UI'
value: ${{ steps.run.outputs.session-url }}
session-phase:
description: 'Final session phase (only set when wait is true)'
value: ${{ steps.run.outputs.session-phase }}
Expand Down Expand Up @@ -102,6 +109,7 @@ runs:
INPUT_LABELS: ${{ inputs.labels }}
INPUT_ENV_VARS: ${{ inputs.environment-variables }}
INPUT_TIMEOUT: ${{ inputs.timeout }}
INPUT_STOP_ON_RUN_FINISHED: ${{ inputs.stop-on-run-finished }}
INPUT_MODEL: ${{ inputs.model }}
INPUT_WORKFLOW: ${{ inputs.workflow }}
INPUT_WAIT: ${{ inputs.wait }}
Expand Down Expand Up @@ -149,8 +157,14 @@ runs:
if [ "$INPUT_WAIT" = "true" ]; then
ARGS+=(--wait)
fi
if [ "$INPUT_STOP_ON_RUN_FINISHED" = "true" ]; then
ARGS+=(--stop-on-run-finished)
fi
if [ -n "$INPUT_SESSION_NAME" ]; then
ARGS+=(--session-name "$INPUT_SESSION_NAME")
# Pre-set session-url for reuse case (output file may not have it)
BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||')
echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${INPUT_SESSION_NAME}" >> "$GITHUB_OUTPUT"
Comment on lines +165 to +167

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "action.yml" -type f

Repository: ambient-code/ambient-action

Length of output: 83


🏁 Script executed:

# First find the file
fd -type f -name "action.yml" | head -1 | xargs wc -l

Repository: ambient-code/ambient-action

Length of output: 260


🏁 Script executed:

# Read the file to see the context around lines 165-167 and related sections
fd -type f -name "action.yml" | head -1 | xargs cat -n | sed -n '155,210p'

Repository: ambient-code/ambient-action

Length of output: 241


🏁 Script executed:

cat -n action.yml | sed -n '155,210p'

Repository: ambient-code/ambient-action

Length of output: 2736


🏁 Script executed:

cat -n action.yml | sed -n '1,30p'

Repository: ambient-code/ambient-action

Length of output: 1105


🏁 Script executed:

cat -n action.yml | sed -n '173,180p'

Repository: ambient-code/ambient-action

Length of output: 722


🏁 Script executed:

# Check if the Python script file exists and understand its purpose
find . -name "create_session.py" -type f | head -1

Repository: ambient-code/ambient-action

Length of output: 90


🏁 Script executed:

cat -n action.yml | sed -n '100,180p'

Repository: ambient-code/ambient-action

Length of output: 4086


Move session-url output after Python execution completes and use safe writes for $GITHUB_OUTPUT.

Currently, session-url is emitted at line 167 before the Python command executes at line 173. If Python fails and no output file is created, the pre-emitted URL persists in $GITHUB_OUTPUT while other fields (session-name, session-phase) indicate failure. This creates inconsistent, misleading output for downstream workflows.

Additionally, direct echo key=value writes at lines 167, 181–183, 188, 190, and 201 are unsafe for special characters in $INPUT_API_URL, $INPUT_PROJECT, or $INPUT_SESSION_NAME. Use GitHub Actions' safe delimiter pattern (heredoc with GHEOF) for all output writes, capture the Python exit code, and emit outputs only after confirming success.

🔧 Proposed fix
         if [ -n "$INPUT_SESSION_NAME" ]; then
           ARGS+=(--session-name "$INPUT_SESSION_NAME")
-          # Pre-set session-url for reuse case (output file may not have it)
-          BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||')
-          echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${INPUT_SESSION_NAME}" >> "$GITHUB_OUTPUT"
         fi
@@
-        python3 "${GITHUB_ACTION_PATH}/create_session.py" "${ARGS[@]}"
+        set +e
+        python3 "${GITHUB_ACTION_PATH}/create_session.py" "${ARGS[@]}"
+        PY_EXIT=$?
+        set -e
+
+        write_output_kv() {
+          local key="$1"
+          local value="$2"
+          {
+            echo "${key}<<GHEOF"
+            echo "$value"
+            echo "GHEOF"
+          } >> "$GITHUB_OUTPUT"
+        }
@@
-          echo "session-name=$SESSION_NAME" >> "$GITHUB_OUTPUT"
-          echo "session-uid=$SESSION_UID" >> "$GITHUB_OUTPUT"
-          echo "session-phase=$SESSION_PHASE" >> "$GITHUB_OUTPUT"
+          write_output_kv "session-name" "$SESSION_NAME"
+          write_output_kv "session-uid" "$SESSION_UID"
+          write_output_kv "session-phase" "$SESSION_PHASE"
@@
-          # Construct session URL from API URL
-          if [ -n "$SESSION_NAME" ]; then
-            BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||')
-            echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${SESSION_NAME}" >> "$GITHUB_OUTPUT"
-          else
-            echo "session-url=" >> "$GITHUB_OUTPUT"
-          fi
+          if [ -n "$SESSION_NAME" ]; then
+            BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||')
+            write_output_kv "session-url" "${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${SESSION_NAME}"
+          else
+            write_output_kv "session-url" ""
+          fi
@@
         else
-          echo "session-name=" >> "$GITHUB_OUTPUT"
-          echo "session-uid=" >> "$GITHUB_OUTPUT"
-          echo "session-url=" >> "$GITHUB_OUTPUT"
-          echo "session-phase=CreateFailed" >> "$GITHUB_OUTPUT"
-          echo "session-result=" >> "$GITHUB_OUTPUT"
+          write_output_kv "session-name" ""
+          write_output_kv "session-uid" ""
+          write_output_kv "session-url" ""
+          write_output_kv "session-phase" "CreateFailed"
+          write_output_kv "session-result" ""
         fi
+
+        exit "$PY_EXIT"

Also applies to: 173, 185–191, 201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@action.yml` around lines 165 - 167, Move the pre-emitted session-url output
so it is written only after the Python command completes successfully: run the
Python step, capture its exit code, and only if it succeeded write session-url
(and the other outputs referenced at the diff) to $GITHUB_OUTPUT; replace all
direct echo "key=value" writes (the session-url write and the writes at the
other mentioned locations) with GitHub Actions safe heredoc writes using a
delimiter like GHEOF to correctly handle special characters in $INPUT_API_URL,
$INPUT_PROJECT, and $INPUT_SESSION_NAME, and ensure outputs are not emitted when
the Python step fails.

fi
if [ "$INPUT_NO_VERIFY_SSL" = "true" ]; then
ARGS+=(--no-verify-ssl)
Expand All @@ -167,6 +181,15 @@ runs:
echo "session-name=$SESSION_NAME" >> "$GITHUB_OUTPUT"
echo "session-uid=$SESSION_UID" >> "$GITHUB_OUTPUT"
echo "session-phase=$SESSION_PHASE" >> "$GITHUB_OUTPUT"

# Construct session URL from API URL
if [ -n "$SESSION_NAME" ]; then
BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||')
echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${SESSION_NAME}" >> "$GITHUB_OUTPUT"
else
echo "session-url=" >> "$GITHUB_OUTPUT"
fi

{
echo "session-result<<GHEOF"
echo "$SESSION_RESULT"
Expand All @@ -175,6 +198,7 @@ runs:
else
echo "session-name=" >> "$GITHUB_OUTPUT"
echo "session-uid=" >> "$GITHUB_OUTPUT"
echo "session-url=" >> "$GITHUB_OUTPUT"
echo "session-phase=CreateFailed" >> "$GITHUB_OUTPUT"
echo "session-result=" >> "$GITHUB_OUTPUT"
fi
36 changes: 33 additions & 3 deletions create_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def create_session(
labels: dict | None = None,
env_vars: dict | None = None,
timeout: int = 0,
stop_on_run_finished: bool = False,
model: str = "",
verify_ssl: bool = True,
) -> dict | None:
Expand All @@ -192,6 +193,8 @@ def create_session(
body["environmentVariables"] = env_vars
if timeout:
body["inactivityTimeout"] = timeout
if stop_on_run_finished:
body["stopOnRunFinished"] = True
if model:
body["llmSettings"] = {"model": model}

Expand All @@ -218,6 +221,9 @@ def create_session(
return None


AGENT_DONE_STATUSES = {"idle", "waiting_input"}


def poll_session(
api_url: str,
api_token: str,
Expand All @@ -227,7 +233,12 @@ def poll_session(
timeout_minutes: int = 30,
verify_ssl: bool = True,
) -> dict:
"""Poll session status until a terminal phase is reached."""
"""Poll session until the agent is done or session reaches a terminal phase.

Exits when:
- Session phase is terminal (Completed, Error, Timeout, Stopped, Failed)
- Agent status is idle or waiting_input (agent finished its run, session still alive)
"""
url = f"{api_url.rstrip('/')}/projects/{project}/agentic-sessions/{session_name}"
headers = {"Authorization": f"Bearer {api_token}"}
deadline = time.time() + (timeout_minutes * 60) + 120
Expand All @@ -237,6 +248,7 @@ def poll_session(
f"(timeout: {timeout_minutes}m + 2m buffer)"
)

seen_working = False
while time.time() < deadline:
try:
resp = requests.get(
Expand All @@ -247,23 +259,39 @@ def poll_session(

status = data.get("status", {})
phase = status.get("phase", "Unknown")
agent_status = status.get("agentStatus", "")

logger.info(f"Session {session_name}: phase={phase}")
logger.info(f"Session {session_name}: phase={phase}, agentStatus={agent_status}")

if phase in TERMINAL_PHASES:
return {
"phase": phase,
"agentStatus": agent_status,
"result": status.get("result", ""),
"completionTime": status.get("completionTime", ""),
}

# Track if the agent has been active at least once
if agent_status and agent_status not in AGENT_DONE_STATUSES:
seen_working = True

# Only exit on idle/waiting_input after the agent has been working
if seen_working and agent_status in AGENT_DONE_STATUSES:
logger.info(f"Session {session_name}: agent is {agent_status}, done waiting")
return {
"phase": phase,
"agentStatus": agent_status,
"result": status.get("result", ""),
"completionTime": "",
}

except requests.RequestException as e:
logger.warning(f"Poll request failed (will retry): {e}")

time.sleep(poll_interval)

logger.error("Polling timed out waiting for session completion")
return {"phase": "PollTimeout", "result": "", "completionTime": ""}
return {"phase": "PollTimeout", "agentStatus": "", "result": "", "completionTime": ""}


def write_output(output_file: str, data: dict) -> None:
Expand Down Expand Up @@ -294,6 +322,7 @@ def main():
parser.add_argument("--labels", default="")
parser.add_argument("--env-vars", default="")
parser.add_argument("--timeout", type=int, default=0)
parser.add_argument("--stop-on-run-finished", action="store_true")
parser.add_argument("--model", default="")
parser.add_argument("--wait", action="store_true")
parser.add_argument("--poll-interval", type=int, default=15)
Expand Down Expand Up @@ -377,6 +406,7 @@ def main():
labels=labels,
env_vars=env_vars,
timeout=args.timeout,
stop_on_run_finished=args.stop_on_run_finished,
model=args.model,
verify_ssl=verify_ssl,
)
Expand Down
Loading