diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..5902cbf66
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+.git
+.github
+.DS_Store
+
+frontend/node_modules
+frontend/.next
+
+web/bin
+web/obj
diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml
index 4bfd95642..95c741063 100644
--- a/.github/workflows/lighthouse.yml
+++ b/.github/workflows/lighthouse.yml
@@ -17,7 +17,7 @@ jobs:
- name: setup dotnet ef
run: dotnet tool install --tool-path .\.dotnet-tools dotnet-ef --version 9.0.11
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@v7
with:
node-version: '22.x'
- name: install node deps
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 9aba64a74..1b3b29f98 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -17,7 +17,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@v7
with:
node-version: '22.x'
- name: install node deps
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index 6b8136283..5f845f382 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -16,14 +16,14 @@ jobs:
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Cache SonarCloud packages
- uses: actions/cache@v5
+ uses: actions/cache@v6
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
- uses: actions/cache@v5
+ uses: actions/cache@v6
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index a1c4f66b0..97652b017 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -14,16 +14,63 @@ jobs:
- name: checkout
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+ - name: setup pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
+ with:
+ version: 10.34.5
+
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '22.x'
+ cache: 'pnpm'
- name: install node deps
- run: npm install --ignore-scripts --no-audit --no-fund
+ run: pnpm install --ignore-scripts --frozen-lockfile
- name: lint
- run: npm run lint
+ run: pnpm run lint
+ frontend_tests:
+ name: 'frontend tests'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: checkout
+ uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
+
+ - name: setup pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
+ with:
+ version: 10.34.5
+
+ - name: setup node
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
+ with:
+ node-version: '22.x'
+ cache: 'pnpm'
+
+ - name: install node deps
+ run: pnpm install --ignore-scripts --frozen-lockfile
+
+ - name: test with coverage
+ run: pnpm run frontend:test:coverage
+
+ - name: upload cov
+ if: always()
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
+ with:
+ files: ./frontend/coverage/cobertura-coverage.xml
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: false
+ verbose: true
+
+ - name: upload test results
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ./frontend/coverage/test-report.junit.xml
+ report-type: test_results
install_test:
name: 'install test'
runs-on: windows-latest
@@ -38,14 +85,19 @@ jobs:
uses: actions/setup-dotnet@131b410979e0b49e2162c0718030257b22d6dc2c
with:
dotnet-version: '9.0.x'
+ - name: setup pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
+ with:
+ version: 10.34.5
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '22.x'
+ cache: 'pnpm'
- name: install node deps
- run: npm install --ignore-scripts --no-audit --no-fund
+ run: pnpm install --ignore-scripts --frozen-lockfile
- name: node build
- run: npm run build
+ run: pnpm run build
- name: create settings file
run: |
cd web
@@ -79,31 +131,36 @@ jobs:
dotnet-version: '9.0.x'
- name: setup dotnet ef
run: dotnet tool install --tool-path .\.dotnet-tools dotnet-ef --version 9.0.11
+ - name: setup pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
+ with:
+ version: 10.34.5
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '22.x'
+ cache: 'pnpm'
- name: setup java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
with:
distribution: 'microsoft'
java-version: '17'
- name: install node deps
- run: npm install --ignore-scripts --no-audit --no-fund
+ run: pnpm install --ignore-scripts --frozen-lockfile
- name: node build
- run: npm run build
+ run: pnpm run build
- name: install dotnet deps
run: |
dotnet tool install -g dotnet-reportgenerator-globaltool
dotnet restore
- name: build
- run: npm run dotnet:build
+ run: pnpm run dotnet:build
- name: start solr
run: ./web/solr/bin/solr start
- name: test
run: |
- npm run test:integrationTests
+ dotnet test web.Tests/web.Tests.csproj --filter IntegrationTests --no-build -e Demo=True --collect:"XPlat Code Coverage;Format=cobertura" --results-directory TestResults --logger:"junit;LogFilePath=TestResults/integration-tests.junit.xml"
env:
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
@@ -135,6 +192,25 @@ jobs:
Write-Host "Coverage file not found, skipping report generation"
}
+ - name: enforce coverage threshold
+ if: always()
+ shell: pwsh
+ env:
+ COVERAGE_THRESHOLD: '1'
+ run: |
+ if (!(Test-Path coverage.cobertura.xml)) {
+ Write-Host "Coverage file not found, skipping threshold check."
+ exit 0
+ }
+
+ [xml]$coverage = Get-Content coverage.cobertura.xml
+ $lineRate = [double]$coverage.coverage.'line-rate' * 100
+ Write-Host ("Line coverage: {0:N2}%" -f $lineRate)
+
+ if ($lineRate -lt [double]$env:COVERAGE_THRESHOLD) {
+ throw "Line coverage $lineRate is below threshold $env:COVERAGE_THRESHOLD."
+ }
+
- name: upload cov
if: always()
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
@@ -144,6 +220,15 @@ jobs:
fail_ci_if_error: false
verbose: true
+ - name: upload test results
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ./TestResults/integration-tests.junit.xml
+ report-type: test_results
+ flags: integration
+
# browser_tests:
# name: 'browser tests'
# runs-on: windows-latest
@@ -217,31 +302,36 @@ jobs:
uses: actions/setup-dotnet@131b410979e0b49e2162c0718030257b22d6dc2c
with:
dotnet-version: '9.0.x'
+ - name: setup pnpm
+ uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093
+ with:
+ version: 10.34.5
- name: setup node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '22.x'
+ cache: 'pnpm'
- name: setup java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
with:
distribution: 'microsoft'
java-version: '17'
- name: install node deps
- run: npm install --ignore-scripts --no-audit --no-fund
+ run: pnpm install --ignore-scripts --frozen-lockfile
- name: node build
- run: npm run build
+ run: pnpm run build
- name: install dotnet deps
run: |
dotnet tool install -g dotnet-reportgenerator-globaltool
dotnet restore
- name: build
- run: npm run dotnet:build
+ run: pnpm run dotnet:build
- name: start solr
run: ./web/solr/bin/solr start
- name: test
run: |
- npm run test:functionTests
+ dotnet test web.Tests/web.Tests.csproj --filter FunctionTests --no-build -e Demo=True --collect:"XPlat Code Coverage;Format=cobertura" --results-directory TestResults --logger:"junit;LogFilePath=TestResults/function-tests.junit.xml"
env:
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
@@ -273,6 +363,25 @@ jobs:
Write-Host "Coverage file not found, skipping report generation"
}
+ - name: enforce coverage threshold
+ if: always()
+ shell: pwsh
+ env:
+ COVERAGE_THRESHOLD: '1'
+ run: |
+ if (!(Test-Path coverage.cobertura.xml)) {
+ Write-Host "Coverage file not found, skipping threshold check."
+ exit 0
+ }
+
+ [xml]$coverage = Get-Content coverage.cobertura.xml
+ $lineRate = [double]$coverage.coverage.'line-rate' * 100
+ Write-Host ("Line coverage: {0:N2}%" -f $lineRate)
+
+ if ($lineRate -lt [double]$env:COVERAGE_THRESHOLD) {
+ throw "Line coverage $lineRate is below threshold $env:COVERAGE_THRESHOLD."
+ }
+
- name: upload cov
if: always()
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
@@ -281,3 +390,12 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
+
+ - name: upload test results
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ./TestResults/function-tests.junit.xml
+ report-type: test_results
+ flags: function
diff --git a/docker-compose.yml b/docker-compose.yml
index 31130c17b..e503e0884 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
sqlserver:
- image: mcr.microsoft.com/mssql/server:2025-latest
+ image: mcr.microsoft.com/mssql/server:2022-latest
container_name: atlas_sqlserver
environment:
ACCEPT_EULA: 'Y'
@@ -80,8 +80,8 @@ services:
frontend:
build:
- context: ./frontend
- dockerfile: Dockerfile
+ context: .
+ dockerfile: frontend/Dockerfile
args:
API_URL: http://web:${WEB_PORT:-5000}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5000}
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index c4736ae6a..a43c8b3f1 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -1,29 +1,33 @@
FROM node:22-alpine AS base
+RUN apk add --no-cache git
RUN npm install -g pnpm@10.32.1
FROM base AS deps
WORKDIR /app
-COPY package.json pnpm-lock.yaml ./
-COPY .husky/install.mjs ./.husky/install.mjs
-RUN pnpm install --frozen-lockfile
+COPY package.json ./package.json
+COPY pnpm-lock.yaml ./pnpm-lock.yaml
+COPY frontend ./frontend
+RUN pnpm install --ignore-scripts --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
-COPY . .
+COPY package.json ./package.json
+COPY frontend ./frontend
ARG API_URL
ARG NEXT_PUBLIC_API_URL
ENV API_URL=$API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
-RUN pnpm build
+RUN pnpm frontend:build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
-COPY --from=builder /app/public ./public
-COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
-COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+COPY --from=builder --chown=nextjs:nodejs /app/frontend/public ./frontend/public
+COPY --from=builder --chown=nextjs:nodejs /app/frontend/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/frontend/.next/static ./frontend/.next/static
+WORKDIR /app/frontend
USER nextjs
EXPOSE 3000
ENV PORT=3000
diff --git a/frontend/app/auth/redirect-origin.test.ts b/frontend/app/auth/redirect-origin.test.ts
index 7fa0a7c3d..d251b04dd 100644
--- a/frontend/app/auth/redirect-origin.test.ts
+++ b/frontend/app/auth/redirect-origin.test.ts
@@ -1,4 +1,4 @@
-import type { NextRequest } from "next/server"
+import { NextRequest } from "next/server"
import { afterEach, describe, expect, test } from "vitest"
import { GET as callbackGet } from "./callback/route"
import { GET as logoutGet } from "./logout/route"
@@ -25,7 +25,7 @@ describe("auth public redirect origin", () => {
test("logout redirects to configured public origin", () => {
process.env.AUTH_RETURN_URL_ORIGIN = "https://library.atlas.bi"
- const response = logoutGet(new Request("https://0.0.0.0:3000/auth/logout") as NextRequest)
+ const response = logoutGet(new NextRequest("https://0.0.0.0:3000/auth/logout"))
expect(response.headers.get("location")).toBe("https://library.atlas.bi/")
})
diff --git a/frontend/app/collections/edit/page.tsx b/frontend/app/collections/edit/page.tsx
index 4389cf46b..280f25a53 100644
--- a/frontend/app/collections/edit/page.tsx
+++ b/frontend/app/collections/edit/page.tsx
@@ -17,7 +17,7 @@ function getSingleValue(value: string | string[] | undefined): string | undefine
export default async function EditCollectionPage({
searchParams,
}: {
- searchParams: EditSearchParams
+ searchParams: Promise
Missing or invalid group id.
- -Missing or invalid group id.
+ +- {denied ? "You do not have access to this page." : message} -
- -+ {denied ? "You do not have access to this page." : message} +
+ +You do not have access to this page.
-You do not have access to this page.
+- Site analytics for this group are available in the Analytics application. -
- -+ Site analytics for this group are available in the Analytics application. +
+ +- {denied ? "You do not have access to this page." : message} -
-+ {denied ? "You do not have access to this page." : message} +
+You do not have access to this page.
-You do not have access to this page.
++ Missing or invalid initiative id. +
+ ++ Invalid initiative id format. +
+ +{message}
+ ++ You do not have permission to edit this initiative. +
+ ++ You do not have permission to create initiatives. +
+ ++ {denied ? "You do not have access to this initiative." : message} +
+ ++ {denied ? "You do not have access to this page." : message} +
+ +Missing or invalid term id.
+ +{message}
+ ++ You do not have permission to edit this term. +
+ ++ You do not have permission to create terms. +
+ +Missing or invalid term id.
+ +{message}
+ +{message}
+ {statusHint ?{statusHint}
: null} + {backendHint ?{backendHint}
: null} + +No terms found.
+ ) : ( +
{children}
),
pre: ({ children }: { children?: ReactNode }) => (
- {children}
+ {children}
),
}
@@ -39,7 +39,7 @@ export function MarkdownContent({ content, className }: { content: string; class
return (
| Operational Owner | ++ {canViewProfiles ? ( + + {operationOwner.fullName || operationOwner.username} + + ) : ( + <>{operationOwner.fullName || operationOwner.username}> + )} + | +
| Executive Owner | ++ {canViewProfiles ? ( + + {executiveOwner.fullName || executiveOwner.username} + + ) : ( + <>{executiveOwner.fullName || executiveOwner.username}> + )} + | +
| Financial Impact | +{financialImpact.name} | +
| Strategic Importance | +{strategicImportance.name} | +
| Last Updated By | ++ {canViewProfiles ? ( + + {lastUpdatedBy.fullName || lastUpdatedBy.username} + + ) : ( + <>{lastUpdatedBy.fullName || lastUpdatedBy.username}> + )} + | +
| Last Updated | +{lastModifiedDisplay} | +
+ + {c.description ? ( + <> + {c.description}{" "} + + read more + + > + ) : ( + + Open to view details. + + )} +
+ +
+ + {item.description ? ( + <> + {item.description.substring(0, 160)}...{" "} + read more + > + ) : ( + Open to view details + )} +
+