Skip to content

Commit e040584

Browse files
Merge branch 'main' into feat/1243
2 parents 65a9969 + 22ebb35 commit e040584

63 files changed

Lines changed: 4519 additions & 616 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,13 @@ jobs:
127127
retention-days: 30
128128
if-no-files-found: error
129129

130-
# Dogfood the two CI gates we ship and document to users:
131-
# - Gate A (consumer-side): `apm audit --ci` -- lockfile / install fidelity.
132-
# - Gate B (producer-side): regeneration drift -- did someone hand-edit
133-
# a regenerated file under .github/ without updating canonical .apm/?
130+
# Dogfood the audit-only CI gate we ship and document to users:
131+
# - Gate A (consumer-side): `apm audit --ci --no-drift` -- lockfile /
132+
# content-integrity check. setup-only: true keeps managed files
133+
# untouched so the SHA-256 content-integrity check can detect tampered
134+
# files. The drift check (install-replay comparison) is skipped
135+
# via --no-drift because there is no warm cache in a setup-only
136+
# run; content-integrity covers the same tamper signal.
134137
# See microsoft/apm#883 for context. Tier 1 (no secrets needed).
135138
apm-self-check:
136139
name: APM Self-Check
@@ -140,32 +143,30 @@ jobs:
140143
steps:
141144
- uses: actions/checkout@v4
142145

143-
# Installs the APM CLI (latest stable) and runs `apm install` against
144-
# this repo's apm.yml. Auto-detects target from the existing .github/
145-
# directory and re-integrates local .apm/ content, regenerating
146-
# .github/instructions/, .github/agents/, .github/skills/, etc.
147-
# Adds `apm` to PATH for subsequent steps.
146+
# Installs the APM CLI (latest stable) and adds `apm` to PATH.
147+
# setup-only: true skips `apm install` so managed files on disk are
148+
# not overwritten before the audit runs. This preserves the committed
149+
# state so content-integrity can detect any tampered file hashes.
148150
- uses: microsoft/apm-action@v1
151+
with:
152+
setup-only: true
149153

150154
# Gate A: lockfile / install fidelity (consumer-side).
151155
# Verifies every file in lockfile.deployed_files exists, ref consistency
152156
# between apm.yml and apm.lock.yaml, no orphan packages, and
153-
# content-integrity (hidden Unicode) on deployed package content.
154-
# Does NOT verify deployed-file content vs lockfile (see #684).
155-
- name: apm audit --ci
156-
run: apm audit --ci
157-
158-
# Gate B: regeneration drift (producer-side).
159-
# NOTE: Once `apm-action` ships a CLI version that includes the
160-
# default-on `apm audit` drift detection (issue #1071), this entire
161-
# step becomes redundant -- Gate A above already catches the same
162-
# divergence via install-replay. Keep this bash check until then as
163-
# a defense-in-depth fallback.
164-
#
165-
# The action's `apm install` step re-integrated local .apm/ into
166-
# .github/ via target auto-detection. If anything in the governed
167-
# integration directories changed, someone edited the regenerated
168-
# output without updating the canonical .apm/ source.
157+
# content-integrity (SHA-256 hashes against deployed_file_hashes in the
158+
# lockfile) on deployed package content. --no-drift skips the
159+
# install-replay because there is no warm cache (setup-only did not
160+
# run apm install); content-integrity still catches tampered files.
161+
- name: apm audit --ci --no-drift
162+
run: apm audit --ci --no-drift
163+
164+
# Gate B: regeneration drift (producer-side) -- legacy bash fallback.
165+
# NOTE: With setup-only: true this step is a guaranteed no-op.
166+
# apm install did not run and the working tree is unchanged, so the
167+
# git status check always finds nothing. It is kept so the pattern is
168+
# visible; a full install+audit workflow would rely on this step to
169+
# detect hand-edits to regenerated .github/ files.
169170
- name: Check APM integration drift (legacy bash fallback, see #1071)
170171
run: |
171172
if [ -n "$(git status --porcelain -- .github/ .claude/ .cursor/ .opencode/)" ]; then

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- Non-skill integrators (agent, instruction, prompt, command, hook script-copy) silently adopt byte-identical pre-existing files so a degraded `deployed_files=[]` lockfile no longer permanently blocks installs gated by `required-packages-deployed`. (#1313)
17+
- `apm audit` drift check now returns skip-with-info (`passed=True`) when the install cache is cold, instead of failing the audit; bare `apm audit` surfaces the skip reason on stderr so CI pipelines that have not yet run `apm install` are not incorrectly red-marked. (#1289)
18+
- `extends: org` now correctly layers `dependencies.require` and `dependencies.deny` from the parent policy when the child omits the `dependencies:` block entirely; `None` signals "no opinion" (transparent) while `[]` signals explicit override. (#1290)
19+
- CI self-check job now uses `setup-only: true` + `apm audit --ci --no-drift` so managed files are not overwritten by `apm install` before `content-integrity` runs; documented the audit-only CI pattern and the install-before-audit blind spot in the enterprise and CI/CD guides. (#1291)
1620
- Pin `Path.home()` under unit tests via a session-scoped autouse conftest fixture, fixing 56 Windows runner failures on the new `windows-2025-vs2026` GitHub-hosted image where `USERPROFILE`/`HOMEDRIVE`+`HOMEPATH` are not seeded for pytest workers; also patch the `_check_and_notify_updates` import binding in the disabled-self-update test so it no longer races on the version-check cache. (#1270)
1721
- `apm install` now works on macOS git 2.53.0 (Homebrew): bare-cache commands switch to `--git-dir` to satisfy the `safe.bareRepository=explicit` default; fetched SHAs are pinned as synthetic refs so `git clone --local --shared` no longer silently omits them. (#1268)
1822
- Set the unit-test hermetic HOME at conftest import time so a single xdist worker on the `windows-2025-vs2026` runner can no longer race fixture setup and re-trigger the 53 `Path.home()` failures the session-scoped autouse fixture was supposed to prevent. (#1271)
1923
- Override `Path.home()` itself in the root test conftest so the 46 remaining Windows `RuntimeError: Could not determine home directory` failures on xdist worker `gw2` cannot recur regardless of which conftest the worker imports first; per-test `monkeypatch.setenv("HOME", ...)` continues to work because the override consults env vars before falling back to the hermetic tmp dir. (#1272)
2024
- Retry the `apm mcp search` and `apm mcp show` integration tests on the documented "Could not reach MCP registry" transient (with backoff and a final skip) so a brief `api.mcp.github.com` outage no longer red-marks the Windows integration job. (#1274)
2125
- Also wrap `Path.expanduser()` in the root test conftest so the `windows-2025-vs2026` runner cannot raise `RuntimeError("Could not determine home directory.")` from `ntpath.expanduser` when production code (e.g. `install.package_resolution.user_scope_rejection_reason`) calls `Path("~/pkg").expanduser()`. Falls back to the hermetic tmp dir; assertions about `~/pkg` being absolute still hold. (#1276)
26+
- `apm install` from marketplaces registered on `*.ghe.com` (GHE Cloud) hosts now routes auth at the registered enterprise host instead of silently defaulting to `github.com` and failing with 401; the marketplace resolver backfills the enterprise host onto the canonical so downstream `DependencyReference.parse` recovers it, and the resulting `apm.yml` entry records the correct enterprise `git:` URL instead of `https://github.com/...`. (#1292)
27+
- `apm view --help` and the `view` row in `apm --help` now render in release binaries; PyInstaller's `optimize=2` was stripping `__doc__` from every Click command, and `view` was the only command that relied on its docstring instead of the explicit `help=` kwarg every other command defensively sets. Lowered the spec to `optimize=1` so asserts are still removed but docstrings survive, restoring Click's documented help-from-docstring fallback for all current and future commands. (#1298)
2228

2329
## [0.13.0] - 2026-05-11
2430

@@ -33,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3339
- `apm install --frozen` performs a CI-safe, read-only install that fails fast (exit 1) when `apm.lock.yaml` is missing or out of sync with `apm.yml`; mutually exclusive with `--update`. (#1244)
3440
- Zero-config private-package auth on github.com, `*.ghe.com`, and GHES when the `gh` CLI is logged in: APM uses `gh auth token --hostname <host>` before falling back to `git credential fill`. (#630)
3541
- GitLab marketplace and install support: `gitlab.com` and self-managed instances (via `GITLAB_HOST` / `APM_GITLAB_HOSTS`) use GitLab REST v4 for `marketplace.json` and raw file reads; nested group paths are disambiguated via object-form `git:` + `path:`. (#1149)
42+
- **Windows installer parity:** CI and GHES/air-gapped runners can **pin `VERSION`**, set **`GITHUB_URL`** (https only) and **`APM_REPO`**, and use **`APM_INSTALL_DIR`** like `install.sh`. Pinned installs verify the release **`.sha256`** unless **`APM_SKIP_CHECKSUM=1`** or **`-SkipChecksum`** (emergency only); `GITHUB_URL` drives GitHub.com (`api.github.com`) vs GHES (`{host}/api/v3`) for latest discovery. (#668)
3643
- Virtual subdirectory and raw-file packages now resolve from self-hosted Git services (Gitea, Gogs) via raw URL with API v1/v3 fallback. (#587)
3744
- `git: parent` lets packages in a git monorepo reference sibling paths via `{ git: parent, path: ... }` without repeating the full `git:` URL; the lockfile stores expanded host, repo, and resolved ref like every other virtual git dependency. (#1149)
3845
- `shared/apm.md` gh-aw workflow exposes a `target:` import input (default `all`) so consumer workflows can ship slim, single-harness bundles. (#1184)

build/apm.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ a = Analysis(
271271
win_private_assemblies=False,
272272
cipher=None,
273273
noarchive=False,
274-
optimize=2, # Python optimization level for smaller, faster binaries
274+
optimize=1, # -O: strip asserts; keep __doc__ so Click reads command help from docstrings (#1298)
275275
)
276276

277277
# Exclude bundled OpenSSL shared libraries on Linux.

docs/src/content/docs/enterprise/drift-detection.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ lockfile-exists -> ref-consistency -> deployed-files-present
4242
After the baseline passes, it replays the install in a scratch directory
4343
from the cache and diffs against the working tree to surface
4444
`unintegrated`, `modified`, and `orphaned` files. Pass `--no-drift` to
45-
skip the replay. With `--policy <source>` it also evaluates the
46-
discovered policy against the lockfile. Source:
47-
`src/apm_cli/commands/audit.py`, `src/apm_cli/policy/ci_checks.py`.
45+
skip the replay. When the install cache has not been warmed yet (fresh
46+
checkout before the first `apm install`), the drift check is skipped
47+
with an informational message rather than failing; run `apm install` to
48+
warm the cache and enable the check on the next run. With `--policy
49+
<source>` it also evaluates the discovered policy against the lockfile.
50+
Source: `src/apm_cli/commands/audit.py`, `src/apm_cli/policy/ci_checks.py`.
4851

4952
### `apm audit` (default)
5053

@@ -92,6 +95,40 @@ as either eviction-and-refetch (silent self-heal) or a hard failure
9295
when the cache cannot be repopulated -- never as wrong content under
9396
the right name.
9497

98+
## Install before audit and tamper detection
99+
100+
Running `apm install` before `apm audit --ci` is the correct pattern when
101+
the goal is detecting a developer who forgot to run `apm install` after
102+
editing `apm.yml`. The install step regenerates deployed files so the
103+
subsequent audit can compare them against the lockfile.
104+
105+
That sequence has a blind spot: `apm install` overwrites every managed file
106+
with a clean copy before the audit runs. If a deployed file was modified on
107+
disk after the last install -- for example a hand-edit to
108+
`.github/instructions/` -- the install step restores the original bytes.
109+
The `content-integrity` check then compares the restored file against a
110+
matching hash and reports no finding.
111+
112+
To detect post-install modification, use `setup-only: true` on the action
113+
so it only provides the CLI without running `apm install`, then audit with
114+
`--no-drift`:
115+
116+
```yaml
117+
- uses: microsoft/apm-action@v1
118+
with:
119+
setup-only: true
120+
- run: apm audit --ci --no-drift
121+
```
122+
123+
`--no-drift` skips the install-replay (which requires a warm cache that
124+
`setup-only` does not populate). The `content-integrity` check verifies
125+
SHA-256 hashes of every deployed file against `deployed_file_hashes` in
126+
`apm.lock.yaml` without needing to replay the install. Any byte-level
127+
change to a deployed file since the last install is caught by this check.
128+
129+
See [Enforce in CI](../enforce-in-ci/#audit-only-ci-pattern) for the full
130+
recipe and a comparison table of the two patterns.
131+
95132
## Org-wide sweeps
96133

97134
APM runs per repository. There is no built-in fleet console. The

docs/src/content/docs/enterprise/enforce-in-ci.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,54 @@ jobs:
7777
Make this job a required status check via
7878
[GitHub Rulesets](../github-rulesets/) and a violating PR cannot merge.
7979

80+
## Audit-only CI pattern
81+
82+
The default `microsoft/apm-action@v1` runs `apm install` before any
83+
subsequent steps. That is the right default for most workflows: it ensures
84+
the lockfile and deployed files are present before the audit reads them.
85+
86+
However, `apm install` overwrites every managed file with a fresh copy
87+
before `apm audit --ci` runs. If a managed file was modified on disk after
88+
the last install -- its bytes changed without updating the lockfile hash --
89+
the install step silently restores the clean copy. The `content-integrity`
90+
check then compares the freshly restored file against a hash that matches,
91+
and the tampering goes undetected.
92+
93+
To detect post-install file modification, run the action in setup-only mode
94+
so it only adds the CLI to `PATH` without touching deployed files:
95+
96+
```yaml
97+
jobs:
98+
audit:
99+
runs-on: ubuntu-latest
100+
permissions:
101+
contents: read
102+
steps:
103+
- uses: actions/checkout@v4
104+
- uses: microsoft/apm-action@v1
105+
with:
106+
setup-only: true # CLI only; does not run apm install
107+
- run: apm audit --ci --no-drift
108+
env:
109+
GITHUB_APM_PAT: ${{ secrets.APM_PAT }}
110+
```
111+
112+
`setup-only: true` leaves every deployed file exactly as checked out.
113+
`--no-drift` skips the install-replay check because no warm cache exists;
114+
the `content-integrity` check still verifies that every deployed file's
115+
SHA-256 hash matches the `deployed_file_hashes` recorded in `apm.lock.yaml`.
116+
Any file whose bytes were changed after the last install fails this check.
117+
118+
The two patterns serve different goals:
119+
120+
| Pattern | Use when |
121+
|---|---|
122+
| Full install then audit | Catching developers who skipped `apm install` after editing `apm.yml`; ensuring deployed files are present on a fresh runner |
123+
| Audit-only (`setup-only: true`) | Detecting modification of deployed files after install; committed files and lockfile are the ground truth |
124+
125+
Both patterns enforce policy and the eight baseline lockfile checks. The
126+
difference is only in whether content-integrity can see tampered bytes.
127+
80128
## Recipe: SARIF for GitHub Code Scanning
81129

82130
Emit SARIF and upload it so each violation appears inline on the PR

docs/src/content/docs/getting-started/installation.md

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ sidebar:
1111
- [git](https://git-scm.com/) for dependency management
1212
- Python 3.10+ (only for pip or from-source installs)
1313

14+
On **Windows ARM64**, the one-line installer currently downloads the **x86_64** ZIP (same as the GitHub Release asset); it runs via emulation. Native ARM64 Windows binaries are not selected yet.
15+
1416
## Quick install (recommended)
1517

1618
**macOS / Linux:**
@@ -29,7 +31,7 @@ The installer automatically detects your platform (macOS/Linux/Windows, Intel/AR
2931

3032
### Installer options
3133

32-
The Unix installer supports environment variables for custom environments:
34+
**macOS / Linux (`install.sh`):**
3335

3436
```bash
3537
# Install a specific version
@@ -42,15 +44,60 @@ curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh
4244
GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh
4345
```
4446

47+
**Windows (`install.ps1` in PowerShell):**
48+
49+
Air-gapped hosts should **save `install.ps1` locally** (the `irm` one-liner needs reachability to the script URL).
50+
51+
```powershell
52+
# Pin a version (skips GitHub API - required for many air-gapped / GHES setups)
53+
# Pinned installs verify SHA256 from the matching .sha256 unless you set:
54+
# $env:APM_SKIP_CHECKSUM = "1" # emergency only
55+
$env:VERSION = "v1.2.3"; irm https://aka.ms/apm-windows | iex
56+
57+
# Saved script: pass -SkipChecksum only when the release has no .sha256 sidecar (not recommended).
58+
# .\install.ps1 v1.2.3 -SkipChecksum
59+
60+
# Custom directory for apm.cmd (default: %LOCALAPPDATA%\Programs\apm\bin)
61+
$env:APM_INSTALL_DIR = "$env:LOCALAPPDATA\Programs\apm\bin"; irm https://aka.ms/apm-windows | iex
62+
63+
# Fork, enterprise host, or internal mirror (GITHUB_URL must be https://)
64+
$env:GITHUB_URL = "https://github.corp.com"
65+
$env:APM_REPO = "my-org/apm"
66+
$env:VERSION = "v1.2.3"
67+
irm https://aka.ms/apm-windows | iex
68+
```
69+
70+
**GitHub Actions (`windows-latest`):**
71+
72+
```yaml
73+
jobs:
74+
install-apm:
75+
runs-on: windows-latest
76+
steps:
77+
- name: Install APM (pinned, CI-safe)
78+
shell: pwsh
79+
env:
80+
VERSION: v0.13.0
81+
# For GHES or a mirror, set GITHUB_URL (https only) and APM_REPO as needed.
82+
run: |
83+
irm https://aka.ms/apm-windows | iex
84+
apm --version
85+
- uses: actions/checkout@v4
86+
- run: apm install --frozen
87+
```
88+
4589
| Variable | Default | Description |
4690
|----------|---------|-------------|
47-
| `APM_INSTALL_DIR` | `/usr/local/bin` | Directory for the `apm` symlink |
48-
| `APM_LIB_DIR` | `$(dirname APM_INSTALL_DIR)/lib/apm` | Directory for the full binary bundle |
49-
| `GITHUB_URL` | `https://github.com` | Base URL for downloads (mirrors, GHE) |
50-
| `APM_REPO` | `microsoft/apm` | GitHub repository |
51-
| `VERSION` | *(latest)* | Pin a specific release (skips GitHub API) |
52-
53-
> **Note:** When using `GITHUB_URL` for a GitHub Enterprise or air-gapped mirror, set `VERSION` as well. The GitHub API call for latest-release discovery still targets `api.github.com`; `VERSION` bypasses it entirely.
91+
| `APM_INSTALL_DIR` | `/usr/local/bin` (Unix) / `%LOCALAPPDATA%\Programs\apm\bin` (Windows) | Directory for the `apm` symlink / `apm.cmd` shim |
92+
| `APM_LIB_DIR` | `$(dirname APM_INSTALL_DIR)/lib/apm` | *(Unix only)* Directory for the full binary bundle |
93+
| `GITHUB_URL` | `https://github.com` | Base GitHub URL (asset downloads **and** API host: `api.github.com` on github.com, `{GITHUB_URL}/api/v3` on GHES). Must be `https://`. |
94+
| `APM_REPO` | `microsoft/apm` | Repository as `owner/name` |
95+
| `VERSION` | *(latest)* | Pin a release tag (skips the **releases/latest** HTTP API). Must look like `v1.2.3` or `1.2.3`. |
96+
| `APM_SKIP_CHECKSUM` | *(unset)* | Windows only: set to `1` to skip `.sha256` verification on **pinned** installs (emergency only). |
97+
98+
> **Note - Unix (`install.sh`):** Latest-release discovery still calls `https://api.github.com/repos/.../releases/latest` unless `VERSION` is set. For GHES or mirrors with no access to `api.github.com`, pin `VERSION` so the script never hits that endpoint.
99+
>
100+
> **Note - Windows (`install.ps1`):** The **releases/latest** URL is derived from `GITHUB_URL`: `https://api.github.com` for GitHub.com, or `{GITHUB_URL}/api/v3` for GitHub Enterprise Server. Air-gapped runners should still set `VERSION` so the installer does not need the API at all. When `VERSION` is pinned, the release **`.sha256`** file is required unless you set **`APM_SKIP_CHECKSUM=1`** (emergency only).
54101

55102
## Package managers
56103

0 commit comments

Comments
 (0)