Skip to content
Open
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
8 changes: 8 additions & 0 deletions workspaces/quay/.changeset/polite-cobras-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@backstage-community/plugin-scaffolder-backend-module-quay': patch
'@backstage-community/plugin-quay-backend': patch
'@backstage-community/plugin-quay-common': patch
'@backstage-community/plugin-quay': patch
---

Add CI bump-trust test coverage and contributor guides for Quay plugins.
10 changes: 6 additions & 4 deletions workspaces/quay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ This workspace contains plugins for viewing and managing [Quay](https://docs.qua

This workspace is composed of several packages:

- [quay](./plugins/quay/README.md) - The frontend plugin that displays information about your container images from the Quay registry on entity pages.
- [quay-backend](./plugins/quay-backend/README.md) - The backend plugin that queries the Quay API, with support for permissions and OAuth2 access token authentication.
- [quay-actions](./plugins/quay-actions/README.md) - A scaffolder backend module providing software template actions for Quay, such as creating a Quay repository.
- [quay-common](./plugins/quay-common/README.md) - A common library containing shared types and utilities used by the other Quay plugins.
- [quay](./plugins/quay/) — The frontend plugin that displays information about your container images from the Quay registry on entity pages. Operator docs: [README](./plugins/quay/README.md); contributors: [CONTRIBUTING](./plugins/quay/CONTRIBUTING.md)
- [quay-backend](./plugins/quay-backend/) — The backend plugin that queries the Quay API, with support for permissions and OAuth2 access token authentication. Operator docs: [README](./plugins/quay-backend/README.md); contributors: [CONTRIBUTING](./plugins/quay-backend/CONTRIBUTING.md)
- [quay-actions](./plugins/quay-actions/) — A scaffolder backend module providing software template actions for Quay, such as creating a Quay repository. Operator docs: [README](./plugins/quay-actions/README.md); contributors: [CONTRIBUTING](./plugins/quay-actions/CONTRIBUTING.md)
- [quay-common](./plugins/quay-common/) — A common library containing shared types and utilities used by the other Quay plugins. Operator docs: [README](./plugins/quay-common/README.md); contributors: [CONTRIBUTING](./plugins/quay-common/CONTRIBUTING.md)

The `packages/app` and `packages/backend` directories are leftover stubs and are **not** used for day-to-day development. Prefer each package's `dev/` harness — see the contributor guides above.

## Quick start

Expand Down
93 changes: 69 additions & 24 deletions workspaces/quay/plugins/quay-actions/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,69 @@
# Setting up the development environment for Quay actions

1. Add the local package dependency to the Backstage instance

```shell
yarn workspace backend add file:./plugins/quay-actions
```

2. [Register](./README.md#configuration) the Quay actions in your Backstage project
3. **Optional**: You can use the sample template from this repository and add it as `locations` in your `app-config.yaml` file

```yaml
---
catalog:
locations:
- type: file
target: ../../plugins/quay-actions/examples/templates/01-quay-template.yaml
rules:
- allow: [Template]
```

4. Run `yarn start`
5. If you don't have a Quay account created yet you can create one for free on the [quay](https://quay.io) website
6. Start using the Quay actions in your templates
# Contributing — Quay scaffolder backend module

Developer guide for `@backstage-community/plugin-scaffolder-backend-module-quay`. For operator install and configuration, see [README.md](./README.md).

## Prerequisites

- Node.js **22 or 24** (see workspace `engines` in the workspace root `package.json`)
- Yarn (workspace uses its own `yarn.lock`; run commands from `workspaces/quay`)

## Development harness

Start this module in isolation:

```bash
yarn workspace @backstage-community/plugin-scaffolder-backend-module-quay start
```

This runs a minimal backend with `@backstage/plugin-scaffolder-backend` and the Quay scaffolder module (`dev/index.ts`). Use it for action registration and local scaffolder smoke.

Only one plugin `dev/` harness should run on port **7007** at a time.

### Environment setup

The `quay:create-repository` action takes Quay credentials and URLs as **template inputs** (see the [example template](./examples/templates/01-quay-template.yaml)). Use local-only placeholder values for development — do not commit secrets.

To exercise that sample template from a consumer Backstage app (or a fuller local app that loads catalog templates), add it as a catalog location in an untracked `app-config.local.yaml` (or your app's `app-config.yaml`):

```yaml
catalog:
locations:
- type: file
target: ../../plugins/quay-actions/examples/templates/01-quay-template.yaml
rules:
- allow: [Template]
```

Adjust `target` to the path from that app's config file. You need a Quay account and an OAuth token to run the create-repository step for real.

## Validation commands

From the workspace root (`workspaces/quay`):

```bash
yarn workspace @backstage-community/plugin-scaffolder-backend-module-quay test
yarn workspace @backstage-community/plugin-scaffolder-backend-module-quay lint
yarn tsc
```

## What automated tests cover

CI exercises:

- **Module wiring** — `startTestBackend` asserts `scaffolder.addActions` registers `quay:create-repository`
- **Action handler** — create-repository request/response and error paths (unit tests)

CI does **not** replace reading [Backstage release notes](https://github.com/backstage/backstage/releases) for the `@backstage/*` packages this module depends on. After a dependency bump, review those notes and decide whether additional validation is warranted.

## Optional manual smoke checklist

Use when you change scaffolder integration code or are reviewing a Backstage version bump:

1. Start this harness and confirm the backend starts without errors. In the logs, look for scaffolder listing enabled actions and verify `quay:create-repository` is present (alongside built-in actions such as `fetch:template` and `debug:log`).
2. To run the [sample template](./examples/templates/01-quay-template.yaml) end-to-end (create a real Quay repository), use a consumer Backstage app or overlays with the catalog location from [Environment setup](#environment-setup) and a Quay OAuth token — not covered by this harness alone.

## Related packages

- [Quay backend](../quay-backend/CONTRIBUTING.md) — registry API plugin
- [Quay frontend](../quay/CONTRIBUTING.md) — entity page UI
- [Quay common](../quay-common/CONTRIBUTING.md) — shared types/permissions
2 changes: 2 additions & 0 deletions workspaces/quay/plugins/quay-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The following actions are currently supported in this module:

- Create a Quay repository

**Contributors:** see [CONTRIBUTING.md](./CONTRIBUTING.md) for development harnesses, tests, and bump-review guidance.

## Installation

Run the following command to install the action package in your Backstage project
Expand Down
24 changes: 24 additions & 0 deletions workspaces/quay/plugins/quay-actions/dev/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(import('../src'));

backend.start();
3 changes: 3 additions & 0 deletions workspaces/quay/plugins/quay-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
"yaml": "^2.6.0"
},
"devDependencies": {
"@backstage/backend-defaults": "^0.17.3",
"@backstage/backend-test-utils": "^1.11.4",
"@backstage/cli": "^0.36.3",
"@backstage/plugin-scaffolder-backend": "^4.0.1",
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.12"
},
"files": [
Expand Down
40 changes: 40 additions & 0 deletions workspaces/quay/plugins/quay-actions/src/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node';

import { scaffolderModuleQuayAction } from './module';

describe('scaffolderModuleQuayAction', () => {
it('registers createQuayRepository via the scaffolder extension point', async () => {
const registeredIds: string[] = [];
const extensionPoint = {
addActions: (...actions: { id: string }[]) => {
registeredIds.push(...actions.map(action => action.id));
},
};

await startTestBackend({
extensionPoints: [[scaffolderActionsExtensionPoint, extensionPoint]],
features: [
scaffolderModuleQuayAction,
mockServices.rootConfig.factory({ data: {} }),
],
});

expect(registeredIds).toEqual(['quay:create-repository']);
});
});
92 changes: 92 additions & 0 deletions workspaces/quay/plugins/quay-backend/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Contributing — Quay backend plugin

Developer guide for `@backstage-community/plugin-quay-backend`. For operator install and configuration, see [README.md](./README.md).

## Prerequisites

- Node.js **22 or 24** (see workspace `engines` in the workspace root `package.json`)
- Yarn (workspace uses its own `yarn.lock`; run commands from `workspaces/quay`)

## Development harness

Start this plugin in isolation:

```bash
yarn workspace @backstage-community/plugin-quay-backend start
```

This runs a minimal backend with the Quay backend plugin (`dev/index.ts`). Use it for router, permissions, and Quay API client work.

Only one plugin `dev/` harness should run on port **7007** at a time. For UI smoke against a live entity page, also start the [frontend plugin harness](../quay/CONTRIBUTING.md) (separate process / port) after the backend is up.

### Environment setup

Configure Quay via the workspace [`app-config.yaml`](../../app-config.yaml) or an untracked `app-config.local.yaml`. Use local-only placeholder values for development — do not commit secrets.

| Config key / variable | Purpose |
| ---------------------------- | ------------------------------------------------------------------------------------------ |
| `quay.apiUrl` | Quay API base URL (single-instance) |
| `quay.apiKey` | OAuth access token / API key (placeholder locally) |
| `quay.uiUrl` | Quay UI base URL (optional, single-instance) |
| `quay.instances` | Multi-instance list (`name`, `apiUrl`, `apiKey`, …) — do not mix with single-instance keys |
| `BACKSTAGE_DEV_STATIC_TOKEN` | Optional static bearer token if you enable `backend.auth.externalAccess` for `curl` |

Optional overrides can go in an untracked `app-config.local.yaml` at the workspace root.

### API authentication for `curl`

When the default backend auth policy applies, authenticated requests need a Bearer token. You can register a **static** backend access token (see [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth)):

```yaml
backend:
auth:
externalAccess:
- type: static
options:
token: ${BACKSTAGE_DEV_STATIC_TOKEN}
subject: user:default/guest
```

Example (adjust instance / org / repo):

```bash
curl -H "Authorization: Bearer ${BACKSTAGE_DEV_STATIC_TOKEN}" \
"http://localhost:7007/api/quay/default/repository/my-org/my-repo/tag"
```

Requests without a valid `Authorization: Bearer …` header are rejected when the default auth policy applies. Viewing Quay data also requires the `quay.view.read` permission (`quayViewPermission`).

## Validation commands

From the workspace root (`workspaces/quay`):

```bash
yarn workspace @backstage-community/plugin-quay-backend test
yarn workspace @backstage-community/plugin-quay-backend lint
yarn tsc
```

## What automated tests cover

CI exercises:

- **Plugin wiring** — `startTestBackend` mount (`httpRouter.use`)
- **Permission middleware** — DENY returns 403
- **Router validation** — 400 when `instanceName` / `org` / `repo` are missing or whitespace
- **QuayService** — config parsing and HTTP client behavior (unit tests)

CI does **not** replace reading [Backstage release notes](https://github.com/backstage/backstage/releases) for the `@backstage/*` packages this plugin depends on. After a dependency bump, review those notes and decide whether additional validation is warranted.

## Optional manual smoke checklist

Use when you change backend integration code or are reviewing a Backstage version bump:

1. Configure Quay placeholders and start this harness.
2. Call a tag list endpoint with Bearer token (see above). Expect `200` with tag JSON when config and permissions allow, or a clear `403` / `404` for deny / unknown instance.
3. Full entity-page UI smoke needs the [frontend harness](../quay/CONTRIBUTING.md) as well.

## Related packages

- [Quay frontend](../quay/CONTRIBUTING.md) — entity page UI
- [Quay common](../quay-common/CONTRIBUTING.md) — shared permission contracts
- [Quay scaffolder actions](../quay-actions/CONTRIBUTING.md) — `quay:create-repository`
2 changes: 2 additions & 0 deletions workspaces/quay/plugins/quay-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ A simple plugin that queries the quay.io api, but provides additional features l
- setting permissions
- using OAuth2 access tokens for authentication

**Contributors:** see [CONTRIBUTING.md](./CONTRIBUTING.md) for development harnesses, tests, and bump-review guidance.

## Setup

### Installation
Expand Down
1 change: 1 addition & 0 deletions workspaces/quay/plugins/quay-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"express-promise-router": "^4.1.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.11.4",
"@backstage/cli": "^0.36.3",
"@types/express": "^4.17.6",
"@types/supertest": "^7.0.0",
Expand Down
26 changes: 23 additions & 3 deletions workspaces/quay/plugins/quay-backend/src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,30 @@
* limitations under the License.
*/

import { mockServices, startTestBackend } from '@backstage/backend-test-utils';

import { quayPlugin } from './plugin';

describe('quay-backend', () => {
it('should export the quay backend plugin', () => {
expect(quayPlugin).toBeDefined();
describe('quayPlugin', () => {
it('registers the quay backend plugin router', async () => {
const httpRouterMock = mockServices.httpRouter.mock();

await startTestBackend({
extensionPoints: [],
features: [
quayPlugin,
httpRouterMock.factory,
mockServices.rootConfig.factory({
data: {
quay: {
apiUrl: 'https://quay.example.com',
apiKey: 'test-token',
},
},
}),
],
});

expect(httpRouterMock.use).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,36 @@ describe('createRouter', () => {

const response = await request(app).get(endpoint);
expect(response.status).toEqual(403);
expect(response.body.error).toEqual(
'Unauthorized, please ensure you have the correct permissions.',
);
},
);

it.each([
{
endpoint: '/default/repository/%20/repo/tag',
description: 'whitespace org',
},
{
endpoint: '/default/repository/org/%20/tag',
description: 'whitespace repo',
},
{
endpoint: '/default/repository/%20/%20/manifest/sha256:123',
description: 'whitespace org and repo on manifest',
},
])(
'should return 400 for missing required parameters ($description)',
async ({ endpoint }) => {
const response = await request(app).get(endpoint);
expect(response.status).toEqual(400);
expect(response.body.error).toEqual('Missing required parameters');
expect(mockQuayService.getQuayInstance).not.toHaveBeenCalled();
expect(mockQuayService.getTags).not.toHaveBeenCalled();
expect(mockQuayService.getLabels).not.toHaveBeenCalled();
expect(mockQuayService.getManifestByDigest).not.toHaveBeenCalled();
expect(mockQuayService.getSecurityDetails).not.toHaveBeenCalled();
},
);
});
Expand Down
Loading
Loading