Skip to content

Commit b150164

Browse files
committed
fix(ci): simplify API deployment version check and extend timeout
- Simplified the API deployment verification step in the GitHub workflow to use `git describe --tags` as the single source of truth for the expected version. - Extended the timeout for API readiness check from 5 minutes to 15 minutes to accommodate cold starts. - Minor cleanup and comments added for clarity in the deployment workflow. - Updated GitAgent.py to improve staging logic and added tests for git_add behavior. Test plan: - Verified the workflow changes locally and in CI runs. - Confirmed that the new timeout allows for longer cold starts without failure. - Ran unit tests for GitAgent modifications and confirmed all pass.
1 parent 602af54 commit b150164

3 files changed

Lines changed: 98 additions & 45 deletions

File tree

.github/workflows/deploy_api.yml

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -100,54 +100,30 @@ jobs:
100100
- name: Verify API deployment and version
101101
run: |
102102
echo "🔍 Verifying API deployment with expected version..."
103-
103+
104104
# Get current commit hash
105105
CURRENT_COMMIT=$(git rev-parse HEAD)
106106
CURRENT_COMMIT_SHORT=$(git rev-parse --short HEAD)
107107
echo "Current commit (full): $CURRENT_COMMIT"
108108
echo "Current commit (short): $CURRENT_COMMIT_SHORT"
109-
110-
# Debug: Get all releases and their target commits
111-
echo "🔍 Fetching releases from GitHub API..."
112-
RELEASES_JSON=$(curl -s -H "Authorization: token ${{ secrets.ACCESS_TOKEN }}" \
113-
"https://api.github.com/repos/${{ github.repository }}/releases")
114-
115-
echo "📋 Available releases and their target commits (ordered by date):"
116-
echo "$RELEASES_JSON" | jq -r '.[] | "\(.tag_name) -> \(.target_commitish) (created: \(.created_at))"' | head -10
117-
118-
# Search for release that points to current commit
119-
# Try multiple matching strategies
120-
EXPECTED_VERSION=$(echo "$RELEASES_JSON" | \
121-
jq -r --arg commit "$CURRENT_COMMIT" --arg short_commit "$CURRENT_COMMIT_SHORT" \
122-
'.[] | select(.target_commitish == $commit or .target_commitish == $short_commit or .target_commitish == ("refs/heads/" + $commit) or .target_commitish == ("refs/heads/" + $short_commit)) | .tag_name' | \
123-
head -1)
124-
125-
# If still not found, try matching against the default branch
126-
if [ -z "$EXPECTED_VERSION" ] || [ "$EXPECTED_VERSION" = "null" ]; then
127-
echo "🔄 Trying to match against default branch releases..."
128-
# Get the most recent release targeting main/master, sorted by creation date
129-
EXPECTED_VERSION=$(echo "$RELEASES_JSON" | \
130-
jq -r 'sort_by(.created_at) | reverse | .[] | select(.target_commitish == "main" or .target_commitish == "master" or .target_commitish == "") | .tag_name' | \
131-
head -1)
132-
echo "📌 Selected most recent release targeting main branch: $EXPECTED_VERSION"
133-
fi
134-
135-
# Fallback to git describe if no release found
136-
if [ -z "$EXPECTED_VERSION" ] || [ "$EXPECTED_VERSION" = "null" ]; then
137-
EXPECTED_VERSION=$(git describe --tags 2>/dev/null || echo "v0.0.1")
138-
echo "❌ No release found for commit, using git describe: $EXPECTED_VERSION"
139-
else
140-
echo "✅ Found release for commit: $EXPECTED_VERSION"
141-
fi
142-
109+
110+
# The API reports its version via get_git_tag() -> the VERSION file baked
111+
# into the image at build time from `git describe --tags`
112+
# (see .deploy/docker/images/Dockerfile.linux.x86_64). Use the exact same
113+
# source of truth here so the strings can actually match.
114+
EXPECTED_VERSION=$(git describe --tags 2>/dev/null || echo "unknown")
115+
echo "✅ Expected version (git describe --tags): $EXPECTED_VERSION"
116+
143117
# API URL based on registry name
144118
API_URL="https://${{ env.REGISTRY_NAME }}.default.space.naas.ai"
145119
OPENAPI_URL="$API_URL/openapi.json"
146-
120+
147121
echo "Checking API at: $OPENAPI_URL"
148-
149-
# Wait up to 5 minutes for API to be ready with correct version
150-
TIMEOUT=300 # 5 minutes
122+
123+
# Wait for API to be ready with correct version. A fresh space
124+
# create/update is a cold start (image pull + app boot + DNS/TLS) that
125+
# routinely takes longer than 5 minutes, so allow up to 15.
126+
TIMEOUT=900 # 15 minutes
151127
INTERVAL=10 # Check every 10 seconds
152128
ELAPSED=0
153129

libs/naas-abi-marketplace/naas_abi_marketplace/applications/git/agents/GitAgent.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class GitAgent(Agent):
5353
6) If the user also asked to open/update a PR, continue with **Path PR-only** steps 2–4.
5454
5555
Constraints:
56+
- Only stage and commit files that were changed as part of the requested work. NEVER stage untracked or unrelated files (e.g. scratch notes, generated markdown like `_KICKSTART.md`). Do not use `git add .` or `git add -A`; pass explicit paths to `git_add`, which skips untracked/unchanged paths automatically.
5657
- Do NOT use destructive git operations (no force push, no hard reset).
5758
- Keep PR body concise: include Summary + Test plan.
5859
- Before any push, the branch must be up to date with origin. `git_push` enforces this by running `git pull` first when the branch exists on origin; never bypass it.
@@ -121,16 +122,41 @@ def git_log(limit: int = 10) -> str:
121122
"Stage files for the next commit (git add). Provide the list of "
122123
"paths to stage. Use this before `git_commit` when the files you "
123124
"want to commit are not yet staged (e.g. lockfiles modified by "
124-
"tooling)."
125+
"tooling). Only paths that are actual changes to tracked files "
126+
"are staged; untracked and unchanged paths are skipped so that "
127+
"unrelated files are never committed."
125128
)
126129
)
127130
def git_add(paths: list[str]) -> str:
128131
if not paths:
129132
return "No paths provided to stage."
130-
code, output = _run_allow_fail(["git", "add", "--", *paths])
131-
if code != 0:
132-
raise RuntimeError(f"git add failed:\n{output}")
133-
return f"Staged: {', '.join(paths)}"
133+
134+
to_stage: list[str] = []
135+
skipped: list[str] = []
136+
for path in paths:
137+
# Porcelain status for this path: empty => no change;
138+
# every line starting with "??" => untracked (a new file, not a
139+
# change to a tracked file).
140+
status = _run(["git", "status", "--porcelain", "--", path])
141+
lines = [line for line in status.splitlines() if line]
142+
if not lines:
143+
skipped.append(f"{path} (no changes)")
144+
elif all(line.startswith("??") for line in lines):
145+
skipped.append(f"{path} (untracked)")
146+
else:
147+
to_stage.append(path)
148+
149+
if to_stage:
150+
code, output = _run_allow_fail(["git", "add", "--", *to_stage])
151+
if code != 0:
152+
raise RuntimeError(f"git add failed:\n{output}")
153+
154+
parts = []
155+
if to_stage:
156+
parts.append(f"Staged: {', '.join(to_stage)}")
157+
if skipped:
158+
parts.append(f"Skipped (not a tracked change): {', '.join(skipped)}")
159+
return " | ".join(parts) if parts else "Nothing to stage."
134160

135161
@tool(description="Restore files to HEAD (discard working-tree changes)")
136162
def git_restore(paths: list[str]) -> str:

libs/naas-abi-marketplace/naas_abi_marketplace/applications/git/agents/GitAgent_test.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,27 @@ def test_git_commit_nothing_staged_is_recoverable(agent, tmp_path, monkeypatch):
6363
assert "git_add" in result, result
6464

6565

66+
def _commit_file(path, name, content):
67+
import subprocess
68+
69+
(path / name).write_text(content)
70+
subprocess.run(["git", "add", "--", name], cwd=path, check=True, capture_output=True)
71+
subprocess.run(
72+
["git", "commit", "-m", f"add {name}", "-n"],
73+
cwd=path,
74+
check=True,
75+
capture_output=True,
76+
)
77+
78+
6679
def test_git_add_then_commit(agent, tmp_path, monkeypatch):
6780
import subprocess
6881

6982
_init_git_repo(tmp_path)
7083
monkeypatch.chdir(tmp_path)
71-
(tmp_path / "uv.lock").write_text("locked\n")
84+
# uv.lock is a tracked file that then gets modified (the real scenario).
85+
_commit_file(tmp_path, "uv.lock", "locked\n")
86+
(tmp_path / "uv.lock").write_text("locked v2\n")
7287

7388
add_tool = agent._tools_by_name["git_add"]
7489
add_result = add_tool.invoke({"paths": ["uv.lock"]})
@@ -82,3 +97,39 @@ def test_git_add_then_commit(agent, tmp_path, monkeypatch):
8297
["git", "log", "--oneline"], cwd=tmp_path, capture_output=True, text=True
8398
).stdout
8499
assert "chore: update lockfile" in log, log
100+
101+
102+
def test_git_add_skips_untracked_files(agent, tmp_path, monkeypatch):
103+
import subprocess
104+
105+
_init_git_repo(tmp_path)
106+
monkeypatch.chdir(tmp_path)
107+
_commit_file(tmp_path, "uv.lock", "locked\n")
108+
# A modified tracked file plus an unrelated untracked scratch file.
109+
(tmp_path / "uv.lock").write_text("locked v2\n")
110+
(tmp_path / "_KICKSTART.md").write_text("scratch\n")
111+
112+
add_tool = agent._tools_by_name["git_add"]
113+
result = add_tool.invoke({"paths": ["uv.lock", "_KICKSTART.md"]})
114+
115+
assert "Staged: uv.lock" in result, result
116+
assert "_KICKSTART.md (untracked)" in result, result
117+
118+
staged = subprocess.run(
119+
["git", "diff", "--cached", "--name-only"],
120+
cwd=tmp_path,
121+
capture_output=True,
122+
text=True,
123+
).stdout
124+
assert "uv.lock" in staged, staged
125+
assert "_KICKSTART.md" not in staged, staged
126+
127+
128+
def test_git_add_skips_unchanged_files(agent, tmp_path, monkeypatch):
129+
_init_git_repo(tmp_path)
130+
monkeypatch.chdir(tmp_path)
131+
_commit_file(tmp_path, "uv.lock", "locked\n") # committed, now unchanged
132+
133+
add_tool = agent._tools_by_name["git_add"]
134+
result = add_tool.invoke({"paths": ["uv.lock"]})
135+
assert "no changes" in result, result

0 commit comments

Comments
 (0)