Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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

### Added

- GitLab org-policy discovery now walks the subgroup tree from the project's own group up to the top-level group and applies the closest `apm-policy` project, so a team can scope its own governance under a subgroup instead of only the top-level group; a subgroup policy can `extends:` an ancestor group's policy (including nested-namespace references). Flat `<group>/<project>` remotes are unchanged. (closes #2753) (#2933)
- gh-aw's shared APM import now supports `token-source: github-token`; after consumers re-vendor the workflow, its read-only current-repository identity can fetch same-repository private packages, while `cascade` remains the default and cross-repository packages still require a dedicated token or GitHub App. (#2706)
- OpenAPM v0.1 adds `req-pl-018` for dependency-policy identity casing and amends `req-rs-016` clause (3), the Section 6.4 merge rules, and the Section 6.5 pattern grammar so repository identity and policy matching cannot diverge; Section 11.2 item 6 now requires the per-host case rule in `CONFORMANCE.md`. (#2706)

Expand Down
3 changes: 2 additions & 1 deletion CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -696,9 +696,10 @@
"keyword": "MUST",
"section": "6.1.1",
"status": "active",
"test_count": 2,
"test_count": 3,
"tests": [
"tests/spec_conformance/test_policy_reqs.py::test_policy_gitlab_discovery_provider_is_a_distinct_convention",
"tests/spec_conformance/test_policy_reqs.py::test_policy_gitlab_discovery_walks_subgroups_closest_first",
"tests/spec_conformance/test_policy_reqs.py::test_policy_provides_default_allow_list_shape"
]
},
Expand Down
2 changes: 1 addition & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Repository-coordinate segments are case-insensitive for `github.com`, GitHub Ent
| [req-pl-008](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-008) | MUST | 6.3.1 | governance | active | 1 | - |
| [req-pl-009](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-009) | MUST | 6.6 | governance | active | 1 | - |
| [req-pl-010](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-010) | MUST | 6.2 | governance | active | 1 | - |
| [req-pl-011](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-011) | MUST | 6.1.1 | governance | active | 2 | - |
| [req-pl-011](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-011) | MUST | 6.1.1 | governance | active | 3 | - |
| [req-pl-012](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-012) | MUST | 6.1.1 | governance | active | 1 | - |
| [req-pl-013](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-013) | MUST | 6.8 | governance | active | 1 | - |
| [req-pl-014](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-014) | MUST | 6.8 | governance | active | 1 | - |
Expand Down
17 changes: 14 additions & 3 deletions docs/src/content/docs/enterprise/apm-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,22 @@ On GitHub and GitHub API-compatible hosts, the `.github-private` repo is preferr

```
<top-level-group>/
apm-policy/
apm-policy.yml # auto-discovered by projects whose remote starts with <top-level-group>/
[<subgroups>/] # optional -- apm-policy may sit at any level
apm-policy/
apm-policy.yml # discovered by projects under this group or any descendant
```

GitLab discovery uses only the top-level group: APM takes the first path segment from the project remote and looks for `<top-level-group>/apm-policy`. It does not search nested subgroup scopes. Set `APM_GITLAB_POLICY_REPO` to use a different project name if your org already publishes policy under another name. A project without `apm-policy` (or the configured override) is treated as a clean "no policy" outcome, matching the fallthrough behaviour on GitHub and ADO -- it does not print a warning.
GitLab discovery walks the subgroup tree from the project's own group up to the top-level group, and the **closest** `apm-policy` wins. For a project remote `gitlab.com/acme/dept-a/team-x/my-project`, APM probes in order:

1. `acme/dept-a/team-x/apm-policy`
2. `acme/dept-a/apm-policy`
3. `acme/apm-policy`

This lets an individual team publish its own `apm-policy` under its subgroup without affecting the rest of the organization, while a top-level `acme/apm-policy` still applies to every project that has no closer policy. A flat `<group>/<project>` remote (including a personal namespace like `gitlab.com/<user>/<project>`) probes only `<group>/apm-policy`, identical to the previous behaviour.

Composing policies across levels is opt-in via `extends:`. A team policy can extend an ancestor group's policy to inherit and tighten it, using either an explicit path (`extends: "acme/dept-a/apm-policy"` for the immediate parent group, or a deeper ancestor) or the `extends: "org"` shorthand, which on GitLab always resolves to the **top-level** group's policy (`<top-level-group>/apm-policy`) -- not the nearest ancestor. Children can only tighten, never relax.
Comment thread
Copilot marked this conversation as resolved.

Set `APM_GITLAB_POLICY_REPO` to use a different project name if your org already publishes policy under another name. A project with no `apm-policy` at any level (or the configured override) is treated as a clean "no policy" outcome, matching the fallthrough behaviour on GitHub and ADO -- it does not print a warning.

:::caution[Self-managed GitLab requires GITLAB_HOST or APM_GITLAB_HOSTS]
An arbitrary FQDN is never auto-classified as GitLab -- the same domain shape could be Bitbucket, Gitea, or a plain git server. `gitlab.com` is recognised automatically, but a self-managed instance (e.g. `gitlab.example.com`) is only recognised once you set `GITLAB_HOST=gitlab.example.com` (or `APM_GITLAB_HOSTS` for more than one instance). Without it, APM falls through to the GitHub-style cascade above, which is invalid on GitLab and behaves exactly like the unfixed discovery this section describes. This mirrors `GITHUB_HOST` for GitHub Enterprise Server and `ADO_HOST` for on-prem Azure DevOps Server -- see [Environment Variables](../../reference/environment-variables/).
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/enterprise/governance-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ These are the sharp edges. Plan around them; do not assume they are solved.
- **`apm compile` and `apm run` do not re-check policy.** They trust install. Operational mitigation: ensure that no compile or run step in CI is reachable without a preceding `apm install` that ran the gate.
- **`apm audit --ci` in `warn` mode rewrites violations to `passed=True`.** Warn mode never fails CI exit. The visibility is in the SARIF output, not the exit code. Operational mitigation: monitor Code Scanning alerts during the warn-mode rollout phase; do not assume CI green means "no policy violations" while in warn mode.
- **`apm install` has no `--policy <path>` flag.** Only `apm audit` does. This is the air-gapped install gap. Operational mitigation: use `extends:` from a reachable mirror, or run audit (which does support `--policy <path>`) as the gating check and skip install-time enforcement in air-gapped CI.
- **Plain git remotes are not auto-discovered.** GitHub and GitHub Enterprise use the `.github` cascade, Azure DevOps uses the org `apm` project and `apm-policy` repository (with `_apm/_apm` as a legacy fallback), and GitLab uses `<top-level-group>/apm-policy/apm-policy.yml`, where the top-level group is the first path segment of the remote. Nested subgroup scopes are not searched. Configure self-managed GitLab with `GITLAB_HOST` or `APM_GITLAB_HOSTS`. For unsupported remotes, pass `apm audit --ci --policy <path-or-url>` explicitly.
- **Plain git remotes are not auto-discovered.** GitHub and GitHub Enterprise use the `.github` cascade, Azure DevOps uses the org `apm` project and `apm-policy` repository (with `_apm/_apm` as a legacy fallback), and GitLab walks the subgroup tree from the project's own group up to the top-level group and applies the closest `apm-policy` (see [Policy Files](./apm-policy/#where-it-lives)). Configure self-managed GitLab with `GITLAB_HOST` or `APM_GITLAB_HOSTS`. For unsupported remotes, pass `apm audit --ci --policy <path-or-url>` explicitly.
- **Trust anchor is `git remote get-url origin`.** A developer who pushes the project to a personal org will have policy discovery resolve `<their-org>/.github/apm-policy.yml` -- which they control. Operational mitigation: branch protection on the canonical repo is the trust boundary; nothing about a personal fork can bypass what your CI requires before merge.
- **`apm install --dry-run` silently downgrades hash-mismatch.** In dry-run, `raise_blocking_errors=False` (outcome_routing.py:104-119) causes the mismatch to surface as `discovery_miss` with no "Would be blocked" line and exit 0. Operational mitigation: rely on `apm audit --ci` in CI for hash-pin verification, not on `apm install --dry-run`.
- **`apm audit --ci --no-policy` and `APM_POLICY_DISABLE=1` skip policy checks.** The 21 policy checks are bypassed in audit, but the 8 baseline lockfile checks still run. Operational mitigation: keep bypass flags out of required CI workflows; the bypass contract in section 7 is authoritative.
Expand Down
5 changes: 3 additions & 2 deletions docs/src/content/docs/enterprise/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,8 +619,9 @@ found in the organization cascade: `.github-private`, `.github`, `.apm`, then
`_apm`. Azure DevOps remotes use the org `apm` project and `apm-policy`
repository. Legacy `_apm/_apm` is a temporary fallback after a 404 from the
primary coordinate.
GitLab remotes use `<top-level-group>/apm-policy/apm-policy.yml`, using the
first path segment of the remote; nested subgroup scopes are not searched. Set
GitLab walks the subgroup tree from the project's own group up to the top-level
group and applies the closest `apm-policy` project; a flat `<group>/<project>`
remote probes only `<group>/apm-policy`. Set
Comment thread
Copilot marked this conversation as resolved.
`GITLAB_HOST` or `APM_GITLAB_HOSTS` to recognize a self-managed host, and use
`APM_GITLAB_POLICY_REPO` to select another project name. Plain git remotes
fall through with no policy applied. Repositories with no detectable git remote
Expand Down
10 changes: 7 additions & 3 deletions packages/apm-guide/.apm/skills/apm-usage/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,13 @@ may use. This section covers how that contract is enforced at `apm install` time
APM auto-discovers org policy from the project's git remote by checking
`.github-private`, `.github`, `.apm`, and `_apm` policy repos in order on GitHub
API-compatible hosts. Azure DevOps hosts use repository `apm-policy` in project
`apm`, with a legacy `_apm/_apm` fallback after a 404. GitLab uses
`<top-level-group>/apm-policy/apm-policy.yml`, derived from the first remote
path segment; nested subgroup scopes are not searched. Configure a self-managed host with
`apm`, with a legacy `_apm/_apm` fallback after a 404. GitLab walks the subgroup
tree from the project's own group up to the top-level group and applies the
closest `apm-policy` (e.g. `acme/dept-a/team-x/apm-policy` before
`acme/dept-a/apm-policy` before `acme/apm-policy`), so a team can scope its own
policy under a subgroup; a team policy can `extends:` an ancestor group's policy
to inherit it. A flat `<group>/<project>` remote probes only `<group>/apm-policy`.
Configure a self-managed host with
`GITLAB_HOST` or `APM_GITLAB_HOSTS`, and use `APM_GITLAB_POLICY_REPO` to select
another project name. Repositories with no detectable git remote (unpacked
bundles, temp dirs) emit an explicit "could not determine org" line and skip
Expand Down
16 changes: 12 additions & 4 deletions src/apm_cli/policy/_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,12 @@ def _fetch_gitlab_chain_parent(
)
org = current_org
else:
parts = parent_ref.strip("/").split("/")
if len(parts) == 3:
parts = [p for p in parent_ref.strip("/").split("/") if p]
# Strip an explicit leaf-host prefix so ``host/namespace/.../repo`` and
# ``namespace/.../repo`` are treated the same. Only the leaf host (with
# matching port) is accepted; cross-host refs are rejected upstream by
# ``_validate_extends_host``. Requires at least host + namespace + repo.
if len(parts) >= 3:
try:
explicit = urlsplit(f"//{parts[0]}")
except ValueError:
Expand All @@ -149,13 +153,17 @@ def _fetch_gitlab_chain_parent(
and explicit.port == port
):
parts = parts[1:]
if len(parts) != 2:
# A GitLab namespace may be nested (subgroups, see #2753): everything
# before the final segment is the namespace, the final segment is the
# policy repo. Requires at least ``namespace/repo``.
if len(parts) < 2:
return PolicyFetchResult(
source=f"org:{parent_ref}",
error=f"Invalid GitLab policy reference: {parent_ref}",
outcome="cache_miss_fetch_fail",
)
org, repo = parts
org = "/".join(parts[:-1])
repo = parts[-1]
return _fetch_from_gitlab_repo(
org=org,
repo=repo,
Expand Down
Loading