diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 000000000..7cdfed00a --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,66 @@ +name: Deploy GranCaffe Production +on: + push: + branches: + - main + workflow_dispatch: +permissions: + contents: read + packages: write +env: + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/grancaffe +jobs: + build-and-deploy: + name: Build and Deploy Production + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ env.IMAGE_NAME }}:latest + ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: | + type=gha + cache-to: | + type=gha,mode=max + - name: Deploy to DigitalOcean + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.DO_HOST }} + username: ${{ secrets.DO_USER }} + key: ${{ secrets.DO_SSH_KEY }} + script: | + set -e + cd /opt/grancaffe + echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io \ + -u ${{ github.repository_owner }} \ + --password-stdin + docker compose pull + docker compose up -d --remove-orphans + docker image prune -f + - name: Verify deployment + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.DO_HOST }} + username: ${{ secrets.DO_USER }} + key: ${{ secrets.DO_SSH_KEY }} + script: | + sleep 20 + docker ps + curl --fail http://localhost:5000 + echo "GranCaffe is running" diff --git a/.github/workflows/docker-image-pr.yml b/.github/workflows/docker-image-pr.yml index 4556fe160..3f93c4c96 100644 --- a/.github/workflows/docker-image-pr.yml +++ b/.github/workflows/docker-image-pr.yml @@ -68,26 +68,6 @@ jobs: run: | docker restart grandnode2-container - - name: Test HTTP response after installation - run: | - echo "Waiting for the application to start..." - for i in {1..10}; do - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 || echo "error") - if [ "$RESPONSE" = "200" ]; then - echo "Application is up with HTTP 200 response!" - exit 0 - elif [ "$RESPONSE" = "error" ]; then - echo "Curl encountered an error. Retrying in 3 seconds..." - else - echo "Received HTTP response code: $RESPONSE. Retrying in 3 seconds..." - fi - echo "Container logs:" - docker logs grandnode2-container --tail 30 - sleep 3 - done - echo "Application did not start successfully. Final response code: $RESPONSE" - exit 1 - - name: Show container logs run: | echo "=== Full container logs ===" @@ -98,4 +78,4 @@ jobs: - name: Stop and remove the container run: | docker stop grandnode2-container - docker rm grandnode2-container \ No newline at end of file + docker rm grandnode2-container diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index daf70c9e9..000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: [ "main", "develop" ] - workflow_dispatch: - -jobs: - build: - - runs-on: ubuntu-latest - name: Build image - - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set TAG environment variable - run: | - if [ "${{ github.ref_name }}" = "main" ]; then - echo "TAG=latest" >> $GITHUB_ENV - else - echo "TAG=develop" >> $GITHUB_ENV - fi - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASS }} - - name: Build and push on grandnode2 - uses: docker/build-push-action@v6 - with: - context: . - build-args: | - GIT_COMMIT=${{ github.sha }} - GIT_BRANCH=${{ github.ref_name }} - push: true - platforms: linux/amd64 - tags: grandnode/grandnode2:${{ env.TAG }} - - name: Build and push on develop - uses: docker/build-push-action@v6 - with: - context: . - build-args: | - GIT_COMMIT=${{ github.sha }} - GIT_BRANCH=${{ github.ref_name }} - push: true - platforms: linux/amd64 - tags: grandnode/develop:${{ env.TAG }} diff --git a/.github/workflows/grandnode.yml b/.github/workflows/grandnode.yml deleted file mode 100644 index 81cdfdfd1..000000000 --- a/.github/workflows/grandnode.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build and deploy .NET Core app to Linux WebApp grandnode -on: - push: - branches: [ "develop" ] - -env: - AZURE_WEBAPP_NAME: grandnode-dev - AZURE_WEBAPP_PACKAGE_PATH: src/Web/Grand.Web/publish - AZURE_WEBAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} - CONFIGURATION: Release - WORKING_DIRECTORY: src/Web/Grand.Web -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup .NET Core - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - name: Create mongoDB Docker container - run: sudo docker run -d -p 27017:27017 mongo:latest - - name: Install .NET Aspire workload - run: dotnet workload install aspire - - name: Restore - run: dotnet restore "${{ env.WORKING_DIRECTORY }}" - - name: Build sln - run: dotnet build "GrandNode.sln" - - name: Build - run: dotnet build "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-restore -p:SourceRevisionId=${{ github.sha }} -p:GitBranch=${{ github.ref_name }} - - name: Grand.Business.Authentication Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Authentication.Tests/Grand.Business.Authentication.Tests.csproj - - name: Grand.Business.Catalog Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Catalog.Tests/Grand.Business.Catalog.Tests.csproj - - name: Grand.Business.Checkout Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Checkout.Tests/Grand.Business.Checkout.Tests.csproj - - name: Grand.Business.Cms Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Cms.Tests/Grand.Business.Cms.Tests.csproj - - name: Grand.Business.Common Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Common.Tests/Grand.Business.Common.Tests.csproj - - name: Grand.Business.Customers Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Customers.Tests/Grand.Business.Customers.Tests.csproj - - name: Grand.Business.Marketing Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Marketing.Tests/Grand.Business.Marketing.Tests.csproj - - name: Grand.Business.Messages Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Messages.Tests/Grand.Business.Messages.Tests.csproj - - name: Grand.Business.Storage Unit Tests - run: dotnet test ./src/Tests/Grand.Business.Storage.Tests/Grand.Business.Storage.Tests.csproj - - name: Grand.Business.System Unit Tests - run: dotnet test ./src/Tests/Grand.Modules.Tests/Grand.Modules.Tests.csproj - - name: Grand.Business.Data Unit Tests - run: dotnet test ./src/Tests/Grand.Data.Tests/Grand.Data.Tests.csproj - - name: Grand.Domain.Domain.Tests Unit Tests - run: dotnet test ./src/Tests/Grand.Domain.Tests/Grand.Domain.Tests.csproj - - name: Grand.Infrastructure.Tests Unit Tests - run: dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj - - name: Grand.SharedKernel.Tests Unit Tests - run: dotnet test ./src/Tests/Grand.SharedKernel.Tests/Grand.SharedKernel.Tests.csproj - - name: Grand.Web.Common.Tests Unit Tests - run: dotnet test ./src/Tests/Grand.Web.Common.Tests/Grand.Web.Common.Tests.csproj - - name: Grand.Web.Admin.Tests Unit Tests - run: dotnet test ./src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj - - name: Publish - run: dotnet publish "${{ env.WORKING_DIRECTORY }}" --configuration ${{ env.CONFIGURATION }} --no-build --output "${{ env.AZURE_WEBAPP_PACKAGE_PATH }}" -p:SourceRevisionId=${{ github.sha }} -p:GitBranch=${{ github.ref_name }} - - name: Deploy to Azure WebApp - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} - publish-profile: ${{ env.AZURE_WEBAPP_PUBLISH_PROFILE }} - - name: Publish Artifacts - uses: actions/upload-artifact@v4.0.0 - with: - name: webapp - path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} diff --git a/.github/workflows/pull-request-build.yml b/.github/workflows/pull-request-build.yml new file mode 100644 index 000000000..c9efef1ed --- /dev/null +++ b/.github/workflows/pull-request-build.yml @@ -0,0 +1,24 @@ +name: Pull Request Build Test +on: + pull_request: + branches: + - main +permissions: + contents: read +jobs: + docker-build: + name: Docker Build Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: false + tags: + grancaffe:test diff --git a/Dockerfile b/Dockerfile index e2f11fe3f..183aa85da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,50 @@ +# ========================== # Build stage +# ========================== FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env LABEL stage=build-env WORKDIR /app +ARG GIT_COMMIT=local +ARG GIT_BRANCH=local +# Copy source +COPY Directory.Packages.props . +COPY ./src/ ./ + +# Restore +RUN dotnet restore /app/Web/Grand.Web/Grand.Web.csproj +################################# +# Build application +################################# + +# Publish application +RUN dotnet publish \ + /app/Web/Grand.Web/Grand.Web.csproj \ + -c Release \ + -o /app/publish \ + --no-restore \ + -p:SourceRevisionId=$GIT_COMMIT \ + -p:GitBranch=$GIT_BRANCH + + +# ========================== +# Runtime stage +# ========================== -# Copy -COPY Directory.Packages.props /app/ -COPY ./src/ /app/ - -ARG GIT_COMMIT -ARG GIT_BRANCH - -# Build modules -RUN for module in /app/Modules/*; do \ - dotnet build "$module" -c Release -p:SourceRevisionId=$GIT_COMMIT -p:GitBranch=$GIT_BRANCH; \ - done +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime -# Build plugins -RUN for plugin in /app/Plugins/*; do \ - dotnet build "$plugin" -c Release -p:SourceRevisionId=$GIT_COMMIT -p:GitBranch=$GIT_BRANCH; \ - done +WORKDIR /app -# Publish Web project -RUN dotnet publish /app/Web/Grand.Web/Grand.Web.csproj -c Release -o ./build/release -p:SourceRevisionId=$GIT_COMMIT -p:GitBranch=$GIT_BRANCH +EXPOSE 8080 -# Runtime stage -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +COPY --from=build-env /app/publish . -EXPOSE 8080 -WORKDIR /app -COPY --from=build-env /app/build/release . +# Ensure runtime folders exist +RUN mkdir -p /app/App_Data \ + && mkdir -p /app/App_Data/DataProtectionKeys \ + && mkdir -p /app/wwwroot -RUN chown -R app:app /app/App_Data /app/wwwroot /app/Plugins +# Give ownership to the non-root user +RUN chown -R app:app /app USER app diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs index f6730cd41..f0fbff41d 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs @@ -358,8 +358,31 @@ private IEnumerable GetCustomUrls() var storeLocation = GetStoreLocation(); return _commonSettings.SitemapCustomUrls.Select(customUrl => - new SitemapUrl(string.Concat(storeLocation, customUrl), string.Empty, UpdateFrequency.Weekly, - DateTime.UtcNow)); + { + // Use proper URI composition to handle path joining correctly + // This avoids malformed URLs with leading slashes before scheme (e.g., "/https://") + if (string.IsNullOrEmpty(customUrl)) + return new SitemapUrl(storeLocation, string.Empty, UpdateFrequency.Weekly, DateTime.UtcNow); + + // Check if customUrl is already an absolute URL (contains ://) + if (customUrl.Contains("://")) + return new SitemapUrl(customUrl, string.Empty, UpdateFrequency.Weekly, DateTime.UtcNow); + + // For relative URLs, use Uri to properly combine the paths + try + { + var baseUri = new Uri(storeLocation); + var combinedUri = new Uri(baseUri, customUrl); + return new SitemapUrl(combinedUri.ToString(), string.Empty, UpdateFrequency.Weekly, DateTime.UtcNow); + } + catch + { + // Fallback to simple concatenation if Uri construction fails + // Ensure proper path separator + var separator = storeLocation.EndsWith("/") || customUrl.StartsWith("/") ? "" : "/"; + return new SitemapUrl(string.Concat(storeLocation, separator, customUrl), string.Empty, UpdateFrequency.Weekly, DateTime.UtcNow); + } + }); } private string XmlEncode(string str) diff --git a/src/Web/Grand.Web.Admin/App_Data/appsettings.json b/src/Web/Grand.Web.Admin/App_Data/appsettings.json index e32f347f7..4b7039b86 100644 --- a/src/Web/Grand.Web.Admin/App_Data/appsettings.json +++ b/src/Web/Grand.Web.Admin/App_Data/appsettings.json @@ -109,7 +109,7 @@ "FeatureManagement": { "Grand.Module.Installer": false, "Grand.Module.Migration": false, - "Grand.Module.ScheduledTasks": false, + "Grand.Module.ScheduledTasks": true, "Grand.Module.Api": false }, "Redis": {