diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 587ef2c4..31a0f3ed 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,8 +26,8 @@ jobs: outputs: turnierplan_version: ${{ steps.extract-version.outputs.turnierplan_version }} - container-image: - name: 'Build container image' + container-image-amd64: + name: 'Build container image (amd64)' runs-on: ubuntu-24.04 permissions: contents: read @@ -51,8 +51,8 @@ jobs: - name: 'Build and push container image' uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: - context: ./src - file: ./src/Turnierplan.App/Dockerfile + context: . + file: ./docker/turnierplan-amd64/Dockerfile push: true # Note: Always push "latest" even when building from a non-main branch. This is because currently, no patches are released for a non-latest minor release. tags: 'ghcr.io/turnierplan-net/turnierplan:latest,ghcr.io/turnierplan-net/turnierplan:${{ needs.extract-version.outputs.turnierplan_version }}' diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 4411e655..e0b7ba61 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -72,8 +72,9 @@ jobs: if: ${{ env.RUN_SONARQUBE_ANALYSIS == 'true' }} run: 'dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"' working-directory: './src' - e2e: - name: 'E2E Tests' + + e2e-amd64: + name: 'E2E Tests (amd64)' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v2 @@ -87,7 +88,7 @@ jobs: run: 'npm ci' working-directory: './src/Turnierplan.App/Client/' - name: 'Prepare e2e test environment' - run: 'npm run e2e:prepare' + run: 'npm run e2e:prepare:amd64' working-directory: './src/Turnierplan.App/Client/' - name: 'Run e2e tests' run: 'npm run e2e:run' diff --git a/README.md b/README.md index 8fa5a9ee..4ce20359 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ If you have an Entra ID app registration with the necessary permissions on the s ## Documentation -The developer documentation of **turnierplan.NET** is located in the `docs` directory of this repository. +The developer documentation of **turnierplan.NET** is located in the `docs` directory of this repository. Information about the container images and how to build them can be found in the [README.md in the docker folder](./docker/README.md). ## Development diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..078489c2 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,16 @@ +# turnierplan.NET · Docker Images + +Currently, there exists one Dockerfile with the `dotnet/aspnet:10.0-alpine` base image for the amd64 architecture. To build the container image, run the following command from the *repository root*: + +```shell +docker build -t turnierplan:dev -f docker/turnierplan-amd64/Dockerfile . +``` + +The resulting container image can be run as described in the [main readme](../README.md). The following minimal example uses an in-memory data store and does not mount any volumes: + +```shell +docker run -p 80:8080 -e Turnierplan__ApplicationUrl="http://localhost" -e Database__InMemory="true" turnierplan:dev +``` + +> [!WARNING] +> Manually built container images should not be used in production! This is because the relevant GitHub workflow performs other important steps before actually building the images. diff --git a/src/Turnierplan.App/Dockerfile b/docker/turnierplan-amd64/Dockerfile similarity index 54% rename from src/Turnierplan.App/Dockerfile rename to docker/turnierplan-amd64/Dockerfile index 6115e3fe..0177b991 100644 --- a/src/Turnierplan.App/Dockerfile +++ b/docker/turnierplan-amd64/Dockerfile @@ -1,13 +1,12 @@ -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0.1-alpine3.23-amd64 AS base -# Where are the files stored in the container? This folder should be mapped as a volume +# Where are the files stored inside the container? This folder should be mapped as a volume ARG DATA_DIRECTORY=/var/turnierplan -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0.101-noble-amd64 AS build # Copy the source files and build the solution. -WORKDIR /src -COPY . . +COPY src /src/ WORKDIR "/src/Turnierplan.App" RUN dotnet build "./Turnierplan.App.csproj" -c Release -o /app/build @@ -21,16 +20,11 @@ RUN dotnet publish "./Turnierplan.App.csproj" -c Release --runtime linux-x64 -o FROM base AS final -# Since .NET 8, the official container image no longer includes the 'Kerberos' package. Since Npgsql 10.0, an -# error is shown upon startup if that package is not installed. Because of this, we install the package manually. -# The official documentation states you have to install the 'libkrb5-3' package: -# - https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/krb5-libs-package -# However, this seems to have no effect. By digging in the 'dotnet-docker' repository, you can find the Dockerfile -# used for the .NET 6 images which states the 'libgssapi-krb5-2' package which is installed below. -# - source: https://github.com/dotnet/dotnet-docker/blob/0a4258cc250885be5162d01f178bff5c856291f4/src/runtime-deps/6.0/jammy/amd64/Dockerfile -RUN apt-get update \ - && apt-get -y install libgssapi-krb5-2 \ - && rm -rf /var/lib/apt/lists/* +# Install some packages: +# - 'gcompat' to run QuestPDF dependencies on Alpine Linux +# - 'krb5-libs' to enable Npgsql GSS session encryption +# - 'tzdata' contains relevant time zone information +RUN apk add gcompat krb5-libs tzdata # Prepare the directory used by the turnierplan.NET application RUN mkdir "$DATA_DIRECTORY" && chown "$APP_UID" "$DATA_DIRECTORY" @@ -39,7 +33,6 @@ RUN mkdir "$DATA_DIRECTORY" && chown "$APP_UID" "$DATA_DIRECTORY" USER $APP_UID WORKDIR /app EXPOSE 8080 -EXPOSE 8081 # Copy the publish output i.e. the application binaries and the static web assets including the client application COPY --from=publish /app/publish . diff --git a/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml b/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml index b306079b..8cbdf49e 100644 --- a/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml +++ b/src/Turnierplan.App/Client/cypress/docker/docker-compose.yaml @@ -1,10 +1,10 @@ # This docker compose file is used for running the E2E tests during the CI workflow services: - turnierplan.e2e.application: + turnierplan.e2e.application.amd64: build: - dockerfile: 'Turnierplan.App/Dockerfile' - context: '../../../../' + dockerfile: 'docker/turnierplan-amd64/Dockerfile' + context: '../../../../../' ports: - "45001:8080" environment: diff --git a/src/Turnierplan.App/Client/package.json b/src/Turnierplan.App/Client/package.json index f22d77ee..c5bb28f7 100644 --- a/src/Turnierplan.App/Client/package.json +++ b/src/Turnierplan.App/Client/package.json @@ -21,7 +21,7 @@ "test:ci": "ng test --watch=false --source-map=true --code-coverage --browsers ChromeHeadless", "e2e:swap-environment": "replace-in-file \"environment.prod.ts\" \"environment.e2e.ts\" angular.json", "e2e:swap-environment-back": "replace-in-file \"environment.e2e.ts\" \"environment.prod.ts\" angular.json", - "e2e:prepare": "npm run e2e:swap-environment && docker compose -f cypress/docker/docker-compose.yaml up --build -d && npm run e2e:swap-environment-back", + "e2e:prepare:amd64": "npm run e2e:swap-environment && docker compose -f cypress/docker/docker-compose.yaml up --build -d turnierplan.e2e.application.amd64 && npm run e2e:swap-environment-back", "e2e:destroy": "docker compose -f cypress/docker/docker-compose.yaml down", "e2e:open": "npx cypress open", "e2e:run": "npx cypress run" diff --git a/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts b/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts index c8b44897..ffed0b04 100644 --- a/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/components/document-manager/document-manager.component.ts @@ -188,6 +188,7 @@ export class DocumentManagerComponent { const document = this.documents.find((x) => x.id === id); if (document) { document.name = name; + this.sortDocuments(); } this.currentlyUpdatingName = undefined; }, @@ -197,6 +198,16 @@ export class DocumentManagerComponent { }); } + private sortDocuments(): void { + this.documents = [...this.documents].sort((a, b) => { + const nameComparison = a.name.localeCompare(b.name); + if (nameComparison !== 0) { + return nameComparison; + } + return new Date(a.lastModifiedAt).getTime() - new Date(b.lastModifiedAt).getTime(); + }); + } + protected deleteDocument(id: string): void { if (id === this.currentlyViewedDocumentId) { this.displayPdfViewer = false; diff --git a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts index b74b4f72..135f4d39 100644 --- a/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts +++ b/src/Turnierplan.App/Client/src/app/portal/pages/view-tournament/view-tournament.component.ts @@ -255,7 +255,7 @@ export class ViewTournamentComponent implements OnInit, OnDestroy { this.turnierplanApi.invoke(getDocuments, { tournamentId: this.tournament.id }).subscribe({ next: (documents) => { this.documents = documents ?? []; - this.documents.sort((a, b) => a.id.localeCompare(b.id)); + this.sortDocuments(); this.isLoadingDocuments = false; }, error: (error) => { @@ -888,11 +888,24 @@ export class ViewTournamentComponent implements OnInit, OnDestroy { return this.turnierplanApi.invoke(getDocuments, { tournamentId: this.tournament.id }).pipe( tap((result) => { this.documents = result; - this.documents.sort((a, b) => a.id.localeCompare(b.id)); + this.sortDocuments(); }) ); } + private sortDocuments(): void { + if (!this.documents) { + return; + } + this.documents = [...this.documents].sort((a, b) => { + const nameComparison = a.name.localeCompare(b.name); + if (nameComparison !== 0) { + return nameComparison; + } + return new Date(a.lastModifiedAt).getTime() - new Date(b.lastModifiedAt).getTime(); + }); + } + private setTournament(tournament: TournamentDto | undefined): void { this.tournament = tournament; this.titleService.setTitleFrom(tournament); diff --git a/src/Turnierplan.App/Turnierplan.App.csproj b/src/Turnierplan.App/Turnierplan.App.csproj index b1897606..751f1ef2 100644 --- a/src/Turnierplan.App/Turnierplan.App.csproj +++ b/src/Turnierplan.App/Turnierplan.App.csproj @@ -33,12 +33,6 @@ - - - .dockerignore - - -