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
199648e
Update project configuration and add new features
Abhishekfm Aug 21, 2026
2f5969f
Merge branch 'dev' into update-next
Abhishekfm Aug 21, 2026
613f128
sync package-lock
Abhishekfm Aug 21, 2026
c408512
Merge pull request #438 from CivicDataLab/update-next
Abhishekfm Aug 27, 2026
16a8cca
ci: releases/symlink deploy with health-check rollback and smoke-test…
saqibmanan Aug 27, 2026
3f00e51
ci: fix runtime secrets file to .env.local, not .env
saqibmanan Aug 27, 2026
8e04801
Merge remote-tracking branch 'origin/dev' into ci/release-rollback-qu…
saqibmanan Aug 27, 2026
5be47e4
Merge pull request #441 from CivicDataLab/ci/release-rollback-quality…
saqibmanan Aug 27, 2026
c0ee2c1
ci: fix nvm node/npm/pm2 resolution over non-interactive SSH
saqibmanan Aug 27, 2026
ce3e8e8
Refactor dashboard components and enhance translation support
Abhishekfm Aug 27, 2026
7d6b928
ci: include env.ts in the release artifact
saqibmanan Aug 27, 2026
b083aa6
ci: fix false-negative failure in release-pruning cleanup
saqibmanan Aug 27, 2026
cb5f1c8
ci: add environment: development to promote-dev/rollback-dev
saqibmanan Aug 27, 2026
f68dc39
docs: add deploy pipeline handover for the releases/rollback rework
saqibmanan Aug 27, 2026
dd28e0c
docs: note the stale-shell-cwd gotcha from release pruning
saqibmanan Aug 27, 2026
d750e88
refactor: update ESLint configuration and improve type safety across …
Abhishekfm Aug 31, 2026
87e0123
Merge pull request #442 from CivicDataLab/bhashini-plugin
Abhishekfm Aug 31, 2026
ab167a1
Merge branch 'dev' into lint-fixes
Abhishekfm Sep 1, 2026
50a58db
fix: update Resources component to ensure proper value conversion
Abhishekfm Sep 1, 2026
d2e0a31
Privacy-policy
UdayRajSahai2 Sep 1, 2026
c0d5834
Merge pull request #444 from CivicDataLab/Privacy-policy
saqibmanan Sep 1, 2026
f96e37c
Added Privacy in Footer
UdayRajSahai2 Sep 2, 2026
371d361
ci: pass KEYCLOAK_CLIENT_SECRET to the smoke workflow
saqibmanan Sep 2, 2026
ce1a157
Merge pull request #451 from CivicDataLab/ci/pass-keycloak-secret-to-…
saqibmanan Sep 2, 2026
dee567e
fix: stop the NextAuth signout loop
saqibmanan Sep 3, 2026
fac319a
Merge pull request #453 from CivicDataLab/fix/nextauth-signout-loop
saqibmanan Sep 3, 2026
f7141e8
Merge pull request #443 from CivicDataLab/lint-fixes
Abhishekfm Sep 8, 2026
e5e300a
Merge pull request #447 from CivicDataLab/Privacy-policy
Abhishekfm Sep 8, 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
190 changes: 171 additions & 19 deletions .github/workflows/deploy-Dataspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ on:
- dev
- main

# Prevent two deploys to the same environment from racing each other and
# corrupting the releases/ directory or the .last_good / .rollback_target markers.
concurrency:
group: deploy-dataspace-${{ github.ref_name }}
cancel-in-progress: false

jobs:
deploy:
build-and-deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'production' || 'development' }}
outputs:
release: ${{ steps.meta.outputs.release }}
env:
KEYCLOAK_CLIENT_ID: ${{ secrets.KEYCLOAK_CLIENT_ID }}
KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}
Expand Down Expand Up @@ -39,14 +47,31 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Compute release name
id: meta
run: echo "release=$(date -u +%Y%m%d%H%M%S)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'

- name: Install dependencies
run: npm install
# npm ci, not npm install: installs exactly what package-lock.json pins.
# npm install can silently resolve a different version than the lockfile
# (this is how a canary/newer `next` ended up in a previous build).
- name: Install dependencies (from lockfile only)
run: npm ci --legacy-peer-deps

- name: Guard - installed Next.js must match package.json
run: |
DECLARED=$(node -p "require('./package.json').dependencies.next")
INSTALLED=$(node -p "require('./node_modules/next/package.json').version")
echo "declared next: $DECLARED / installed next: $INSTALLED"
if [ "$DECLARED" != "$INSTALLED" ]; then
echo "::error::Installed next ($INSTALLED) does not match package.json ($DECLARED). Aborting before build."
exit 1
fi

- name: Generate
run: npm run generate:ci
Expand All @@ -60,41 +85,114 @@ jobs:
- name: Build
run: npm run build

- name: Rename .next to .next2
run: mv .next .next2

- name: Rename public to public2
run: mv public public2
# next.config.mjs runs again on every `next start` boot (not just at
# build time) and does `jiti('./env')` synchronously - env.ts has to
# exist on disk in the release dir or the server crash-loops with
# "Cannot find module './env'" (found via a live failed deploy).
- name: Package release artifact
run: tar czf release.tar.gz .next public package.json package-lock.json next.config.mjs env.ts

- name: Send .next2 to EC2
- name: Send release artifact to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: .next2
target: DataExchange/DataExFrontend
source: release.tar.gz
target: DataExchange/incoming/${{ steps.meta.outputs.release }}

- name: Send public2 to EC2
uses: appleboy/scp-action@v0.1.7
# Extract into a brand-new releases/<release>/ directory, install deps
# THERE (never touching the currently-live release), verify, and only
# then flip the DataExFrontend symlink. If anything up to and including
# the health check fails, the previous release is still on disk untouched
# and this step rolls the symlink back itself before exiting non-zero.
- name: Extract, install, verify, and activate release on EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: public2
target: DataExchange/DataExFrontend
script: |
set -euo pipefail
BASE=/home/ubuntu/DataExchange
RELEASE=${{ steps.meta.outputs.release }}
RELEASE_DIR="$BASE/releases/$RELEASE"
NODE_BIN=/home/ubuntu/.nvm/versions/node/v24.13.0/bin
# npm/pm2 are scripts with a `#!/usr/bin/env node` shebang, so
# invoking them by absolute path is not enough - env still
# re-resolves `node` via PATH, which appleboy/ssh-action's
# non-interactive shell leaves pointed at the ancient system
# node (v10) since it never sources nvm. Put NODE_BIN on PATH
# first so that shebang resolution finds the right node too.
export PATH="$NODE_BIN:$PATH"
PM2="$NODE_BIN/pm2"
NPM="$NODE_BIN/npm"
NODE="$NODE_BIN/node"

mkdir -p "$RELEASE_DIR" "$BASE/releases" "$BASE/shared"
tar xzf "$BASE/incoming/$RELEASE/release.tar.gz" -C "$RELEASE_DIR"
rm -rf "$BASE/incoming/$RELEASE"

# Runtime secrets live only in shared/.env.local (NOT the docker-compose .env one level up), never shipped by CI.
ln -sfn "$BASE/shared/.env.local" "$RELEASE_DIR/.env.local"

- name: Update with new Build
cd "$RELEASE_DIR"
"$NPM" ci --omit=dev

INSTALLED=$("$NODE" -p "require('./node_modules/next/package.json').version")
DECLARED=$("$NODE" -p "require('./package.json').dependencies.next")
if [ "$INSTALLED" != "$DECLARED" ]; then
echo "Next version mismatch on server ($INSTALLED vs $DECLARED). Not activating." >&2
exit 1
fi

if [ -f "$BASE/releases/.last_good" ]; then
PREVIOUS=$(cat "$BASE/releases/.last_good")
else
PREVIOUS=$(basename "$(readlink -f "$BASE/DataExFrontend")")
fi
echo "$PREVIOUS" > "$BASE/releases/.rollback_target"

ln -sfn "$RELEASE_DIR" "$BASE/DataExFrontend"
"$PM2" restart dataspace

ATTEMPTS=0
until curl -f -s -o /dev/null http://127.0.0.1:3000; do
ATTEMPTS=$((ATTEMPTS+1))
if [ "$ATTEMPTS" -ge 10 ]; then
echo "Health check failed after $ATTEMPTS attempts. Rolling back to $PREVIOUS." >&2
ln -sfn "$BASE/releases/$PREVIOUS" "$BASE/DataExFrontend"
"$PM2" restart dataspace
exit 1
fi
sleep 3
done

echo "Release $RELEASE is live and passed the boot health check."

# Keep the 5 most recent releases plus whatever the rollback target is.
# `grep -v` exits 1 when there's nothing to prune (fewer than 6
# releases so far) - under `set -o pipefail` that would otherwise
# kill the script right after the success message above and report
# this whole deploy as failed despite it actually having succeeded
# (found via a live deploy: app was healthy, CI still said failure).
cd "$BASE/releases"
ls -1dt */ 2>/dev/null | tail -n +6 | grep -v "^${PREVIOUS}/$" | xargs -r rm -rf || true

# Production has no smoke-test job today, so its only quality gate is the
# boot health check above. Mark this release good immediately so a FUTURE
# deploy knows what to roll back to if it fails.
- name: Mark release as last-known-good (prod only)
if: github.ref_name == 'main'
uses: appleboy/ssh-action@v1.0.3
continue-on-error: false
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: rm -rf DataExchange/DataExFrontend/.next; rm -rf DataExchange/DataExFrontend/public; mv DataExchange/DataExFrontend/.next2 DataExchange/DataExFrontend/.next; mv DataExchange/DataExFrontend/public2 DataExchange/DataExFrontend/public; /home/ubuntu/.nvm/versions/node/v20.11.1/bin/pm2 restart dataspace
script: echo "${{ steps.meta.outputs.release }}" > /home/ubuntu/DataExchange/releases/.last_good

smoke-tests:
needs: deploy
needs: build-and-deploy
if: github.ref_name == 'dev'
uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI
secrets:
Expand All @@ -103,3 +201,57 @@ jobs:
TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }}
TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }}
TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }}
# api-smoke authenticates against Keycloak via ROPC. `dataspace` is a
# confidential client, so without this the token request returns 401
# and the job fails its preflight.
KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}

# dev only: smoke tests are the real quality gate here. Only once they pass
# does this release become the thing a future rollback would target.
promote-dev:
needs: [build-and-deploy, smoke-tests]
if: github.ref_name == 'dev' && needs.smoke-tests.result == 'success'
runs-on: ubuntu-latest
# vars.EC2_HOST is an environment-scoped variable (Settings -> Environments
# -> development), only visible to jobs that declare `environment:`.
# Without this, appleboy/ssh-action fails with "missing server host"
# (found via a live run - build-and-deploy/smoke-tests both succeeded but
# this job still failed, which is why `if:` here is already gated to dev).
environment: development
steps:
- name: Mark this release as last-known-good
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: echo "${{ needs.build-and-deploy.outputs.release }}" > /home/ubuntu/DataExchange/releases/.last_good

rollback-dev:
needs: [build-and-deploy, smoke-tests]
if: github.ref_name == 'dev' && needs.smoke-tests.result == 'failure'
runs-on: ubuntu-latest
# Same environment-scoped vars.EC2_HOST issue as promote-dev - see the
# comment there.
environment: development
steps:
- name: Revert to last known-good release
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
set -euo pipefail
BASE=/home/ubuntu/DataExchange
NODE_BIN=/home/ubuntu/.nvm/versions/node/v24.13.0/bin
# See the comment in build-and-deploy's activation step: pm2's
# shebang re-resolves node via PATH, so NODE_BIN must be on it.
export PATH="$NODE_BIN:$PATH"
PM2="$NODE_BIN/pm2"
TARGET=$(cat "$BASE/releases/.rollback_target")
echo "Smoke tests failed. Reverting DataExFrontend -> releases/$TARGET"
ln -sfn "$BASE/releases/$TARGET" "$BASE/DataExFrontend"
"$PM2" restart dataspace
sleep 3
curl -f http://127.0.0.1:3000 -o /dev/null -s -w "post-rollback HTTP %{http_code}\n"
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ next-env.d.ts
# generated graphql files
/gql/generated/gql.ts
/gql/generated/graphql.ts
/gql/generated/
/gql/generated/
# next-agents-md
.next-docs/
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
4 changes: 2 additions & 2 deletions app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Metadata from './components/Metadata';
import PrimaryData from './components/PrimaryData';
import Versions from './components/Versions';

const aiModelQuery: any = graphql(`
const aiModelQuery = graphql(`
query getAIModel($modelId: Int!) {
getAiModel(modelId: $modelId) {
id
Expand Down Expand Up @@ -102,7 +102,7 @@ export default function AIModelDetailsPage({
}
);

const modelData = (data as any)?.getAiModel;
const modelData = data?.getAiModel;

const jsonLd = generateJsonLd({
'@context': 'https://schema.org',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,25 @@

import { Text } from 'opub-ui';

interface ModelEndpoint {
id?: string | number;
url?: string;
isPrimary?: boolean;
httpMethod?: string;
authType?: string;
timeoutSeconds?: number;
isActive?: boolean;
}

interface DetailsData {
inputSchema?: Record<string, unknown> | null;
outputSchema?: Record<string, unknown> | null;
metadata?: Record<string, unknown> | null;
endpoints?: ModelEndpoint[] | null;
}

interface DetailsProps {
data: any;
data: DetailsData;
}

export default function Details({ data }: DetailsProps) {
Expand Down Expand Up @@ -71,7 +88,7 @@ export default function Details({ data }: DetailsProps) {
API Endpoints
</Text>
<div className="flex flex-col gap-3">
{data.endpoints.map((endpoint: any, index: number) => (
{data.endpoints.map((endpoint, index) => (
<div
key={endpoint.id || index}
className="rounded-lg border border-greyExtralight p-4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,40 @@ import Image from 'next/image';
import Link from 'next/link';
import { Divider, Tag, Text, Tooltip } from 'opub-ui';

interface MetadataProvider {
provider: string;
isPrimary?: boolean;
}

interface MetadataVersion {
isLatest?: boolean;
providers?: MetadataProvider[] | null;
}

interface MetadataData {
organization?: {
id: string;
name: string;
slug?: string | null;
logo?: { url: string } | null;
} | null;
user?: {
id: string;
fullName: string;
profilePicture?: { url: string } | null;
} | null;
modelType: string;
domain?: string | null;
versions?: MetadataVersion[] | null;
metadata?: {
usageLicense?: string;
} | null;
sectors?: Array<{ name: string }> | null;
geographies?: Array<{ name: string }> | null;
}

interface MetadataProps {
data: any;
data: MetadataData;
}

export default function Metadata({ data }: MetadataProps) {
Expand Down Expand Up @@ -67,9 +99,9 @@ export default function Metadata({ data }: MetadataProps) {

// Get primary version info
const primaryVersion =
data.versions?.find((v: any) => v.isLatest) || data.versions?.[0];
data.versions?.find((v) => v.isLatest) || data.versions?.[0];
const primaryProvider =
primaryVersion?.providers?.find((p: any) => p.isPrimary) ||
primaryVersion?.providers?.find((p) => p.isPrimary) ||
primaryVersion?.providers?.[0];

const providerLabels: Record<string, string> = {
Expand Down Expand Up @@ -212,7 +244,7 @@ export default function Metadata({ data }: MetadataProps) {
Sector
</Text>
<div className="flex flex-wrap gap-2">
{data.sectors.map((sector: any, index: number) => (
{data.sectors.map((sector, index) => (
<Tooltip content={sector.name} key={index}>
<Image
src={`/Sectors/${sector.name}.svg`}
Expand All @@ -237,7 +269,7 @@ export default function Metadata({ data }: MetadataProps) {
Geography
</Text>
<div className="flex flex-wrap gap-2">
{data.geographies.map((geo: any, index: number) => (
{data.geographies.map((geo, index) => (
<Tag
key={index}
fillColor="var(--orange-secondary-color)"
Expand Down
Loading
Loading