Skip to content

fix(deploy): surface API status code in latest-revision failure#21

Merged
facundofarias merged 2 commits into
mainfrom
fix/deploy-latest-revision-status-code
Jun 8, 2026
Merged

fix(deploy): surface API status code in latest-revision failure#21
facundofarias merged 2 commits into
mainfrom
fix/deploy-latest-revision-status-code

Conversation

@facundofarias

@facundofarias facundofarias commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Mixpanel keeps showing a steady "Could not fetch latest revision" bucket on dhq deploy (6 hits in the last 7d on v0.18.x). Today we can't tell from telemetry whether those failures are 404 (project repo not synced yet), 5xx (transient API), or something else — telemetry.SanitizeErrorMessage keeps only the first line of err.Error(), so the status code that lived in the Hint was getting stripped before it reached the dashboard.

  • internal/commands/deploy.go — when the primary /repository/latest_revision call returns an *sdk.APIError, embed (api: <code>) in the UserError.Message itself so it survives sanitization. Tailor the Hint by status: 404 → "repository may not be configured or synced; check dhq repos show"; 5xx → "DeployHQ API is having trouble, try again or specify --revision".
  • internal/commands/deploy_test.go — table-driven test for 404 / 500 / 503 that asserts on the first line of err.Error() (mirroring what telemetry does), so a regression putting the status code back into the Hint fails the test. Also covers the existing deployment-list fallback path so the new branch doesn't silently bypass it.

Why the assertion is shaped this way

Naively asserting err.Error() contains (api: 404) would also pass if the status code were only in the Hint, because UserError.Error() concatenates Message and Hint. The first-line assertion is what telemetry actually consumes.

Test plan

  • go test ./... — all packages green
  • golangci-lint run ./... — 0 issues
  • Verified the strengthened test fails when the production fix is reverted (git stash on deploy.go produces "Could not fetch latest revision" does not contain "(api: 503)")

Follow-ups (deliberately out of scope)

  • SanitizeErrorMessage still drops the Hint entirely; the broader fix needs a redaction-policy decision (see the existing memory note on hostname/account-name handling). This PR is the minimum to make the existing bucket actionable.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Deployment error messages now include the HTTP status code and provide more specific, scenario-tailored guidance (including distinct handling for 404, 5xx, and other 4xx cases). A fallback to the previous deployment revision is used when the primary lookup fails.
  • Tests
    • Expanded test coverage to verify status-code inclusion in error output, tailored hints, and the fallback behavior.

Mixpanel showed a steady "Could not fetch latest revision" bucket on
v0.18.x (6 hits last week) with no way to tell 404 (repo not synced)
from 5xx (transient API) — telemetry's SanitizeErrorMessage keeps only
the first line of err.Error(), so the status code that was buried in
the Hint never reached the dashboard.

- Embed (api: %d) in the Message itself when the primary API error is
  *sdk.APIError so the status code survives sanitization.
- Tailor the Hint by status: 404 points at repo configuration/sync;
  5xx points at a transient API issue.
- Add regression tests that mirror SanitizeErrorMessage's first-line
  behaviour to lock down where the status code must appear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 911fc7e8-ae65-4a45-81f9-df5aca769f6e

📥 Commits

Reviewing files that changed from the base of the PR and between 122336d and 4281506.

📒 Files selected for processing (2)
  • internal/commands/deploy.go
  • internal/commands/deploy_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/commands/deploy_test.go
  • internal/commands/deploy.go

Walkthrough

The PR improves error reporting in the deployment command's resolveLatestRevision function. It adds HTTP status code-aware error messages with tailored hints for 404 and 5xx failures, and introduces fallback behavior to the deployments list. Tests validate the status-code-specific messaging and fallback logic.

Changes

Error Handling Improvements

Layer / File(s) Summary
Imports to support API error inspection
internal/commands/deploy.go
Adds errors and net/http imports to enable detection of *sdk.APIError and branching on HTTP status codes.
Status-code-aware error messaging
internal/commands/deploy.go
Refactors resolveLatestRevision to use errors.As to detect *sdk.APIError, include api: <status> in the error's first line, and select hints for 404, 5xx, or other 4xx; non-API errors preserve the generic message while embedding the raw error in the hint. Also preserves fallback to extracting end_revision.ref from /deployments.
Error handling test coverage
internal/commands/deploy_test.go
Adds strings import and table-driven tests that assert the first line of err.Error() contains the (api: <status>) marker across multiple statuses (404/500/503/401/422) and a test verifying fallback returns end_revision.ref when the latest-revision call fails.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • deployhq/deployhq-cli#16: Both PRs inspect *sdk.APIError and HTTP status codes to produce error outcomes—main PR does this in resolveLatestRevision for deployment errors, while the retrieved PR does it in global error classification and auth handling.
  • deployhq/deployhq-cli#3: The main PR changes how resolveLatestRevision formats UserError messages with status codes on the first line, which feeds into the retrieved PR's telemetry error_message field that sanitizes the first line of err.Error().
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: surfacing API status codes in error messages for latest-revision failures, which is the core objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deploy-latest-revision-status-code

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 122336dfe8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +145
if errors.As(primaryErr, &apiErr) {
message = fmt.Sprintf("Could not fetch latest revision (api: %d)", apiErr.StatusCode)
switch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve API error details for non-404/5xx failures

When /repository/latest_revision returns an API error other than 404 or 5xx (for example 401/403 auth, 422 validation, or 429 rate limiting) and the deployments fallback has no usable revision, this branch now consumes all *sdk.APIErrors but leaves the generic Specify a revision hint unchanged. That regresses the previous output, which included primaryErr and told the user the actual cause; e.g. invalid credentials now show only Could not fetch latest revision (api: 401), and suggesting --revision will not fix the auth failure. Add a default case that preserves the API error text or tailor the remaining statuses.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/commands/deploy.go (1)

137-157: ⚡ Quick win

Status code correctly embedded in Message for telemetry.

The implementation properly addresses the telemetry requirement by placing the API status code in the Message field (line 144), ensuring it survives SanitizeErrorMessage's first-line extraction. The use of errors.As and status-specific hints (404 → config, 5xx → transient) is correct.

Minor: Consider adding an explicit `default:` case

While the current logic is correct (hint is already initialized on line 141), an explicit default case would make the intent clearer for other status codes (e.g., 400, 403):

 	switch {
 	case apiErr.StatusCode == http.StatusNotFound:
 		hint = "The project's repository may not be configured or synced yet. Check `dhq repos show -p <project>`, or specify --revision <sha>."
 	case apiErr.StatusCode >= 500:
 		hint = "DeployHQ API is having trouble. Try again in a moment, or specify --revision <sha>."
+	default:
+		// Use the generic hint initialized above (line 141)
 	}
🤖 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 `@internal/commands/deploy.go` around lines 137 - 157, Add an explicit default
branch to the switch on apiErr.StatusCode within the block that sets
message/hint (the variables message, hint, and apiErr in deploy.go) so that for
non-404/non-5xx status codes you clearly preserve or reassign the generic hint
(e.g., "Specify a revision with --revision <sha>") instead of relying on the
initialized value; update the switch in the code handling primaryErr to include
case apiErr.StatusCode == http.StatusNotFound, case apiErr.StatusCode >= 500,
and a default: that sets hint to the generic guidance.
internal/commands/deploy_test.go (1)

508-574: ⚡ Quick win

Excellent test coverage for telemetry-aware error messaging.

The table-driven test correctly validates that status codes appear on the first line of err.Error() (mirroring SanitizeErrorMessage behavior on line 567) and that hints are tailored by failure mode. The use of httptest.NewServer follows coding guidelines.

Recommended: Add test case for non-APIError path

The current tests cover *sdk.APIError responses (404, 500, 503), but lines 151-153 in deploy.go handle the case when primaryErr is not an APIError (e.g., network timeout, DNS failure). Consider adding a test case that triggers this path to ensure the fallback hint formatting is correct:

{
    name:           "non-API error → generic hint",
    // Simulate network-level error by closing server before request
    wantInMessage:  "Could not fetch latest revision",
    wantInHintPart: "API error:",
}

This would strengthen confidence that all error paths produce the expected telemetry output. Based on learnings: Use httptest.NewServer for recorded response shapes.

🤖 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 `@internal/commands/deploy_test.go` around lines 508 - 574, Add a table-driven
test case to TestResolveLatestRevision_EmbedsStatusCodeInMessage that exercises
the non-*sdk.APIError* branch in resolveLatestRevision (the primaryErr !=
APIError path handled in deploy.go around the primaryErr checks) by causing a
network-level failure (e.g., create an httptest.NewServer and close it before
calling newTestSDKClient/resolveLatestRevision or use an unreachable server
URL), then assert err.Error() contains the generic leading message ("Could not
fetch latest revision" or whatever the first-line text from
resolveLatestRevision is) and that the hint contains "API error:" (or the exact
hint text produced in that branch) so the test verifies both first-line message
and hint formatting for non-API errors.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@internal/commands/deploy_test.go`:
- Around line 508-574: Add a table-driven test case to
TestResolveLatestRevision_EmbedsStatusCodeInMessage that exercises the
non-*sdk.APIError* branch in resolveLatestRevision (the primaryErr != APIError
path handled in deploy.go around the primaryErr checks) by causing a
network-level failure (e.g., create an httptest.NewServer and close it before
calling newTestSDKClient/resolveLatestRevision or use an unreachable server
URL), then assert err.Error() contains the generic leading message ("Could not
fetch latest revision" or whatever the first-line text from
resolveLatestRevision is) and that the hint contains "API error:" (or the exact
hint text produced in that branch) so the test verifies both first-line message
and hint formatting for non-API errors.

In `@internal/commands/deploy.go`:
- Around line 137-157: Add an explicit default branch to the switch on
apiErr.StatusCode within the block that sets message/hint (the variables
message, hint, and apiErr in deploy.go) so that for non-404/non-5xx status codes
you clearly preserve or reassign the generic hint (e.g., "Specify a revision
with --revision <sha>") instead of relying on the initialized value; update the
switch in the code handling primaryErr to include case apiErr.StatusCode ==
http.StatusNotFound, case apiErr.StatusCode >= 500, and a default: that sets
hint to the generic guidance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e736157c-532e-42ec-824d-64091be6a3f0

📥 Commits

Reviewing files that changed from the base of the PR and between 82c1e4e and 122336d.

📒 Files selected for processing (2)
  • internal/commands/deploy.go
  • internal/commands/deploy_test.go

The previous version of the switch on apiErr.StatusCode handled 404
and 5xx with tailored hints but fell through to the generic "Specify
a revision with --revision <sha>" for everything else. That regresses
the pre-PR-#21 behaviour for 401/403/422/429: callers now see e.g.
"Could not fetch latest revision (api: 401)" with a "specify a
revision" hint, when the real fix is to re-auth.

- Add a default branch that embeds primaryErr's text in the hint,
  matching the non-APIError fallback below it.
- Extend the table test with 401 and 422 cases that assert the API
  error text survives into the hint (e.g. "deployhq api: 401",
  "validation failed").

Codex + CodeRabbit both flagged this on the PR review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@facundofarias facundofarias merged commit ede68ff into main Jun 8, 2026
3 checks passed
@facundofarias facundofarias deleted the fix/deploy-latest-revision-status-code branch June 8, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant