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
14 changes: 14 additions & 0 deletions .github/workflows/readme-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: README sync check

on:
push:
branches: [main]
pull_request:

jobs:
check-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify root README references every labs/ and specs/ directory
run: bash scripts/check-readme.sh
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/003-lab-3-api-quality"
"feature_directory": "specs/004-lab-4-auto-registration"
}
61 changes: 53 additions & 8 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
<!--
Sync Impact Report
==================
Version change: 1.2.0 → 1.3.0
Version change: 1.4.0 → 1.5.0
Added sections:
- Principle IX. Pragmatic Security for Learning Environments
- Lab Structure Standards — new requirement: a lab is not complete until the root README.md
(Lab Series table, Getting Started tree, Repository Structure tree) is updated to reference
its labs/ and specs/ directories; this must be planned/tasked, not left to post-hoc CI
Modified principles: None renamed
Removed sections: None
Templates reviewed:
✅ .specify/templates/plan-template.md — Constitution Check section is generic; no updates required
✅ .specify/templates/plan-template.md — Constitution Check section is generic (gates are
derived from the constitution file at plan time, no fixed per-principle table); no edit
required, the new standard is picked up automatically
✅ .specify/templates/tasks-template.md — Phase N (Polish & Cross-Cutting Concerns) generic
"Documentation updates in docs/" placeholder replaced with an explicit root README update
task referencing this standard
✅ .specify/templates/spec-template.md — No principle-specific references; no updates required
✅ .specify/templates/tasks-template.md — No principle-specific references; no updates required
Instance files requiring updates (in-progress feature work):
Instance files requiring updates (in-progress/completed feature work):
✅ README.md — updated for Lab 4 (2026-07-04): Lab Series table row linked (no longer "coming
soon"), Getting Started and Repository Structure trees include labs/lab-04-auto-registration/
and specs/004-lab-4-auto-registration/
✅ scripts/check-readme.sh + .github/workflows/readme-sync.yml — added 2026-07-04 as a CI
safety net (not the primary mechanism — see rationale above)
⚠ specs/001-lab-1-base-backstage/plan.md — Constitution Check table missing Principle IX
⚠ specs/002-lab-2-users-roles/spec.md — Should note Principle IX applies to auth credential handling
(pre-existing gap, unrelated to this amendment)
⚠ specs/002-lab-2-users-roles/spec.md — Should note Principle IX applies to auth credential
handling (pre-existing gap, unrelated to this amendment)
Follow-up TODOs: None
-->

Expand All @@ -35,8 +48,22 @@ Documentation MUST explain the *why* and *how* of each step, not just the *what*
The journey of setting up Backstage is as important as the final running instance.
Steps that are non-obvious, error-prone, or surprising MUST include explanatory context.

When a lab step instructs the user to create a file, and that file's full content either
(a) cannot reasonably fit on one screen (as a rough guide, more than ~40–50 lines) or
(b) is a complete, reusable source file rather than a short edit/diff snippet — regardless of
length — the file's full content MUST be committed to the repository under the lab's own
directory (convention: a `code/` subdirectory mirroring the target relative path, e.g.
`labs/lab-0N-*/code/packages/app/src/modules/foo/Bar.tsx`), and the README MUST link to that
committed file instead of embedding the whole content inline in a fenced code block. Short
edit-in-place snippets that show surrounding context for a targeted diff against a file created
in an earlier step (not a full new file) are exempt and may remain inline.

**Rationale**: A user who follows steps blindly cannot troubleshoot or adapt. Understanding
the process produces durable knowledge; following a script does not.
the process produces durable knowledge; following a script does not. Embedding entire source
files inline inflates page length without adding explanatory value — the prose around the file
(the *why*) is what teaches; the file content itself is better consulted, copied, or diffed as
a real file. A committed, linked file is also directly reusable by a learner adapting the lab
to their own repo, which an inline fence is not.

### III. Cross-Platform Compatibility

Expand Down Expand Up @@ -162,6 +189,24 @@ Labs that use simplified security practices (Principle IX) MUST also include:
Labs MUST NOT require external network access beyond downloading freely available software
and dependencies. All API samples and test data MUST be included in the repository.

Labs that instruct the user to create a large or reusable source file MUST commit that file's
content under a `code/` subdirectory within the lab's own directory and link to it from the
README, per Principle II, rather than embedding it inline.

A lab is NOT complete until the root `README.md` is updated to reference it: a Lab Series
table row (linked, no longer marked "coming soon"), and an entry in both the Getting Started
tree and the Repository Structure tree for the new `labs/` and `specs/` directories. This is
part of the lab's definition of done, not a follow-up chore — `/speckit-plan` and
`/speckit-tasks` MUST account for it (an explicit task, not a generic "documentation updates"
placeholder), so it is done during `/speckit-implement` rather than caught later at PR review
or in CI.

**Rationale**: The root README previously drifted out of date — Lab 4 shipped fully functional
while the README still listed it as "coming soon" — because README maintenance wasn't part of
any lab's definition of done. It was only caught after the fact, via an ad hoc CI check, which
is too late: by then the fix is a follow-up interruption rather than something completed as
part of the original work.

## Development Workflow

- Each lab corresponds to one speckit feature branch following the `###-lab-name` convention.
Expand Down Expand Up @@ -193,4 +238,4 @@ All plan `Constitution Check` gates MUST reference the principles by Roman numer

- `yarn dev` does not exist. Use `yarn start` instead.

**Version**: 1.3.0 | **Ratified**: 2026-06-07 | **Last Amended**: 2026-06-08
**Version**: 1.5.0 | **Ratified**: 2026-06-07 | **Last Amended**: 2026-07-04
3 changes: 3 additions & 0 deletions .specify/templates/tasks-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ Examples of foundational tasks (adjust based on your project):
**Purpose**: Improvements that affect multiple user stories

- [ ] TXXX [P] Documentation updates in docs/
- [ ] TXXX Update root README.md (Lab Series table entry, Getting Started tree, Repository
Structure tree) to reference this lab's `labs/` and `specs/` directories, per the
Constitution's Lab Structure Standards — required for this lab to be considered complete
- [ ] TXXX Code cleanup and refactoring
- [ ] TXXX Performance optimization across all stories
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
at specs/003-lab-3-api-quality/plan.md
at specs/004-lab-4-auto-registration/plan.md
<!-- SPECKIT END -->
9 changes: 8 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,11 @@ Step 7 is complete and the PR checklist below is satisfied.
6. Confirm the lab starts from the state left by the previous lab and documents any
dependencies on prior lab outputs.
7. Validate manually (Step 7 above) on at least one platform.
8. Open a PR and merge following Step 8 above.
8. Update the root `README.md` — add the lab to the Lab Series table and to the Getting
Started / Repository Structure trees. Run `bash scripts/check-readme.sh` to confirm
nothing is missing; CI runs the same check on every push and PR and will fail the build
if the root README falls out of sync with `labs/` or `specs/`.
9. Open a PR and merge following Step 8 above.

---

Expand Down Expand Up @@ -300,6 +304,9 @@ Before submitting a PR for a new or changed lab, confirm:
- [ ] No Petstore or similarly dated API examples are used (Constitution Principle VII)
- [ ] Pre-committed API spec files are self-contained (no external `$ref`s)
- [ ] The lab builds correctly on top of the previous lab's end state
- [ ] Root `README.md` Lab Series table and Repository Structure sections mention the new
`labs/` and `specs/` directories (run `bash scripts/check-readme.sh` to verify; this is
also enforced by CI on every push and PR)

---

Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each lab is self-contained, cross-platform (Windows and macOS), and costs nothin
| [Lab 1](labs/lab-01-base-backstage/) | Base Backstage | Install Backstage locally; register a REST API (OpenAPI) and an event API (AsyncAPI); verify both are visible and searchable |
| [Lab 2](labs/lab-02-users-roles/) | Users, Roles & API Visibility | Add users, teams, and a custom permission policy; private APIs are visible only to their owning team, shared APIs are visible to everyone |
| [Lab 3](labs/lab-03-api-quality/) | API Quality | Add a shared Spectral ruleset, the api-grade quality plugin, and the Spectral linter plugin; API owners and a platform team see detailed quality/lint results, everyone else sees a summary grade |
| Lab 4 *(coming soon)* | Auto Registration | Auto-discover and register APIs from a Git mono-repo; pull catalog metadata from `x-*` fields in the spec itself |
| [Lab 4](labs/lab-04-auto-registration/) | Auto Registration | Auto-discover and register APIs from a Git mono-repo via a custom `EntityProvider`; source owner, lifecycle, and visibility metadata from `x-*` fields in the spec itself |
| Lab 5 *(coming soon)* | Mocking & Testing | Dynamically mock or exercise a test implementation of any registered API, with support for user-supplied non-production credentials |
| Lab 6 *(coming soon)* | API Lifecycle Management | Register multiple major versions of an API in parallel; track lifecycle state (development/test/production) and deprecation/retirement per version |
| Lab 7 *(coming soon)* | Other Documentation | Add the Thoughtworks Tech Radar plugin; register blips to plot your API landscape |
Expand Down Expand Up @@ -66,10 +66,14 @@ labs/
├── lab-02-users-roles/
│ ├── README.md ← continue here after Lab 1
│ └── catalog/ ← teams.yaml, users, and updated API owners/visibility
└── lab-03-api-quality/
├── README.md ← continue here after Lab 2
├── .spectral.yaml ← shared Spectral ruleset used by both quality plugins
└── catalog/ ← platform team member (eve) added in this lab
├── lab-03-api-quality/
│ ├── README.md ← continue here after Lab 2
│ ├── .spectral.yaml ← shared Spectral ruleset used by both quality plugins
│ └── catalog/ ← platform team member (eve) added in this lab
└── lab-04-auto-registration/
├── README.md ← continue here after Lab 3
├── autoApiRegistration.ts ← backend EntityProvider that scans and registers APIs
└── apis/ ← auto-discovered specs, incl. the Scalar Galaxy vendor copy
```

---
Expand All @@ -81,11 +85,13 @@ backstage-apiportal-lab/
├── labs/ ← one directory per lab
│ ├── lab-01-base-backstage/
│ ├── lab-02-users-roles/
│ └── lab-03-api-quality/
│ ├── lab-03-api-quality/
│ └── lab-04-auto-registration/
├── specs/ ← SDD artifacts (spec, plan, tasks per lab)
│ ├── 001-lab-1-base-backstage/
│ ├── 002-lab-2-users-roles/
│ └── 003-lab-3-api-quality/
│ ├── 003-lab-3-api-quality/
│ └── 004-lab-4-auto-registration/
├── .specify/ ← Speckit configuration and templates
├── GOAL.md ← high-level goals for the full lab series
├── CONTRIBUTING.md ← how to contribute new labs
Expand Down
155 changes: 17 additions & 138 deletions labs/lab-02-users-roles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,77 +319,19 @@ Run this command from inside your Backstage root directory

#### Create `ApiVisibilityCard.tsx`

Create `packages/app/src/modules/apiVisibility/ApiVisibilityCard.tsx` with the following
content:

```typescript
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { InfoCard } from '@backstage/core-components';
import { Typography, Box, Chip } from '@material-ui/core';

const VISIBILITY_ANNOTATION = 'example.com/visibility';

export function ApiVisibilityCard() {
const { entity } = useEntity();
const visibility = entity.metadata.annotations?.[VISIBILITY_ANNOTATION];

if (!visibility) {
return (
<InfoCard title="API Visibility">
<Typography variant="body2" color="textSecondary">
No visibility designation set. Under the permission policy, this API is visible
only to members of the owning team (treated as private by default).
</Typography>
</InfoCard>
);
}

const isShared = visibility === 'shared';

return (
<InfoCard title="API Visibility">
<Box display="flex" alignItems="center" gap={1}>
<Chip
label={isShared ? 'Shared' : 'Private'}
color={isShared ? 'primary' : 'default'}
size="small"
/>
<Typography variant="body2">
{isShared
? 'Visible to all authenticated users regardless of team membership.'
: 'Visible only to members of the owning team.'}
</Typography>
</Box>
</InfoCard>
);
}
```
Create `packages/app/src/modules/apiVisibility/ApiVisibilityCard.tsx` — its full content is
committed alongside this README at
[`code/packages/app/src/modules/apiVisibility/ApiVisibilityCard.tsx`](./code/packages/app/src/modules/apiVisibility/ApiVisibilityCard.tsx);
copy it in as-is. It reads the `example.com/visibility` annotation off the entity and renders a
"Shared"/"Private" chip, with a fallback message when the annotation is absent (treated as
private by the permission policy).

#### Create `index.ts`

Create `packages/app/src/modules/apiVisibility/index.ts` with the following content:

```typescript
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha';
import React from 'react';
import { ApiVisibilityCard } from './ApiVisibilityCard';

const apiVisibilityCard = EntityCardBlueprint.make({
name: 'api-visibility',
params: {
filter: 'kind:API',
type: 'info',
loader: async () => React.createElement(ApiVisibilityCard),
},
});

export const apiVisibilityModule = createFrontendModule({
pluginId: 'catalog',
extensions: [apiVisibilityCard],
});
```
Create `packages/app/src/modules/apiVisibility/index.ts` — its full content is committed at
[`code/packages/app/src/modules/apiVisibility/index.ts`](./code/packages/app/src/modules/apiVisibility/index.ts);
copy it in as-is. It wraps `ApiVisibilityCard` in an `EntityCardBlueprint` extension and bundles
it into a `createFrontendModule`.

> **Note on Alpha imports**: `EntityCardBlueprint` is imported from
> `@backstage/plugin-catalog-react/alpha`. This is the officially supported extension
Expand Down Expand Up @@ -599,76 +541,13 @@ For all non-catalog permissions (scaffolding, TechDocs, search, etc.), the polic

Create the directory `packages/backend/src/extensions/` inside your Backstage root
(`labs/lab-01-base-backstage/backstage/`), then create a new file called
`permissionPolicy.ts` inside it with the following content:

```typescript
// packages/backend/src/extensions/permissionPolicy.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
PolicyDecision,
AuthorizeResult,
isResourcePermission,
} from '@backstage/plugin-permission-common';
import {
PermissionPolicy,
PolicyQuery,
PolicyQueryUser,
} from '@backstage/plugin-permission-node';
import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
import {
catalogConditions,
createCatalogConditionalDecision,
} from '@backstage/plugin-catalog-backend/alpha';

class CatalogOwnershipPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: PolicyQueryUser,
): Promise<PolicyDecision> {
if (isResourcePermission(request.permission, 'catalog-entity')) {
return createCatalogConditionalDecision(
request.permission,
{
anyOf: [
// Rule 1: Non-API entities (User, Group, etc.) are always visible to all users.
// This keeps the org chart and team pages open for everyone.
{ not: catalogConditions.isEntityKind({ kinds: ['API'] }) },

// Rule 2: APIs annotated as shared are visible to all authenticated users,
// regardless of which team they belong to.
catalogConditions.hasAnnotation({
annotation: 'example.com/visibility',
value: 'shared',
}),

// Rule 3: Private APIs are visible only to members of the owning team.
// The user's ownershipEntityRefs contains their user ref plus all their
// group refs, resolved from the catalog User entity's memberOf list.
catalogConditions.isEntityOwner({
claims: user?.info.ownershipEntityRefs ?? [],
}),
],
},
);
}
// All non-catalog permissions (scaffolding, TechDocs, search) are unconditionally allowed.
return { result: AuthorizeResult.ALLOW };
}
}

export default createBackendModule({
pluginId: 'permission',
moduleId: 'permission-policy',
register(reg) {
reg.registerInit({
deps: { policy: policyExtensionPoint },
async init({ policy }) {
policy.setPolicy(new CatalogOwnershipPolicy());
},
});
},
});
```
`permissionPolicy.ts` inside it. Its full content is committed alongside this README at
[`code/packages/backend/src/extensions/permissionPolicy.ts`](./code/packages/backend/src/extensions/permissionPolicy.ts);
copy it in as-is. In short: it's a `CatalogOwnershipPolicy` that, for the `catalog-entity`
resource permission, allows non-API entities unconditionally, allows APIs annotated
`example.com/visibility: shared` for everyone, and otherwise falls back to an ownership check
via `catalogConditions.isEntityOwner`; every other permission is unconditionally allowed. It's
registered as a `permission-policy` backend module via `createBackendModule`.

**Import sources** (all packages are already installed in Lab 1 — no `npm install` needed):

Expand Down
Loading
Loading