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
4 changes: 3 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"@openchoreo/backstage-plugin-thunder-idp-client-node",
"@openchoreo/backstage-plugin-catalog-backend-module-openchoreo-users",
"@openchoreo/cell-diagram",
"@openchoreo/backstage-portal-backend"
"@openchoreo/backstage-portal-backend",
"@openchoreo/backstage-portal-app",
"@openchoreo/create-portal"
]
],
"access": "public",
Expand Down
11 changes: 11 additions & 0 deletions .changeset/create-portal-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@openchoreo/create-portal': minor
---

New `npx @openchoreo/create-portal` CLI that scaffolds a custom OpenChoreo
Portal: a thin Backstage app on the published portal packages, pinned to one
release. The template is rendered from the live monorepo at pack time
(private assistant wiring stripped, `workspace:^` ranges pinned to the
lockstep release version), ships inside the CLI tarball, and is pushed to the
`openchoreo/portal-template` repo per release as the `git merge` upgrade
Comment thread
coderabbitai[bot] marked this conversation as resolved.
base.
21 changes: 21 additions & 0 deletions .changeset/portal-app-publishable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@openchoreo/backstage-portal-app': minor
'@openchoreo/backstage-plugin-react': minor
'@openchoreo/backstage-plugin': patch
'@openchoreo/backstage-plugin-openchoreo-ci': patch
---

Make `@openchoreo/backstage-portal-app` publishable. The portal shell no
longer depends on the private Portal Assistant plugin: the assistant
integration contract (`portalAssistantIntegrationApiRef` /
`usePortalAssistant`, re-exported by the shell) now lives in
`@openchoreo/backstage-plugin-react`, and the stock portal app injects the
assistant through it via `createPortalApp({ features })`, mirroring how the
backend adds the assistant outside `portalBackendFeatures`. Without a
registered integration every slot renders nothing.

This also restores the two assistant surfaces dropped by the NFS entity-page
migration: the component Overview and Build tabs mount the
`BuildFailureNotifier` slot again, and the deploy panel's investigate action
falls back to the integration's `renderInvestigateAction` when no prop is
passed.
76 changes: 75 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,83 @@ jobs:

echo "Published ${published}, skipped ${skipped}."

publish-template:
name: Verify scaffold and publish template
needs: [build, publish-npm]
runs-on: ubuntu-24.04
permissions:
contents: read
env:
RELEASE_TAG: ${{ needs.build.outputs.release-tag }}
IS_PRERELEASE: ${{ needs.build.outputs.is-prerelease }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: 'yarn'

- name: Install dependencies
# This job runs the create-portal CLI from the repo checkout, which
# needs the workspace's node_modules. It holds no OIDC token (npm
# publishing already finished in `publish-npm`), so dependency
# lifecycle scripts here cannot reach a publish credential.
run: yarn install --immutable

- name: Render the portal template
# templates/ is gitignored (rendered at pack time, not committed), so
# a fresh checkout must render it before the CLI can run.
run: yarn workspace @openchoreo/create-portal generate-template

# Runs AFTER publish (the scaffold's pinned ^X.Y.Z versions must exist
# on the registry) and BEFORE the template push / Docker retag, so a
# broken scaffold aborts the release before anything user-visible moves.
- name: Smoke-test the portal scaffold
run: |
node packages/create-portal/bin/create-portal \
--name openchoreo-portal --path /tmp/verify-portal --skip-install
cd /tmp/verify-portal
yarn install --no-immutable
yarn tsc

- name: Push template to openchoreo/portal-template
env:
TEMPLATE_PUSH_TOKEN: ${{ secrets.PORTAL_TEMPLATE_PUSH_TOKEN }}
run: |
if [ -z "${TEMPLATE_PUSH_TOKEN}" ]; then
echo "::warning::PORTAL_TEMPLATE_PUSH_TOKEN not set — skipping the portal-template publish"
exit 0
fi
node packages/create-portal/bin/create-portal \
--name openchoreo-portal --path /tmp/portal-template-render --skip-install
rm -rf /tmp/portal-template-render/.git
git clone "https://x-access-token:${TEMPLATE_PUSH_TOKEN}@github.com/openchoreo/portal-template.git" /tmp/portal-template-repo
# Release retries re-run this step; the tag from the first attempt
# already exists, so skip instead of failing the whole release.
if git -C /tmp/portal-template-repo rev-parse -q --verify "refs/tags/${RELEASE_TAG}" > /dev/null; then
echo "::notice::portal-template already has ${RELEASE_TAG} — skipping the template publish"
exit 0
fi
rsync -a --delete --exclude .git /tmp/portal-template-render/ /tmp/portal-template-repo/
cd /tmp/portal-template-repo
git add -A
git -c user.name=openchoreo-bot -c user.email=bot@openchoreo.dev \
commit -m "chore: portal template for ${RELEASE_TAG}" --allow-empty
git tag "${RELEASE_TAG}"
# Stable releases advance main; prereleases advance a force-pushed
# `next` branch so main only ever contains released templates.
if [ "${IS_PRERELEASE}" = "true" ]; then
git push --force origin HEAD:next "${RELEASE_TAG}"
else
git push origin HEAD:main "${RELEASE_TAG}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi

retag-image:
name: Retag release image
needs: [build, publish-npm]
needs: [build, publish-npm, publish-template]
runs-on: ubuntu-24.04
permissions:
packages: write
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ site
e2e-test-report/


.backstage-db/
.backstage-db/
# create-portal rendered template (generated at prepack/CI; templates-src/ is the source)
packages/create-portal/templates/
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ dist-types
coverage
.vscode
storybook-static
packages/create-portal/templates/
packages/create-portal/templates-src/
2 changes: 1 addition & 1 deletion app-config.local.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ openchoreo:
secretManagement:
enabled: true # Set to false to hide Secrets settings tab
assistant:
enabled: true # Opt-in. Set to true after deploying perch-agent and OPENCHOREO_PERCH_AGENT_URL
enabled: false # Opt-in. Set to true after deploying the Portal Assistant and uncommenting `portalAssistantUrl` above

# Thunder IDP configuration (k3d cluster)
thunder:
Expand Down
7 changes: 7 additions & 0 deletions app-config.production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ auth:
dangerouslyAllowOutsideDevelopment: ${BACKSTAGE_GUEST_DANGEROUSLY_ALLOW_OUTSIDE_DEVELOPMENT}

catalog:
# The Dockerfile ships this file AS /app/app-config.yaml (it replaces the
# base config, not layers on it), so the global kind allowlist must be
# restated here — without it Backstage's defaults apply, which exclude the
# OpenChoreo kinds.
rules:
- allow:
[Component, System, API, Resource, Location, User, Group, Environment]
locations:
# Placeholder groups for Backstage entity owner references
- type: file
Expand Down
4 changes: 4 additions & 0 deletions examples/template/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ spec:
title: Name
type: string
description: Unique name of the component
# Rendered into content/package.json and content/index.js — keep it
# package-name safe.
pattern: '^[a-z0-9][a-z0-9-]*$'
maxLength: 63
ui:autofocus: true
ui:options:
rows: 5
Expand Down
13 changes: 13 additions & 0 deletions packages/app/e2e-tests/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'];
// is present. If neither button shows up, the session is already live
// and we proceed.
async function dismissSignIn(page: import('@playwright/test').Page) {
// Bounded wait for either a sign-in control or the post-login sidebar —
// `isVisible()` returns immediately, so checking straight after `goto`
// races SPA hydration and could skip the sign-in click entirely.
await page
.getByRole('button', { name: /^(Enter|Sign In)$/ })
.or(
page.locator(
'nav[aria-label*="sidebar" i], a[href="/"][aria-label="Home"]',
),
)
.first()
.waitFor({ state: 'visible', timeout: 15_000 })
.catch(() => undefined);
for (const name of ['Enter', 'Sign In'] as const) {
const btn = page.getByRole('button', { name });
if (await btn.isVisible().catch(() => false)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
},
"dependencies": {
"@backstage/cli": "^0.36.2",
"@backstage/core-plugin-api": "^1.12.6",
"@backstage/frontend-plugin-api": "^0.17.0",
"@backstage/ui": "^0.15.0",
"@openchoreo/backstage-plugin-openchoreo-observability": "workspace:^",
"@openchoreo/backstage-plugin-openchoreo-portal-assistant": "workspace:^",
"@openchoreo/backstage-portal-app": "workspace:^",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createPortalApp } from '@openchoreo/backstage-portal-app';
import { assistantFeature } from './assistant';

export default createPortalApp().createRoot();
export default createPortalApp({ features: [assistantFeature] }).createRoot();
30 changes: 30 additions & 0 deletions packages/app/src/assistant.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
AssistantDrawerProvider,
FailedBuildSnackbar,
PerchAgentClient,
} from '@openchoreo/backstage-plugin-openchoreo-portal-assistant';
import { assistantFeature, assistantIntegration } from './assistant';

describe('assistant wiring', () => {
it('fills every shell integration slot', () => {
expect(assistantIntegration.AppWrapper).toBe(AssistantDrawerProvider);
expect(assistantIntegration.BuildFailureNotifier).toBe(FailedBuildSnackbar);
expect(assistantIntegration.renderInvestigateAction).toEqual(
expect.any(Function),
);
});

it('constructs the perch client the api factory registers', () => {
// Replaces the factory coverage that lived in portal-app's apis.test.ts
// before the assistant moved out of the published shell.
const client = new PerchAgentClient({
discoveryApi: { getBaseUrl: async () => 'http://localhost' } as any,
fetchApi: { fetch: (() => undefined) as any } as any,
});
expect(client).toBeInstanceOf(PerchAgentClient);
});

it('exposes the assistant as an installable frontend feature', () => {
expect(assistantFeature).toBeDefined();
});
});
78 changes: 78 additions & 0 deletions packages/app/src/assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
ApiBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
import {
portalAssistantIntegrationApiRef,
PortalAssistantIntegration,
} from '@openchoreo/backstage-portal-app';
import { LogRowActionBlueprint } from '@openchoreo/backstage-plugin-openchoreo-observability/alpha';
import {
AssistantDrawerProvider,
FailedBuildSnackbar,
InvestigateDependencyButton,
InvestigateLogButton,
PerchAgentClient,
perchAgentApiRef,
} from '@openchoreo/backstage-plugin-openchoreo-portal-assistant';

// What the assistant contributes to the shell's optional integration slots:
// the global drawer provider, the failed-build prompt on entity Overview and
// Build tabs, and the deploy panel's investigate action.
export const assistantIntegration: PortalAssistantIntegration = {
AppWrapper: AssistantDrawerProvider,
BuildFailureNotifier: FailedBuildSnackbar,
renderInvestigateAction: scope => <InvestigateDependencyButton {...scope} />,
};

/**
* Wires the Portal Assistant (Perch) into the portal shell. The assistant is
* a PRIVATE plugin — deliberately not part of the published
* `@openchoreo/backstage-portal-app` bundle — so the stock portal injects it
* here, mirroring how packages/backend adds the assistant backend outside
* `portalBackendFeatures`. A custom portal scaffold simply omits this module.
*/
export const assistantFeature = createFrontendModule({
pluginId: 'app',
extensions: [
// Perch agent client. NOTE: ``perchAgentApiRef`` is also declared on
// ``openchoreoPerchPlugin.apis``, but that declaration is never picked
// up at runtime — the plugin exports plain React components and never
// registers a routable or component extension, so the plugin loader
// never visits its ``apis`` array. This factory is the one actually
// wired in; removing it causes ``NotImplementedError: No implementation
// available for apiRef{plugin.openchoreo-portal-assistant.service}`` in
// AssistantDrawerProvider.
ApiBlueprint.make({
name: 'perch-agent',
params: defineParams =>
defineParams({
api: perchAgentApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new PerchAgentClient({ discoveryApi, fetchApi }),
}),
}),
ApiBlueprint.make({
name: 'assistant-integration',
params: defineParams =>
defineParams({
api: portalAssistantIntegrationApiRef,
deps: {},
factory: () => assistantIntegration,
}),
}),
// Per-row "investigate" action for the observability runtime-logs tables.
// Registered here (not inside the observability plugin) so observability
// owns no dependency on perch — the host composes the two.
LogRowActionBlueprint.make({
name: 'investigate-log',
params: {
renderer: (log, getLogsSnapshot) => (
<InvestigateLogButton log={log} getLogsSnapshot={getLogsSnapshot} />
),
},
}),
],
});
5 changes: 4 additions & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ backend.add(portalBackendFeatures);
// For production, config is in app-config.production.yaml with Helm-injected env vars
// backend.add(import('@immobiliarelabs/backstage-plugin-gitlab-backend'));

// portal-template:strip-start
// Portal Assistant backend — forwards Portal Assistant frontend traffic to the
// portal-assistant service in the OpenChoreo control plane. Plugin
// self-disables when openchoreo.portalAssistantUrl is not set.
// (Private package — deliberately not part of the published portal bundle.)
// (Private package — deliberately not part of the published portal bundle;
// the strip markers let the create-portal template generator drop this block.)
backend.add(
import('@openchoreo/backstage-plugin-openchoreo-portal-assistant-backend'),
);
// portal-template:strip-end

backend.start();
6 changes: 6 additions & 0 deletions packages/create-portal/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
ignorePatterns: ['templates/**', 'templates-src/**', 'scripts/**'],
rules: {
'no-console': 0,
},
});
Loading
Loading