Skip to content

feat(open): add service URL opener - #552

Merged
jongio merged 4 commits into
mainfrom
idea/open-service-url
Jul 28, 2026
Merged

feat(open): add service URL opener#552
jongio merged 4 commits into
mainfrom
idea/open-service-url

Conversation

@jongio

@jongio jongio commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Adds azd app open for opening a resolved local service URL. The command supports --path for routes and --print for scripts, and reports a clear error when a service has no URL source.

Closes #378

Validation: go test ./src/cmd/app/commands -run 'Test(ResolveOpenServiceURLFromCustomURL|ResolveOpenServiceURLFromPorts|ResolveOpenServiceURLMissingURL|JoinOpenURLPath)'

@jongio jongio added the idea Feature idea from the idea pipeline label Jul 24, 2026
@jongio jongio self-assigned this Jul 24, 2026
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🚀 Website Preview

Your PR preview was available here.

Preview has been cleaned up as the PR was closed.

github-actions Bot added a commit that referenced this pull request Jul 24, 2026

@wbreza wbreza left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review — feat(open): add service URL opener

Review outcome: Comment. No Critical/High defects; two Medium and several Low findings. Approving is not warranted, but nothing here is a blocker.

What this PR does

Adds azd app open <service> — resolves a local service URL (running-app state first, then azure.yaml) and opens it in the browser, with --path to append a route and --print to emit the URL instead of launching. Clean, well-tested command with sensible flag seams (openURL override for tests).

Verification note

I built and type-checked the branch against the real packages. All referenced symbols resolve correctly (serviceinfo.LocalServiceInfo.CustomURL, service.Service.Local -> LocalServiceConfig.CustomURL, completeServiceArgs, github.com/pkg/browser). URL scheme safety is already enforced upstream: ParseAzureYaml rejects non-http(s) schemes, so file:/javascript: customUrls from azure.yaml cannot reach browser.OpenURL. No security finding.


Medium

1. go mod tidy is not committed — preflight/CI will fail.
github.com/pkg/browser is now used directly, but cli/go.mod still lists it as // indirect. Running go mod tidy under cli/ moves it to the direct require block and modifies go.mod. A CI "tidy" gate (or the repo's Preflight Checks) will flag this. Run go mod tidy in cli/ and commit the updated go.mod/go.sum.

2. Docker container-only ports resolve to the wrong URL.
hostPortFromMapping treats a bare port (e.g. "80") as the host port and emits http://localhost:80. For Docker services a bare port is the container port with an auto-assigned host port — the canonical service.ParsePortSpec returns HostPort: 0 for that case. The hand-rolled parser also skips the Docker/non-Docker distinction and does no numeric validation. Prefer reusing Service.GetPortMappings() / GetPrimaryPort() and only emit a URL when a real host port is published (HostPort > 0). See inline comment.


Low / Nits

  • Swallowed azure.yaml parse error (resolveOpenServiceURL): parseErr is ignored, so a malformed azure.yaml surfaces as "No services are defined" rather than the real error. Consider surfacing non-"missing file" errors. (inline)
  • joinOpenURLPath mutates Path without resetting RawPath: pre-encoded base paths (e.g. .../a%2Fb) can be re-rendered as separate segments (.../a/b/...). Queries/fragments are preserved correctly. (inline)
  • Output channel: --print uses fmt.Println, while sibling commands route through internal/output. Minor consistency nit. (inline)
  • Ignored context.Context: resolveOpenServiceURL accepts ctx as _. Either thread it through the resolvers or drop the parameter.
  • Resolution flow: when a service is found in service-info but has no URL, the break falls through to re-parse azure.yaml (which GetServiceInfo already parsed). The foundService "Start it with 'azd app run'" hint is hard to reach for a service defined in azure.yaml. Worth simplifying to a single source of truth.
  • Test gaps: no coverage for --print, the not-found "Available services" list, hostPortFromMapping edge cases (3-part ip:host:container, /tcp suffix, bare port), or path-with-query. Add a table test for hostPortFromMapping in particular.

Nice, focused feature — the two Mediums (tidy + docker bare-port) are the ones worth addressing before merge.

Comment thread cli/src/cmd/app/commands/open.go Outdated
Comment thread cli/src/cmd/app/commands/open.go Outdated
Comment thread cli/src/cmd/app/commands/open.go Outdated
Comment thread cli/src/cmd/app/commands/open.go
@jongio

jongio commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

@wbreza thanks — genuinely useful review, and the two Mediums were both real. All four inline threads have replies with commit links; below are the items that only appeared in the review body.

Commits: 3270f12 (tidy) · f6faf86 (fixes) · dac97ac (tests)


1. go mod tidy is not committed — preflight/CI will fail.

Confirmed — Preflight was already red on exactly this:

Tidying go.mod and go.sum: go.mod or go.sum changed after running go mod tidy

Fixed in 3270f12: github.com/pkg/browser moved from the indirect block into the direct require block. go.sum was already correct so it is untouched, and re-running go mod tidy is now a verified no-op.


Ignored context.Context: resolveOpenServiceURL accepts ctx as _. Either thread it through or drop the parameter.

Dropped it in f6faf86. Neither serviceinfo.GetServiceInfo nor service.ParseAzureYaml takes a context.Context, so threading it would have meant changing two package APIs for no present benefit. runOpen no longer manufactures a context.Background() either.


Resolution flow: when a service is found in service-info but has no URL, the break falls through to re-parse azure.yaml (which GetServiceInfo already parsed). ... Worth simplifying to a single source of truth.

Partially agreed, and restructured in f6faf86 — but the fall-through itself has to stay.

I checked serviceinfo.mergeServiceInfo: it copies local.customUrl out of azure.yaml, but it never derives a URL from ports:. So for a service that isn't running, azure.yaml is the only source that can produce a ports-based URL — collapsing to a single source would break azd app open api for any stopped service configured with ports: (and TestResolveOpenServiceURLFromPorts with it).

What was genuinely broken, and is now fixed:

  • the service-info loop no longer breaks early, so names is fully populated before it feeds the "Available services" list — previously a match halfway through truncated it;
  • both sources feed one found flag and one "no known URL" error, so the Start it with 'azd app run' hint is reachable again. You were right that it was effectively dead: the azure.yaml branch returned its own error first for any service defined there, which is essentially all of them;
  • the azure.yaml parse moved after the running-state lookup, so a running service still resolves even when azure.yaml is malformed.

Test gaps: no coverage for --print, the not-found "Available services" list, hostPortFromMapping edge cases (3-part ip:host:container, /tcp suffix, bare port), or path-with-query.

All added in dac97ac, and the file is now table-driven testify per AGENTS.md:

Test Covers
TestRunOpenPrintWritesURLWithoutBrowser --print writes the URL and does not launch a browser
TestRunOpenLaunchesBrowser the inverse — browser receives the resolved URL, stdout stays empty
TestResolveOpenServiceURLNotFoundListsServices asserts Available services: api, web
TestOpenURLFromService 11 cases replacing the hostPortFromMapping gap: docker bare port, docker explicit, bind IP, IPv6, /tcp, /udp, malformed spec, multi-port precedence
TestJoinOpenURLPath 8 cases: query, fragment, escaped separator, trailing slash, empty path, both error paths
TestResolveOpenServiceURLSurfacesParseError malformed azure.yaml reports the parse failure

One correction, detailed in the --print thread: there is no internal/output package in this repo and no sibling command uses a shared output helper — fmt.Println was the existing convention, not a deviation. I still moved --print to cmd.OutOrStdout(), because it's the better call and it's what made the --print test possible.

Local verification: go build ./... ✅ · go test ./src/cmd/app/commands/ (full package) ✅ · golangci-lint run ./src/cmd/app/commands/... → 0 issues ✅ · go mod tidy → no-op ✅


Edited: the branch was rebased onto main to resolve a cli/go.mod conflict with a prometheus/client_golang bump that landed on main. Commit links above point at the post-rebase SHAs. Resolution took main's dependency set plus the direct pkg/browser entry; go mod tidy, go build ./..., the full commands package tests and golangci-lint were all re-run green on the rebased tree.

@jongio
jongio requested a review from wbreza July 28, 2026 05:24

@wbreza wbreza left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review (re-review) — feat(open): add service URL opener

Review outcome: Approve. All findings from my previous review have been resolved, and the three follow-up commits introduce no new Critical/High/Medium issues. Verified by building and running the tests against the new head.

Verification

I built the commands package and ran the suite in an isolated worktree at ac7154e:

  • go build ./src/cmd/app/commands/ — pass
  • go vet ./src/cmd/app/commands/ — pass
  • go test ./src/cmd/app/commands/ -run Open -v18/18 pass

(The repo-wide dashboard //go:embed dist build-order artifact is pre-existing and unrelated to this PR.)

Prior findings — all addressed

  • Medium — go mod tidy: pkg/browser is now a committed direct dependency (ebacfdfe). ✅
  • Medium — docker container-only ports: publishedHostPort now defers to the canonical svc.GetPortMappings(), skips HostPort <= 0 (so a docker bare port like "80" yields no URL instead of http://localhost:80), and filters non-TCP protocols. ✅
  • Low — swallowed azure.yaml parse error: new parseOpenAzureYaml uses detector.FindAzureYaml, returns (nil, nil) for a missing file, and surfaces real parse errors (TestResolveOpenServiceURLSurfacesParseError). ✅
  • Low — RawPath staleness in joinOpenURLPath: parsed.RawPath is now cleared after updating Path, with a test asserting re-encoding. ✅
  • Nit — output channel: --print now writes via fmt.Fprintln(cmd.OutOrStdout(), …) with write-error handling. ✅
  • Nit — ignored context: the unused context.Context parameter was removed. ✅
  • Test gaps: excellent table-driven coverage added — TestOpenURLFromService (11 cases incl. docker bare-port, UDP, IPv6/bind-IP, protocol suffix, malformed spec), TestJoinOpenURLPath (query/fragment/re-encode/error cases), --print and browser-launch seams, not-found service listing, and parse-error surfacing. ✅

Optional (non-blocking) nits

  • publishedHostPort discards the bool from GetPortMappings() — intentional and correct, since iterating an empty slice is safe.
  • azure.yaml is parsed twice (once inside GetServiceInfo, once in parseOpenAzureYaml) — minor redundancy kept for clearer error surfacing; fine as-is.
  • Running-state URLs from bestOpenURL aren't scheme-validated the way the azure.yaml path is (validated upstream by ParseAzureYaml); negligible in a local-dev/localhost context.

Clean, well-tested feature that fully incorporates the review feedback. Nice work.

jongio and others added 4 commits July 27, 2026 22:31
Closes #378

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 340b4dd3-b4b3-4f1a-9163-66b16d96fa81
pkg/browser is now imported directly by the open command, so it belongs in the direct require block. Preflight's tidy gate was failing on this.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1bbad110-6dc8-4b15-abdd-576ee731eeaa
…rrors

Use service.GetPortMappings so port specs go through the canonical parser.
A Docker container-only port such as "80" has an auto-assigned host port, so
it no longer resolves to http://localhost:80. Bind IPs, IPv6 binds, protocol
suffixes and malformed specs are now handled consistently, and non-TCP
mappings are skipped.

Parse azure.yaml explicitly after the running-state lookup so a malformed
file reports the real error instead of "service not found", while a project
with no azure.yaml still reports "not found".

Clear url.URL.RawPath after joining --path so String() re-encodes from the
updated Path.

Route --print through cmd.OutOrStdout and check the write error, and drop
the unused context.Context parameter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1bbad110-6dc8-4b15-abdd-576ee731eeaa
Convert to table-driven testify tests per AGENTS.md and close the gaps
called out in review: --print output, browser launch, the not-found
"Available services" list, malformed azure.yaml, port mapping edge cases,
and path-with-query/fragment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1bbad110-6dc8-4b15-abdd-576ee731eeaa
@jongio
jongio force-pushed the idea/open-service-url branch from ac7154e to dac97ac Compare July 28, 2026 05:37
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@jongio
jongio requested a review from wbreza July 28, 2026 05:38

@wbreza wbreza left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review (re-review after rebase) — feat(open): add service URL opener

Review outcome: Approve. The only change since my previous approval is a rebase onto the latest main (pulling in ~20 merged feature PRs and dependency upgrades). The PR's own contribution is unchanged, and I re-verified it still builds and passes tests against the new base.

What changed since last review

  • Branch rebased onto latest main (new head dac97ac). The PR's true delta vs the current merge-base is the same four files I already approved: open.go (+215), open_test.go (+323), main.go (+1, registration), go.mod (+1/-1, direct pkg/browser).
  • open.go is byte-for-byte identical to the version approved at ac7154e — no source changes to the feature itself.

Re-verification against new main (isolated worktree at dac97ac)

  • go build ./src/cmd/app/commands/ — pass
  • go build ./src/internal/service/... ./src/internal/serviceinfo/... ./src/internal/detector/... — pass
  • go vet ./src/cmd/app/commands/ — pass
  • go test ./src/cmd/app/commands/ -run Open -vall Open tests pass (9 functions / 20 subtests)

The dependency upgrades and service/serviceinfo/detector changes on main introduced no API drift: GetPortMappings() ([]PortMapping, bool), PortMapping.HostPort/Protocol, FindAzureYaml, LocalServiceInfo.CustomURL/URL/Port, and LocalServiceConfig.CustomURL are all intact and compatible.

(The repo-wide dashboard //go:embed dist build-order artifact is pre-existing and unrelated to this PR.)

All findings from earlier rounds remain resolved. Clean rebase — good to merge.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.26087% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.91%. Comparing base (d071901) to head (dac97ac).

Files with missing lines Patch % Lines
cli/src/cmd/app/commands/open.go 78.26% 13 Missing and 12 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #552      +/-   ##
==========================================
+ Coverage   61.84%   61.91%   +0.07%     
==========================================
  Files         224      225       +1     
  Lines       29430    29545     +115     
==========================================
+ Hits        18202    18294      +92     
- Misses       9980     9991      +11     
- Partials     1248     1260      +12     
Flag Coverage Δ
unittests 61.91% <78.26%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cli/src/cmd/app/commands/open.go 78.26% <78.26%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Test This PR

A preview build (0.20.0-pr552) is ready for testing!

🌐 Website Preview

Live Preview: https://jongio.github.io/azd-app/pr/552/

One-Line Install (Recommended)

PowerShell (Windows):

iex "& { $(irm https://raw.githubusercontent.com/jongio/azd-app/main/cli/scripts/install-pr.ps1) } -PrNumber 552 -Version 0.20.0-pr552"

Bash (macOS/Linux):

curl -fsSL https://raw.githubusercontent.com/jongio/azd-app/main/cli/scripts/install-pr.sh | bash -s 552 0.20.0-pr552

Uninstall

When you're done testing:

PowerShell (Windows):

iex "& { $(irm https://raw.githubusercontent.com/jongio/azd-app/main/cli/scripts/uninstall-pr.ps1) } -PrNumber 552"

Bash (macOS/Linux):

curl -fsSL https://raw.githubusercontent.com/jongio/azd-app/main/cli/scripts/uninstall-pr.sh | bash -s 552

Build Info:

What to Test:
Please review the PR description and test the changes described there.

@jongio
jongio merged commit a9703b2 into main Jul 28, 2026
20 checks passed
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
jongio added a commit that referenced this pull request Jul 29, 2026
The 15 commits that landed on main while this branch was open added CLI
surface the docs gate correctly rejected. Resolve the cli-reference.md
conflict by keeping both sides, then backfill everything the gate found.

Commands documented:
- open (#552), including cli/docs/commands/open.md and the URL resolution order
- website reference pages for hooks (#515) and remove (#511), which shipped
  without generated pages

Flags documented:
- run --env (#498), run --no-deps (#544)
- logs --summary (#491), logs --min-level (#527), logs --no-timestamps (#561)
- status --exit-code (#563)
- env --prefix (#490)

Also fix the gofumpt failure in env_test.go that currently breaks
mage preflight on main.

mage preflight passes clean. Docs gate covers 31 commands, website
validation covers 27.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

idea Feature idea from the idea pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an open command for service URLs

2 participants