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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Docker build context is this repo root (docker build -f showcase/Dockerfile .).
# Keep it small: drop dependencies, build output, VCS, secrets, logs.
**/node_modules
Comment thread
aaitor marked this conversation as resolved.
**/.next
**/out
**/.git
**/.env
**/.env.*
**/*.log
**/.DS_Store
**/coverage
# Python tutorials leave these locally (Poetry venvs, bytecode) — never needed in context.
**/.venv
**/__pycache__
**/*.pyc
149 changes: 149 additions & 0 deletions .github/workflows/showcase-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: Showcase image

# Builds the tutorials showcase (showcase/Dockerfile) and pushes it to Google
# Artifact Registry so ArgoCD can deploy it. Keyless auth via Workload Identity
# Federation (same SA/provider the other Nevermined repos use — no secrets).
#
# pull_request → verify only (build + sandbox self-check); no credentials, no push.
# push to main → verify, then build & push.
# dispatch → verify, then build & push (optional extra semver tag).
#
# Versioning (immutable-first — the AR repo has immutableTags=true, so a moving
# `latest` tag would be rejected on the second build; we don't publish one):
# - sha-<short> every build — the immutable tag ArgoCD should pin in production
# - <version> optional, when run manually with a version input (e.g. 1.0.0)

on:
Comment thread
aaitor marked this conversation as resolved.
pull_request:
paths:
- "showcase/**"
- ".github/workflows/showcase-image.yml"
push:
branches: [main]
paths:
- "showcase/**"
- "catalog/**/*.mp4"
- ".github/workflows/showcase-image.yml"
workflow_dispatch:
inputs:
version:
description: "Optional explicit semver tag to also publish (e.g. 1.0.0)"
required: false
type: string

# Superseded PR pushes shouldn't keep burning a runner. Cancel only in-flight PR
# runs (grouped per ref, so distinct PRs don't cancel each other); never cancel a
# push/dispatch run — those publish the immutable image and must finish.
concurrency:
group: showcase-image-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
IMAGE: europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-showcase
AR_LOCATION: europe-west3
PROJECT_ID: nevermined-eu-dev
SERVICE_ACCOUNT: github-actions-service-account@nevermined-eu-dev.iam.gserviceaccount.com
WORKLOAD_IDENTITY_PROVIDER: projects/112425687177/locations/global/workloadIdentityPools/github/providers/github-actions

permissions:
contents: read
id-token: write

jobs:
# Runs on every PR and every push. `next build` type-checks the 1,000+ line
# content array, and the sandbox self-check covers the x402 handshake paths —
# neither needs credentials, so a broken PR fails here instead of on main.
verify:
name: Verify (build + sandbox self-check)
runs-on: ubuntu-latest
defaults:
run:
working-directory: showcase
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: showcase/package-lock.json

- name: Install
# setup-node already restores ~/.npm, so every tarball is on disk. Stop
# npm ci from hitting the network for data it has: --prefer-offline skips
# registry revalidation, --no-audit/--no-fund skip the metadata round-trips
# that were stalling install (measured 22s ↔ 421s variance on identical
# inputs — the slow runs hung on the audit/fund calls, not on downloads).
run: npm ci --prefer-offline --no-audit --no-fund

- name: Sandbox agent self-check
run: node lib/demo-agent.mjs

- name: Build (type-checks content/tutorials.ts)
run: npm run build

# Only publishes on main / dispatch — never on a pull request.
build-push:
name: Build & push showcase image
runs-on: ubuntu-latest
needs: verify
if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Docker metadata (tags + labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=sha,prefix=sha-,format=short
type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }}

- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@v2
with:
token_format: access_token
project_id: ${{ env.PROJECT_ID }}
service_account: ${{ env.SERVICE_ACCOUNT }}
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}

- name: Log in to Artifact Registry
uses: docker/login-action@v3
with:
registry: ${{ env.AR_LOCATION }}-docker.pkg.dev
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build & push
uses: docker/build-push-action@v5
with:
# context is the repo root so the Dockerfile can pull the catalog demo media
context: .
file: ./showcase/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
sbom: false

- name: Summary
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
{
echo "### Pushed tutorials-showcase image"
echo '```'
echo "$TAGS"
echo '```'
echo "Pin the immutable sha-* tag in ArgoCD for production."
} >> "$GITHUB_STEP_SUMMARY"
16 changes: 16 additions & 0 deletions showcase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# deps / build
node_modules
.next
out
*.tsbuildinfo
next-env.d.ts

# env
.env
.env*.local

# large demo video — pull with `npm run sync:media` (kept out of git)
public/media/**/*.mp4

# internal design-review snapshots (Impeccable)
.impeccable/
43 changes: 43 additions & 0 deletions showcase/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Nevermined Tutorials showcase — production image (Next.js standalone).
#
# Build from the REPO ROOT so the catalog demo media (committed under catalog/)
# is in the build context:
#
# docker build -f showcase/Dockerfile -t nvm-tutorials-showcase .
# docker run -p 3000:3000 nvm-tutorials-showcase
#
# The container listens on $PORT (default 3000) on 0.0.0.0 — ready for a k8s
# Service / ArgoCD Deployment.

# ---- dependencies ----
FROM node:20-alpine AS deps
WORKDIR /app
COPY showcase/package.json showcase/package-lock.json ./
RUN npm ci

# ---- build ----
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY showcase/ ./
# The recap videos are gitignored under showcase/public — pull them from the
# committed catalog/ demos so the recap panels play in the image.
COPY catalog/song-from-the-headlines/song-from-the-headlines.mp4 ./public/media/song-from-the-headlines/
COPY catalog/diligence-in-a-box/diligence-in-a-box.mp4 ./public/media/diligence-in-a-box/
RUN npm run build

# ---- runner ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs

COPY --from=build /app/public ./public
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
120 changes: 120 additions & 0 deletions showcase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Nevermined Tutorials — Showcase

A visual showcase of the Nevermined Payments tutorials. Every paid-agent and catalog
tutorial in this repo is presented in one normalized shape — **what you'll learn · how it
works · under the hood · see it run** — behind a persistent left sidebar, with an
individual page for each. (`langchain-chat-ui-nvm` is the browser *buyer* front-end for the
LangChain agents rather than a paid agent itself, so it's referenced inside those pages
instead of getting its own entry.)

Built with Next.js (App Router) + TypeScript. Clean, light, docs-style UI on a white
ground with a restrained Nevermined-teal accent. Intended to become the canonical
replacement for `examples.nevermined.app`.

## Quick start

```bash
cd showcase
npm install
npm run sync:media # copy catalog demo video/audio into public/ (recap panels)
npm run dev # http://localhost:3000
```

Build the production bundle:

```bash
npm run build && npm start
```

## Docker (for ArgoCD / k8s)

The app builds to a Next.js **standalone** server. Build the image from the **repo
root** (so the committed `catalog/` demo videos are in context):

```bash
docker build -f showcase/Dockerfile -t nvm-tutorials-showcase .
docker run -p 3000:3000 nvm-tutorials-showcase
```

The container listens on `$PORT` (default `3000`) on `0.0.0.0`, runs as a non-root
user, and needs no build args or secrets — point a k8s Service / ArgoCD Deployment
at it.

### CI → Artifact Registry

`.github/workflows/showcase-image.yml` has two jobs:

- **`verify`** runs on every **pull request** (and every push): `npm ci`, the sandbox
self-check (`node lib/demo-agent.mjs`), and `npm run build` — which type-checks
`content/tutorials.ts`. It needs no credentials, so a broken change fails the PR
instead of landing on `main` and breaking the image build.
- **`build-push`** runs only on **push to `main`** touching `showcase/**` (and on manual
dispatch), never on a PR. It builds the image and pushes it to
`europe-west3-docker.pkg.dev/nevermined-eu-dev/nevermined-io/tutorials-showcase`
(keyless, via Workload Identity Federation).

Tags: an immutable `sha-<short>` per build (**pin this in ArgoCD for production**) and an
optional semver when dispatched with a `version` input. There is no moving `latest` tag —
the registry has `immutableTags=true`, which would reject a second `latest` push.

## How content works

All tutorial content lives in one typed array: [`content/tutorials.ts`](./content/tutorials.ts),
shaped by [`lib/types.ts`](./lib/types.ts). Each entry is sourced from that tutorial's own README.
Adding or editing a tutorial is a data change — no new components. Pages are generated statically
from the array (`generateStaticParams`).

Each tutorial declares a **tier**:

- **`live`** — the `See it run` panel is **functional in the browser**: it does real fetch
round-trips through `/api/agent` and runs the actual x402 handshake — `402 → authorize →
200 + settlement` — with a real per-session credit balance that decrements per call and
responses that react to what you type. It talks to a **local sandbox agent**
(`lib/demo-agent.mjs`), so it spends no real money and needs no credentials or backend.
- **`recap`** — the two `catalog/` demos spend real crypto autonomously across chains, so they are
**watch-only**: embedded video, playable outputs, the on-chain receipt, and a "run it locally"
note.

## The sandbox agent, and going fully real

`/api/agent` (backed by `lib/demo-agent.mjs`) is a local sandbox: it speaks the real x402 shape
and tracks a real per-session balance in an httpOnly cookie, but calls no external service. That
makes every "see it run" panel functional out of the box, with no credentials.

To make a tutorial run against the **real** Nevermined flow (visitor pays via a card delegation,
real agent responds):

1. Deploy its agent backend and note the URL + plan id.
2. In `app/api/agent/route.ts`, proxy that tutorial's requests to the backend instead of the
sandbox, porting the four x402 proxy routes from `../langchain-chat-ui-nvm/src/app/api/`
(session → token → init → passthrough) — they inject the buyer's x402 token server-side, so
`NVM_API_KEY` never reaches the browser.

The panel code doesn't change — only what `/api/agent` talks to.

Unit test for the handshake logic: `node lib/demo-agent.mjs`.

## Media

`npm run sync:media` copies the catalog `.mp4/.mp3/.jpg` into `public/media/`. The large `.mp4`s are
gitignored; the cover art and song are small enough to commit so a fresh clone still shows them.

## Structure

```
showcase/
├── app/
│ ├── layout.tsx # builds the sidebar groups + wraps every page in AppShell
│ ├── page.tsx # overview — intro + grouped index of all tutorials
│ ├── t/[slug]/page.tsx # tutorial page — the 4 normalized sections
│ ├── api/agent/route.ts # the "see it run" endpoint (cookie state → sandbox agent)
│ └── globals.css # the light, docs-style design system (tokens)
├── components/
│ ├── AppShell.tsx # persistent left sidebar + mobile drawer + active state
│ ├── LiveRunPanel.tsx # the interactive "see it run" panel (real fetches → /api/agent)
│ └── RecapPanel.tsx # video + outputs + receipt (recap tier)
├── content/tutorials.ts # ← all tutorial content + sidebar grouping live here
└── lib/
├── types.ts # the normalized content model
└── demo-agent.mjs # sandbox agent logic (x402 handshake) + `node` self-test
```
Loading
Loading