fix: externalize e2e testings and profile name trim (#318)

* fix: externalize e2e testings

* fix: externalize e2e tests.

* refactor: improve e2e workflow.

* chore: upgrade workflow action version.

* fix: createEnv by adding emptyStringAsUndefined (#317)

* chore: release 1.18.2

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(layout): guard undefined user.name in avatar fallbacks (#320)

* fix(layout): guard undefined user.name in avatar fallbacks

* fix: AUTH_DEFAULT_USER as zod email

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>

* fix: profile name trim

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Kutý <6du1ro.n@gmail.com>
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
Killian Larcher
2026-06-11 15:53:07 +02:00
committed by GitHub
co-authored by killian-larcher Charles GTE github-actions[bot] <github-actions[bot]@users.noreply.github.com> charles-gauthereau github-actions[bot] <github-actions[bot]@users.noreply.github.com> Michael Kutý
parent 87f8a50326
commit 51bf9cf679
6 changed files with 71 additions and 102 deletions
+66 -97
View File
@@ -4,114 +4,83 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
permissions:
contents: read
packages: write
jobs: jobs:
build-and-test: build-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository if: github.event.pull_request.head.repo.full_name == github.repository
outputs:
image: ${{ steps.image.outputs.ref }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Setup pnpm - name: Compute image reference
uses: pnpm/action-setup@v4 id: image
run: echo "ref=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/portabase:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- name: Setup Node.js - name: Set up Docker Buildx
uses: actions/setup-node@v4 uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with: with:
node-version: 22 registry: ghcr.io
cache: 'pnpm' username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies - name: Build and push Docker image
run: pnpm install --frozen-lockfile
- name: Build Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./docker/dockerfile/Dockerfile file: ./docker/dockerfile/Dockerfile
push: false push: true
load: true tags: ${{ steps.image.outputs.ref }}
tags: portabase/portabase:test target: prod
- name: Run app container run-e2e:
run: | if: github.event.pull_request.head.repo.full_name == github.repository
docker run -d --name myapp \ needs: build-image
-p 8887:80 \ uses: portabase/e2e-tests/.github/workflows/run-e2e.yml@main
-e NODE_ENV=production \ with:
-e PROJECT_URL=http://localhost:8887 \ server_image: ${{ needs.build-image.outputs.image }}
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \ secrets:
portabase/portabase:test E2E_NOTIFICATION_SMTP_HOST: ${{ secrets.E2E_NOTIFICATION_SMTP_HOST }}
E2E_NOTIFICATION_SMTP_PORT: ${{ secrets.E2E_NOTIFICATION_SMTP_PORT }}
- name: Wait for app port to be listening E2E_NOTIFICATION_SMTP_USER: ${{ secrets.E2E_NOTIFICATION_SMTP_USER }}
run: | E2E_NOTIFICATION_SMTP_PASSWORD: ${{ secrets.E2E_NOTIFICATION_SMTP_PASSWORD }}
for i in {1..40}; do E2E_NOTIFICATION_SMTP_FROM: ${{ secrets.E2E_NOTIFICATION_SMTP_FROM }}
E2E_NOTIFICATION_SMTP_TO: ${{ secrets.E2E_NOTIFICATION_SMTP_TO }}
if curl -fsS http://localhost:8887/ >/dev/null; then E2E_NOTIFICATION_SLACK_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_SLACK_WEBHOOK }}
echo "App is responding on port 8887" E2E_NOTIFICATION_DISCORD_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_DISCORD_WEBHOOK }}
exit 0 E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN }}
fi E2E_NOTIFICATION_TELEGRAM_CHAT_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_CHAT_ID }}
echo "Waiting for app readiness... ($i/40)" E2E_NOTIFICATION_TELEGRAM_TOPIC_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_TOPIC_ID }}
sleep 2 E2E_NOTIFICATION_GOTIFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_GOTIFY_SERVER_URL }}
done E2E_NOTIFICATION_GOTIFY_APP_TOKEN: ${{ secrets.E2E_NOTIFICATION_GOTIFY_APP_TOKEN }}
echo "Timeout: app never became ready" E2E_NOTIFICATION_NTFY_TOPIC: ${{ secrets.E2E_NOTIFICATION_NTFY_TOPIC }}
docker logs myapp E2E_NOTIFICATION_NTFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_NTFY_SERVER_URL }}
exit 1 E2E_NOTIFICATION_NTFY_TOKEN: ${{ secrets.E2E_NOTIFICATION_NTFY_TOKEN }}
E2E_NOTIFICATION_NTFY_USERNAME: ${{ secrets.E2E_NOTIFICATION_NTFY_USERNAME }}
- name: Install Playwright browsers E2E_NOTIFICATION_NTFY_PASSWORD: ${{ secrets.E2E_NOTIFICATION_NTFY_PASSWORD }}
run: pnpm exec playwright install --with-deps chromium E2E_NOTIFICATION_WEBHOOK_URL: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_URL }}
E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER }}
- name: Run Playwright tests E2E_NOTIFICATION_WEBHOOK_SECRET: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET }}
run: pnpm exec playwright test E2E_STORAGE_AWS_S3_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_AWS_S3_ENDPOINT_URL }}
env: E2E_STORAGE_AWS_S3_REGION: ${{ secrets.E2E_STORAGE_AWS_S3_REGION }}
PROJECT_URL: http://localhost:8887 E2E_STORAGE_AWS_S3_ACCESS_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_ACCESS_KEY }}
# E2E Notification E2E_STORAGE_AWS_S3_SECRET_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_SECRET_KEY }}
E2E_NOTIFICATION_SMTP_HOST: ${{ secrets.E2E_NOTIFICATION_SMTP_HOST }} E2E_STORAGE_AWS_S3_BUCKET_NAME: ${{ secrets.E2E_STORAGE_AWS_S3_BUCKET_NAME }}
E2E_NOTIFICATION_SMTP_PORT: ${{ secrets.E2E_NOTIFICATION_SMTP_PORT }} E2E_STORAGE_AWS_S3_PORT: ${{ secrets.E2E_STORAGE_AWS_S3_PORT }}
E2E_NOTIFICATION_SMTP_USER: ${{ secrets.E2E_NOTIFICATION_SMTP_USER }} E2E_STORAGE_R2_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_R2_ENDPOINT_URL }}
E2E_NOTIFICATION_SMTP_PASSWORD: ${{ secrets.E2E_NOTIFICATION_SMTP_PASSWORD }} E2E_STORAGE_R2_REGION: ${{ secrets.E2E_STORAGE_R2_REGION }}
E2E_NOTIFICATION_SMTP_FROM: ${{ secrets.E2E_NOTIFICATION_SMTP_FROM }} E2E_STORAGE_R2_ACCESS_KEY: ${{ secrets.E2E_STORAGE_R2_ACCESS_KEY }}
E2E_NOTIFICATION_SMTP_TO: ${{ secrets.E2E_NOTIFICATION_SMTP_TO }} E2E_STORAGE_R2_SECRET_KEY: ${{ secrets.E2E_STORAGE_R2_SECRET_KEY }}
E2E_NOTIFICATION_SLACK_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_SLACK_WEBHOOK }} E2E_STORAGE_R2_BUCKET_NAME: ${{ secrets.E2E_STORAGE_R2_BUCKET_NAME }}
E2E_NOTIFICATION_DISCORD_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_DISCORD_WEBHOOK }} E2E_STORAGE_R2_PORT: ${{ secrets.E2E_STORAGE_R2_PORT }}
E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN }} E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID }}
E2E_NOTIFICATION_TELEGRAM_CHAT_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_CHAT_ID }} E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET }}
E2E_NOTIFICATION_TELEGRAM_TOPIC_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_TOPIC_ID }} E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID }}
E2E_NOTIFICATION_GOTIFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_GOTIFY_SERVER_URL }}
E2E_NOTIFICATION_GOTIFY_APP_TOKEN: ${{ secrets.E2E_NOTIFICATION_GOTIFY_APP_TOKEN }}
E2E_NOTIFICATION_NTFY_TOPIC: ${{ secrets.E2E_NOTIFICATION_NTFY_TOPIC }}
E2E_NOTIFICATION_NTFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_NTFY_SERVER_URL }}
E2E_NOTIFICATION_NTFY_TOKEN: ${{ secrets.E2E_NOTIFICATION_NTFY_TOKEN }}
E2E_NOTIFICATION_NTFY_USERNAME: ${{ secrets.E2E_NOTIFICATION_NTFY_USERNAME }}
E2E_NOTIFICATION_NTFY_PASSWORD: ${{ secrets.E2E_NOTIFICATION_NTFY_PASSWORD }}
E2E_NOTIFICATION_WEBHOOK_URL: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_URL }}
E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER }}
E2E_NOTIFICATION_WEBHOOK_SECRET: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET }}
# E2E Storage
E2E_STORAGE_AWS_S3_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_AWS_S3_ENDPOINT_URL }}
E2E_STORAGE_AWS_S3_REGION: ${{ secrets.E2E_STORAGE_AWS_S3_REGION }}
E2E_STORAGE_AWS_S3_ACCESS_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_ACCESS_KEY }}
E2E_STORAGE_AWS_S3_SECRET_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_SECRET_KEY }}
E2E_STORAGE_AWS_S3_BUCKET_NAME: ${{ secrets.E2E_STORAGE_AWS_S3_BUCKET_NAME }}
E2E_STORAGE_AWS_S3_PORT: ${{ secrets.E2E_STORAGE_AWS_S3_PORT }}
E2E_STORAGE_R2_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_R2_ENDPOINT_URL }}
E2E_STORAGE_R2_REGION: ${{ secrets.E2E_STORAGE_R2_REGION }}
E2E_STORAGE_R2_ACCESS_KEY: ${{ secrets.E2E_STORAGE_R2_ACCESS_KEY }}
E2E_STORAGE_R2_SECRET_KEY: ${{ secrets.E2E_STORAGE_R2_SECRET_KEY }}
E2E_STORAGE_R2_BUCKET_NAME: ${{ secrets.E2E_STORAGE_R2_BUCKET_NAME }}
E2E_STORAGE_R2_PORT: ${{ secrets.E2E_STORAGE_R2_PORT }}
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID }}
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET }}
E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID }}
- name: Upload report if failed
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Cleanup
if: always()
run: docker stop myapp && docker rm myapp || true
+1 -1
View File
@@ -31,7 +31,7 @@ export const env = createEnv({
SMTP_USER: z.string().optional(), SMTP_USER: z.string().optional(),
AUTH_DEFAULT_USER_NAME: z.string().optional(), AUTH_DEFAULT_USER_NAME: z.string().optional(),
AUTH_DEFAULT_USER: z.string().optional(), AUTH_DEFAULT_USER: z.email().optional(),
AUTH_DEFAULT_PASSWORD: z.string().optional(), AUTH_DEFAULT_PASSWORD: z.string().optional(),
+1 -1
View File
@@ -33,7 +33,7 @@ export const LoggedInButtonClient = ({ user, sessions, currentSession, accounts,
<SidebarMenuButton type="button" className="h-auto justify-between py-2" data-testid="profile-dropdown"> <SidebarMenuButton type="button" className="h-auto justify-between py-2" data-testid="profile-dropdown">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar className="size-6"> <Avatar className="size-6">
<AvatarFallback>{user.name[0].toUpperCase()}</AvatarFallback> <AvatarFallback>{(user.name?.[0] ?? user.email?.[0] ?? "?").toUpperCase()}</AvatarFallback>
{user.image && <AvatarImage src={user.image} />} {user.image && <AvatarImage src={user.image} />}
</Avatar> </Avatar>
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
+1 -1
View File
@@ -72,7 +72,7 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
<Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20"> <Avatar className="w-24 h-24 lg:w-32 lg:h-32 border-4 border-muted/20">
<AvatarImage className="object-cover" src={user.image || undefined}/> <AvatarImage className="object-cover" src={user.image || undefined}/>
<AvatarFallback className="text-3xl">{user.name.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback className="text-3xl">{(user.name?.charAt(0) ?? user.email?.charAt(0) ?? "?").toUpperCase()}</AvatarFallback>
</Avatar> </Avatar>
<div <div
+1 -1
View File
@@ -2,7 +2,7 @@ import z from "zod";
import {zString} from "@/lib/zod"; import {zString} from "@/lib/zod";
export const ProfileSchema = z.object({ export const ProfileSchema = z.object({
name: zString().nonempty(), name: zString().trim().nonempty(),
role: zString().nonempty(), role: zString().nonempty(),
}); });
+1 -1
View File
@@ -11,7 +11,7 @@ import {userAction} from "@/lib/safe-actions/actions";
import {ApiKey} from "@better-auth/api-key"; import {ApiKey} from "@better-auth/api-key";
const UpdateProfileSchema = z.object({ const UpdateProfileSchema = z.object({
name: z.string().optional(), name: z.string().trim().nonempty().optional(),
}); });
export const updateProfileSettingsAction = userAction.schema(UpdateProfileSchema).action(async ({ parsedInput }): Promise<ServerActionResult<{}>> => { export const updateProfileSettingsAction = userAction.schema(UpdateProfileSchema).action(async ({ parsedInput }): Promise<ServerActionResult<{}>> => {