mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
fix: non-zero exit code when a workflow run ends failed or aborted (#2959)
* fix: non-zero exit code when a workflow run ends failed or aborted workflow run and workflow resume printed Status: failed (or emitted the --json payload) and exited 0, so scripts and CI could not rely on the process exit code. Map terminal outcomes: failed|aborted -> 1, completed|paused -> 0, on both the text and --json paths. The previous exit-0-on-failed behavior was pinned by test_workflow_run_failing_yaml_without_project; the pin is updated to the new contract. Fixes #2958 * test: portable exit-code step commands + cover resume failed->exit-1 Address review (#2959): replace non-portable run: 'true'/'false' with 'exit 0'/'exit 1' (Windows cmd.exe has no true/false builtins under shell=True), and add an end-to-end 'workflow resume' test asserting a resumed failed run exits non-zero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4964,3 +4964,95 @@ steps:
|
||||
asset_calls = [(url, h) for url, h in captured_urls if "releases/assets/" in url]
|
||||
assert len(asset_calls) >= 1
|
||||
assert asset_calls[0][1] == {"Accept": "application/octet-stream"}
|
||||
|
||||
|
||||
class TestWorkflowRunExitCodes:
|
||||
"""CLI-level tests for the run/resume process exit codes."""
|
||||
|
||||
_WF_OK = """
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "exit-ok"
|
||||
name: "Exit OK"
|
||||
version: "1.0.0"
|
||||
steps:
|
||||
- id: fine
|
||||
type: shell
|
||||
run: "exit 0"
|
||||
"""
|
||||
|
||||
_WF_FAIL = """
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "exit-fail"
|
||||
name: "Exit Fail"
|
||||
version: "1.0.0"
|
||||
steps:
|
||||
- id: boom
|
||||
type: shell
|
||||
run: "exit 1"
|
||||
"""
|
||||
|
||||
def _write(self, tmp_path, content):
|
||||
path = tmp_path / "wf.yml"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def test_run_completed_exits_zero(self, tmp_path, monkeypatch):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["workflow", "run", str(self._write(tmp_path, self._WF_OK))])
|
||||
assert result.exit_code == 0
|
||||
assert "Status: completed" in result.stdout
|
||||
|
||||
def test_run_failed_exits_nonzero(self, tmp_path, monkeypatch):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["workflow", "run", str(self._write(tmp_path, self._WF_FAIL))])
|
||||
assert "Status: failed" in result.stdout
|
||||
assert result.exit_code == 1
|
||||
|
||||
def test_run_failed_exits_nonzero_with_json(self, tmp_path, monkeypatch):
|
||||
import json as _json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["workflow", "run", str(self._write(tmp_path, self._WF_FAIL)), "--json"],
|
||||
)
|
||||
assert result.exit_code == 1, result.stdout
|
||||
payload = _json.loads(result.stdout)
|
||||
assert payload["status"] == "failed"
|
||||
|
||||
def test_resume_failed_run_exits_nonzero(self, tmp_path, monkeypatch):
|
||||
# End-to-end coverage for the `workflow resume` exit-code mapping:
|
||||
# resuming a run whose outcome is still `failed` must exit non-zero,
|
||||
# mirroring `workflow run`. Resume re-executes the failed step, which
|
||||
# fails again, so the resumed outcome stays `failed`.
|
||||
import json as _json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / ".specify").mkdir() # `workflow resume` requires a project
|
||||
runner = CliRunner()
|
||||
run = runner.invoke(
|
||||
app,
|
||||
["workflow", "run", str(self._write(tmp_path, self._WF_FAIL)), "--json"],
|
||||
)
|
||||
assert run.exit_code == 1, run.stdout
|
||||
run_id = _json.loads(run.stdout)["run_id"]
|
||||
|
||||
resumed = runner.invoke(app, ["workflow", "resume", run_id, "--json"])
|
||||
assert resumed.exit_code == 1, resumed.stdout
|
||||
payload = _json.loads(resumed.stdout)
|
||||
assert payload["status"] == "failed"
|
||||
|
||||
Reference in New Issue
Block a user