backstage / backstage/community-plugins

๐Ÿ› copilot-backend: enterprise metrics always 403 with GitHub App auth โ€” installation resolved via getOrgInstallation

Open
#10,207 4 comments 0 reactions 0 assignees View on GitHub
bug help wanted workspace/copilot
Dominant language
TypeScript
Stars
422
Forks
697
Avg merge
2d 6h
Merged PRs (30d)
286

Description

### Workspace

copilot

### ๐Ÿ“œ Description

When the copilot plugin is configured with GitHub App credentials and `copilot.enterprise`, enterprise metrics ingestion fails on every scheduled run with:

```
[TaskManagementV2] Failed ingest for enterprise: on . Resource not accessible by integration - https://docs.github.com/rest/copilot/copilot-usage-metrics#get-copilot-enterprise-usage-metrics-for-a-specific-day
```

The root cause is in `GithubClientV2.getOctokit` (workspaces/copilot/plugins/copilot-backend/src/client/GithubClientV2.ts):

```ts
const orgName =
type === 'organization'
? this.copilotConfig.organization
: this.copilotConfig.enterprise;
...
const { data: installation } =
await appOctokit.rest.apps.getOrgInstallation({ org: orgName });
```

For the `enterprise` scope the code passes the **enterprise slug** into `GET /orgs/{slug}/installation`. That endpoint can only ever return an **organization** installation. GitHub's enterprise permissions (including "Enterprise Copilot metrics: Read") only attach to installations with `target_type: "Enterprise"` โ€” organization installations cannot carry them. So the token minted from the org installation is always rejected with `403 Resource not accessible by integration` by the `/enterprises/{slug}/copilot/metrics/reports/*` endpoints, regardless of how the app registration or the enterprise installation is configured.

Two secondary consequences:

- The lookup only "works" at all for customers whose enterprise slug happens to equal one of their org slugs; otherwise `getOrgInstallation` 404s.
- An existing enterprise installation of the same app (with the correct enterprise permissions) is never consulted โ€” there is no code path that resolves it.

### ๐Ÿ‘ Expected behavior

With the GitHub App installed on the enterprise account and granted "Enterprise Copilot metrics: Read-only", enterprise metrics ingestion should succeed using a token minted from the **enterprise** installation.

### ๐Ÿ‘Ž Actual Behavior with Screenshots

Every enterprise-scope ingest fails with `403 Resource not accessible by integration`, even when:

- the app registration requests "Enterprise Copilot metrics: Read-only",
- the app is owned by the enterprise,
- the app is installed on the enterprise account with the permission granted,
- the enterprise "Copilot usage metrics" policy is "Enabled everywhere".

Distributed traces confirm the plugin only ever mints tokens for the org installation ID, never the enterprise installation.

### ๐Ÿ‘Ÿ Reproduction steps

1. Configure `copilot.enterprise: ` and `integrations.github[].apps` with an app installed on both an org and the enterprise account (enterprise installation granted "Enterprise Copilot metrics: Read-only").
2. Let the scheduled task run.
3. Observe `Failed ingest for enterprise: ... Resource not accessible by integration` for every day, while organization-scope ingestion succeeds.

Note: in our environment the enterprise slug and organization slug are identical. This is why the failure manifests as a 403 (the lookup silently resolves the org installation of the same name) rather than a 404 from getOrgInstallation. Environments where the slugs differ will see a 404 at the installation lookup instead.

### ๐Ÿ“ƒ Provide the context for the Bug.

For the `enterprise` scope, resolve the installation via `GET /app/installations` and select the one targeting the enterprise:

```ts
const installations = await appOctokit.paginate(
appOctokit.rest.apps.listInstallations,
{ per_page: 100 },
);
const target = orgName.toLowerCase();
const enterpriseInstallation = installations.find(
i =>
i.target_type === 'Enterprise' &&
(i.account?.slug?.toLowerCase() === target ||
i.account?.login?.toLowerCase() === target),
);
```

and fail with a descriptive error if no enterprise installation exists. The organization scope is unaffected.

We are running this as a local `yarn patch` against `@backstage-community/plugin-copilot-backend@1.1.0` and enterprise ingest works with it. Happy to open a PR with the fix (plus tests) if maintainers agree with the approach.

Related history: #3240 documented that enterprise previously required a classic PAT (App auth unsupported for enterprise); the current App-auth path added since then contains this defect. #9458 covers separate enterprise v2 parsing bugs downstream of this call โ€” that issue's fixes don't help if the API call itself 403s.

### ๐Ÿ‘€ Have you spent some time to check if this bug has been raised before?

- [x] I checked and didn't find similar issue

### ๐Ÿข Have you read the Code of Conduct?

- [x] I have read the [Code of Conduct](https://github.com/backstage/community-plugins/blob/main/CODE_OF_CONDUCT.md)

### Are you willing to submit PR?

Yes I am willing to submit a PR!

Contributor guide

Open the contributing guide

Research direction

Begin in workspaces/copilot/plugins/copilot-backend/src/client/GithubClientV2.ts at getOctokit and trace the enterprise path used by scheduled ingestion. Add tests for selecting a target_type Enterprise installation and for the missing-installation error. Done means enterprise metrics use the enterprise installation while organization behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
github, typescript
Domain
api, authentication, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.