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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Custom-port credential errors now include a ready-to-run `git credential fill` verification command and a link to the auth troubleshooting docs, so users can diagnose miskeyed helpers without guessing. (closes #799)
- `apm install` now shows a recovery hint (`apm install --no-policy`) when the `required-packages-deployed` policy check fails, so users know how to unblock without hunting for the flag. (closes #1314)
- `apm install -g` now deploys `instructions` primitives for the Copilot target by concatenating all `*.instructions.md` files from each installed package into `~/.copilot/copilot-instructions.md`, the single file Copilot CLI reads at user scope. Previously this primitive type was silently skipped for global installs. Each package's contribution is wrapped in an HTML provenance comment so the file is auditable and multi-package installs accumulate correctly. (closes #650)
- `apm compile --target copilot` (and `agents`) no longer writes instructions into `AGENTS.md` when `apm install` has already deployed them to `.github/instructions/`, eliminating duplicate context that Copilot would read from both locations. Mirrors the equivalent dedup behaviour that was already in place for the Claude path (`.claude/rules/`). (closes #1550, refs #1445)
Expand Down
8 changes: 7 additions & 1 deletion docs/src/content/docs/getting-started/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,13 @@ For self-hosted Git instances on non-standard ports (e.g. Bitbucket Datacenter o
| `libsecret` (Linux) | Yes (port in URI) |
| `gh auth git-credential` | No -- but only used for GitHub hosts, which do not use custom ports |

If APM resolves the wrong credential for a custom-port host, confirm your helper keys by `host:port`; otherwise either switch helpers or store credentials under fully qualified `https://<host>:<port>/` URLs.
If APM resolves the wrong credential for a custom-port host, confirm your helper keys by `host:port` using the helper-agnostic verification command:

```sh
printf 'protocol=https\nhost=<host>:<port>\n\n' | git credential fill
```

This reproduces exactly what APM sends to the credential helper. If the returned `username`/`password` are wrong or empty, either switch helpers or store credentials under a fully qualified `https://<host>:<port>/` URL.

### SSH connection hangs on corporate/VPN networks

Expand Down
7 changes: 7 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ backend:
| `libsecret` (Linux) | Yes (port in URI) |
| `gh auth git-credential` | No -- but only used for GitHub hosts, which do not use custom ports |

To verify what your helper returns for a custom-port host, use the
helper-agnostic command APM itself calls:

```sh
printf 'protocol=https\nhost=<host>:<port>\n\n' | git credential fill
```

If APM resolves the wrong credential for a custom-port host, confirm your
helper keys by `host:port`; otherwise either switch helpers or store the
credential under a fully qualified `https://<host>:<port>/` URL.
Expand Down
11 changes: 9 additions & 2 deletions src/apm_cli/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@

T = TypeVar("T")

_PORT_CREDENTIAL_DOCS_URL = (
"https://microsoft.github.io/apm/getting-started/authentication/"
"#custom-port-hosts-and-per-port-credentials"
)


# ---------------------------------------------------------------------------
# Data classes
Expand Down Expand Up @@ -709,8 +714,10 @@ def build_error_context(
# return the wrong credential. Point the user at the concrete fix.
if host_info.port is not None:
lines.append(
f"[i] Host '{display}' -- verify your credential helper stores per-port entries "
f"(some helpers key by host only)."
f"[i] Host '{display}' -- this helper may key by host only.\n"
f" Verify with: printf 'protocol=https\\nhost={display}\\n\\n'"
f" | git credential fill\n"
f" Docs: {_PORT_CREDENTIAL_DOCS_URL}"
)

lines.append("Run with --verbose for detailed auth diagnostics.")
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import patch
from urllib.parse import urlparse

import pytest

Expand Down Expand Up @@ -1227,6 +1228,33 @@ def test_port_hint_appears_when_port_set(self):
msg = resolver.build_error_context("bitbucket.corp.com", "clone", port=7999)
assert "per-port" in msg, f"Expected per-port hint when port is set, got:\n{msg}"

def test_port_hint_includes_credential_fill_command(self):
with patch.dict(os.environ, {}, clear=True):
with patch.object(GitHubTokenManager, "resolve_credential_from_git", return_value=None):
resolver = AuthResolver()
msg = resolver.build_error_context("bitbucket.corp.com", "clone", port=7999)
assert "git credential fill" in msg, (
f"Expected 'git credential fill' verification command in hint, got:\n{msg}"
)

def test_port_hint_includes_docs_url(self):
with patch.dict(os.environ, {}, clear=True):
with patch.object(GitHubTokenManager, "resolve_credential_from_git", return_value=None):
resolver = AuthResolver()
msg = resolver.build_error_context("bitbucket.corp.com", "clone", port=7999)
# Extract the docs URL from the hint and validate its components with urlparse
# (substring URL assertions are prohibited; see tests.instructions.md)
url_line = next(
(line for line in msg.splitlines() if "microsoft.github.io/apm" in line), None
)
assert url_line is not None, f"Expected docs URL line in hint, got:\n{msg}"
url = url_line.split()[-1]
parsed = urlparse(url)
assert parsed.hostname == "microsoft.github.io", f"Unexpected hostname: {parsed.hostname}"
assert parsed.fragment == "custom-port-hosts-and-per-port-credentials", (
f"Unexpected fragment: {parsed.fragment}"
)
Comment on lines +1240 to +1256

def test_no_port_hint_when_port_missing(self):
with patch.dict(os.environ, {}, clear=True):
with patch.object(GitHubTokenManager, "resolve_credential_from_git", return_value=None):
Expand Down
Loading