Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ff027ce
feat: add native Radius frontend and standalone host
nicolejms Oct 26, 2026
811d2f0
test: complete native host catalog fixture and cleanup
nicolejms Oct 26, 2026
70abbb9
test: qualify native standalone image rollback
nicolejms Oct 26, 2026
4d94a3a
test: bind rollback qualification to image source tuple
nicolejms Oct 27, 2026
37c2469
docs: record verified FS-16 image tuple and evidence limits
nicolejms Oct 27, 2026
4f9d892
ci: run FS-16 image rollback check on the CI-built candidate
nicolejms Oct 27, 2026
a7aed3d
Merge origin/main into Phase 3 branch
nicolejms Oct 27, 2026
1f1629c
test: restore the previous image with its own journeys
nicolejms Oct 27, 2026
62f6f62
build: match public npm bin metadata in yarn.lock
nicolejms Oct 27, 2026
2fdf7ee
test: pull previous image by digest and allow cold page load
nicolejms Oct 27, 2026
8c41fb1
fix: resolve Phase 3 review dependency and cleanup findings
nicolejms Oct 27, 2026
d30482c
test: require cleanup regressions in the CI test chain
nicolejms Oct 27, 2026
652c3a8
Merge main into Phase 3 migration
nicolejms Oct 28, 2026
ddbc30c
fix: address native frontend review follow-ups
nicolejms Oct 28, 2026
2f871a2
fix(app): annotate frontend features type
Copilot Oct 28, 2026
bf80651
fix: align native Backstage package versions
nicolejms Oct 28, 2026
e2fcb36
test(app): assert guest sign-in failure state
nicolejms Oct 28, 2026
e9f4029
Merge branch 'main' into nicolejms-phase-3-backstage-migration
nicolejms Oct 28, 2026
6e900ad
fix: address native frontend review feedback
nicolejms Oct 30, 2026
b2b9fd1
fix: address CI failures
nicolejms Oct 30, 2026
cff8992
fix: register catalog API with its module
nicolejms Oct 30, 2026
d26f670
fix: stabilize rollback qualification coverage
nicolejms Oct 30, 2026
c3a92a1
fix: type catalog route binding
nicolejms Oct 30, 2026
b6545cc
test: isolate packed host from auth backend
nicolejms Oct 30, 2026
80c46d5
Merge branch 'main' into nicolejms-phase-3-backstage-migration
nicolejms Nov 2, 2026
0fbbfa9
fix: address new rollback review feedback
nicolejms Nov 2, 2026
4f3512f
fix: type shared test cluster config
nicolejms Nov 2, 2026
cb12f23
test: isolate rollback upstream in a dedicated container
nicolejms Nov 2, 2026
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
23 changes: 20 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,15 @@ jobs:
GRAPH_RECORD_BASE_REF: ${{ github.event.pull_request.base.sha }}
run: yarn run test:all

- name: Run E2E Tests
- name: Install Playwright browser
if: ${{ env.CI_TEST == 'true' }}
run: yarn exec playwright install --with-deps chromium

- name: Qualify Built Standalone Host
if: ${{ env.CI_TEST == 'true' }}
run: yarn run test:e2e
env:
NODE_ENV: production
run: yarn run test:e2e:standalone

- name: Run Component Browser Tests
if: ${{ env.CI_TEST == 'true' }}
Expand All @@ -91,7 +97,7 @@ jobs:
build-and-publish-container:
name: Build and Publish Container
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 25
concurrency:
group: dashboard-container-${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.ref || github.run_id }}
cancel-in-progress: false
Expand Down Expand Up @@ -165,6 +171,17 @@ jobs:
if: ${{ always() }}
run: docker stop dashboard-e2e || true

# TODO(#421): remove this gate and pinned image after the first native release restore run.
- name: Qualify image rollback
continue-on-error: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
env:
PREVIOUS_IMAGE: ghcr.io/radius-project/dashboard@sha256:166dcc84f8399b6c7b2e060c22eda390763d9379001e24695a961154679daa44
Comment thread
nicolejms marked this conversation as resolved.
Resolved
PREVIOUS_REVISION: 4f243cc02166e1330fe5769683d2a70aca23ded1
run: |
git fetch --no-tags --depth=1 origin "${PREVIOUS_REVISION}"
candidate="$(docker image inspect "${BUILT_IMAGE}" --format '{{.Id}}')"
yarn run test:e2e:rollback "${PREVIOUS_IMAGE}" "${PREVIOUS_REVISION}" "${candidate}" "$(git rev-parse HEAD)"

- name: Analyze Image
uses: ./.github/actions/analyze-image
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ The `build-image` command uses `packages/backend/Dockerfile`. Its Docker build r
not `GITHUB_SHA`) and `org.opencontainers.image.version` from `REL_VERSION`, defaulting to
`edge` for local builds. A Git checkout is required to produce this provenance.

The Build workflow runs FS-16 on pull requests and `main`. Issue
[radius-project/dashboard#421](https://github.com/radius-project/dashboard/issues/421)
tracks removal of this automatic check and its pinned previous image. Remove them after the
first native-frontend release is published and a successful candidate-and-restore run is
recorded for that release's source revision. Keep the manual rollback runner and legacy
root-entry artifact tests until their retirement is approved separately.

#### Release image staging

The Build workflow stages release images under full-version tags. It does not publish
Expand Down Expand Up @@ -233,6 +240,10 @@ Attestation-bearing indexes are covered for both reuse and post-push verificatio
including rejection of malformed descriptors, extra runnable platforms, and
attestation-only indexes.

The native frontend qualification pins selected Backstage packages to a tested
version set. These exact versions define the qualification set; they are not a
public support range.

**Run E2E tests:**

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ This file is checked in but `.gitignored`'d. Feel free to make changes as needed

The `app-config.dashboard.yaml` configuration is used when deployed as part of a Radius installation.

### Frontend qualification

`@internal/plugin-radius/alpha` is the native Radius frontend entry. The root entry remains
available to legacy hosts. The standalone app owns guest sign-in.

Build and qualify the standalone host with:

```bash
yarn workspace @internal/app build
NODE_ENV=production yarn test:e2e:standalone
```

Run the image restore qualification with immutable image identities:

```bash
yarn test:e2e:rollback <previous image@digest> <previous revision> <candidate image ID or image@digest> <candidate revision>
```

## Developing: rad-components

**Launch Storybook to experiment with rad-components:**
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "yarn test:release-images && backstage-cli repo test --coverage",
"test:all": "yarn test:release-images && yarn test:qualification-runners && backstage-cli repo test --coverage",
"test:release-images": "node --test scripts/release-image.test.ts",
"test:qualification-runners": "node --test scripts/qualify-plugin-host.test.ts",
"test:e2e": "playwright test",
"test:e2e:standalone": "node scripts/qualify-standalone-host.ts",
"test:e2e:rollback": "node scripts/qualify-standalone-rollback.ts",
"test:e2e:components": "playwright test --config playwright.components.config.ts",
"test:package": "node scripts/test-package.js",
"fix": "backstage-cli repo fix",
Expand Down Expand Up @@ -61,6 +64,7 @@
"jsonpath-plus": "^10.3.0",
"mysql2": "^3",
"adm-zip@npm:0.6.0": "0.6.1",
"uuid@npm:^10.0.0": "11.1.1",
"undici@npm:7.29.0": "7.29.1"
},
"prettier": "@spotify/prettier-config",
Expand Down
19 changes: 5 additions & 14 deletions packages/app/e2e-tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,10 @@ import { test, expect } from '@playwright/test';
test('App should render the home page', async ({ page }) => {
await page.goto('/');

// Accept any dialog that appears during the guest sign-in flow.
// Backstage may show a confirm dialog: "Failed to sign in as a guest
// using the auth backend. Do you want to fallback to the legacy guest token?"
// Accepting it allows the test to proceed with the legacy guest token.
page.on('dialog', dialog => dialog.accept());

// Click the Enter button on the sign-in page to complete guest authentication.
const enterButton = page.getByRole('button', { name: 'Enter' });
await enterButton.waitFor({ state: 'visible' });
await enterButton.click();

// Verify home page content is visible
await expect(page.getByText('Learn More')).toBeVisible();
await expect(page.getByText('Join the Community')).toBeVisible();
await expect(page.getByText(/Learn more/i)).toBeVisible({ timeout: 15_000 });
await expect(page.getByText(/Join the community/i)).toBeVisible();
await expect(page.getByText('Get help with Radius')).toBeVisible();
await expect(
page.getByRole('button', { name: 'Enter', exact: true }),
).toHaveCount(0);
});
209 changes: 209 additions & 0 deletions packages/app/e2e-tests/nativeHost.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { test, expect } from '@playwright/test';

test.describe('standalone frontend contracts', () => {
test('FS-09: guest sign-in failure shows an error without a legacy-token dialog', async ({
page,
}) => {
const dialogs: string[] = [];
let guestSignInStatus: number | undefined;
page.on('dialog', async dialog => {
dialogs.push(dialog.message());
await dialog.dismiss();
});
page.on('response', response => {
if (response.url().includes('/api/auth/guest/refresh')) {
guestSignInStatus = response.status();
}
});
await page.route('**/api/auth/guest/refresh*', route =>
route.fulfill({
status: 500,
json: {
error: {
name: 'Error',
message: 'Guest sign-in test failure',
},
},
}),
);

await page.goto('/');
const errorPanel = page.getByRole('alert');
await expect(errorPanel).toBeVisible({ timeout: 20_000 });
await expect(
errorPanel.getByRole('button', {
name: /You do not appear to be signed in/,
}),
).toBeVisible();
expect(guestSignInStatus).toBe(500);
await expect(page.getByText('Learn more')).toHaveCount(0);
expect(dialogs).toEqual([]);
});

test('FS-09/11/12: auto guest, cards, sidebar, settings, themes and native catalog pages', async ({
page,
}) => {
const errors: string[] = [];
const dialogs: string[] = [];
page.on('pageerror', error => errors.push(error.message));
page.on('dialog', async dialog => {
dialogs.push(dialog.message());
await dialog.dismiss();
});
const signIns: number[] = [];
page.on('response', response => {
if (response.url().includes('/api/auth/guest/refresh'))
signIns.push(response.status());
});
await page.goto('/');
await expect(page.getByText('Learn more')).toBeVisible({ timeout: 20_000 });
if (process.env.STANDALONE_QUALIFICATION) expect(signIns).toContain(200);
await expect(page.getByText('Join the community')).toBeVisible();
await expect(page.getByText('Get help with Radius')).toBeVisible();
await expect(
page.getByRole('main').getByText('Applications', { exact: true }),
).toBeVisible();
await expect(
page.getByRole('main').getByText('Environments', { exact: true }),
).toBeVisible();
await expect(
page.getByRole('button', { name: /^Get Started/ }),
).toHaveAttribute('href', 'https://docs.radapp.io/getting-started/');
const nav = page.getByRole('navigation', { name: 'sidebar nav' });
for (const [name, path] of [
['Home', '/'],
['Resource Types', '/resource-types'],
['Applications', '/applications'],
['Environments', '/environments'],
['Resources', '/resources'],
['Recipes', '/recipes'],
]) {
await expect(
nav.getByRole('link', { name, exact: true }).filter({ hasText: name }),
).toHaveCount(1);
await expect(
nav.getByRole('link', { name, exact: true }).filter({ hasText: name }),
).toHaveAttribute('href', path);
}
await page.goto('/settings');
await expect(page.getByText('Appearance')).toBeVisible({ timeout: 15_000 });
await page.getByRole('button', { name: 'Dark', exact: true }).click();
await expect(page.locator('body')).toHaveAttribute(
'data-theme-mode',
'dark',
);
await page.reload();
await expect(page.locator('body')).toHaveAttribute(
'data-theme-mode',
'dark',
);
await page.getByRole('button', { name: 'Light', exact: true }).click();
await expect(page.locator('body')).toHaveAttribute(
'data-theme-mode',
'light',
);
await page.getByRole('tab', { name: 'Feature Flags' }).click();
await expect(
page.getByText('radius-catalog', { exact: true }),
).toBeVisible();
await page.goto('/catalog');
await expect(
page.getByRole('heading', { name: /Catalog/, level: 1 }),
).toBeVisible({ timeout: 15_000 });
await page.goto('/catalog-graph');
await expect(
page.getByRole('heading', { name: /Catalog Graph/, level: 1 }),
).toBeVisible({ timeout: 15_000 });
expect(dialogs).toEqual([]);
expect(errors).toEqual([]);
});

test('FS-12: real backend keeps missing authentication distinct from upstream denial', async ({
request,
}) => {
test.skip(
!process.env.STANDALONE_QUALIFICATION,
'Requires the controlled real backend',
);
const denied = await request.get('/api/kubernetes/proxy/forbidden');
expect(denied.status()).toBe(401);
const signIn = await request.get('/api/auth/guest/refresh');
expect(signIn.status()).toBe(200);
const { backstageIdentity } = await signIn.json();
const forbidden = await request.get('/api/kubernetes/proxy/forbidden', {
headers: {
authorization: `Bearer ${backstageIdentity.token}`,
'Backstage-Kubernetes-Cluster': 'standalone-test-cluster',
},
});
expect(forbidden.status()).toBe(403);
});

test('FS-11: installed catalog entity page and graph resolve the same entity', async ({
page,
}) => {
const catalogErrors: string[] = [];
page.on('response', response => {
if (
response.status() >= 400 &&
response.url().includes('/api/catalog/')
) {
catalogErrors.push(`${response.status()} ${response.url()}`);
}
});
const entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'standalone-component',
namespace: 'default',
uid: 'standalone-component',
description: 'Controlled catalog entity for native host qualification',
},
spec: {
type: 'service',
lifecycle: 'experimental',
owner: 'user:default/guest',
},
relations: [],
};
await page.route(
'**/api/catalog/entities/by-name/component/default/standalone-component',
route => route.fulfill({ json: entity }),
);
await page.route('**/api/catalog/entities/by-refs', route =>
route.fulfill({ json: { items: [entity] } }),
);
await page.route(
'**/api/catalog/entities/by-name/component/default/standalone-component/ancestry',
route =>
route.fulfill({
json: {
rootEntityRef: 'component:default/standalone-component',
items: [{ entity, parentEntityRefs: [] }],
},
}),
);
await page.goto('/catalog/default/component/standalone-component');
await expect(
page.getByRole('heading', { name: 'standalone-component', exact: true }),
).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(entity.metadata.description)).toBeVisible({
timeout: 15_000,
});
await page.reload();
await expect(
page.getByRole('heading', { name: 'standalone-component', exact: true }),
).toBeVisible();
await page.goto(
'/catalog-graph?rootEntityRefs[]=component:default/standalone-component',
);
await expect(
page.getByRole('heading', { name: /Catalog Graph/, level: 1 }),
).toBeVisible({ timeout: 15_000 });
await expect(
page.locator('svg').getByText('standalone-component', { exact: true }),
).toBeVisible();
expect(catalogErrors).toEqual([]);
});
});
7 changes: 6 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@
"lint": "backstage-cli package lint"
},
"dependencies": {
"@backstage/app-defaults": "^1.7.12",
"@backstage/catalog-client": "^1.16.2",
"@backstage/catalog-model": "^1.10.1",
"@backstage/cli": "^0.36.6",
"@backstage/core-app-api": "^1.20.5",
"@backstage/core-compat-api": "0.5.16",
"@backstage/core-components": "^0.18.14",
"@backstage/core-plugin-api": "^1.12.10",
"@backstage/frontend-defaults": "0.5.6",
"@backstage/frontend-plugin-api": "0.18.2",
"@backstage/integration-react": "^1.2.22",
"@backstage/plugin-app-react": "0.2.8",
"@backstage/plugin-catalog": "^2.0.9",
"@backstage/plugin-catalog-common": "^1.2.0",
"@backstage/plugin-catalog-graph": "^0.6.8",
"@backstage/plugin-catalog-react": "^3.2.3",
"@backstage/plugin-home": "^0.9.10",
"@backstage/plugin-home-react": "0.1.43",
"@backstage/plugin-kubernetes": "^0.12.23",
"@backstage/plugin-permission-react": "^0.5.5",
"@backstage/plugin-user-settings": "^0.9.7",
Expand All @@ -45,6 +49,7 @@
"devDependencies": {
"@backstage/cli-defaults": "^0.1.6",
"@backstage/config": "^1.3.9",
"@backstage/frontend-test-utils": "0.6.4",
"@backstage/test-utils": "^1.7.22",
"@playwright/test": "^1.64.1",
"@testing-library/dom": "^10.4.2",
Expand Down
Loading
Loading