diff --git a/.github/actions/build-dotnet/action.yml b/.github/actions/build-dotnet/action.yml new file mode 100644 index 000000000..e68dd918f --- /dev/null +++ b/.github/actions/build-dotnet/action.yml @@ -0,0 +1,37 @@ +name: build-dotnet +description: ".NET restore + build" + +inputs: + project-dir: + description: "Project working directory" + required: true + +runs: + using: composite + steps: + - name: Setup .NET SDK (from global.json) + uses: actions/setup-dotnet@v4 + with: + global-json-file: '${{ inputs.project-dir }}/global.json' + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles(format('{0}/**/*.csproj', inputs.project-dir), format('{0}/global.json', inputs.project-dir)) }} + restore-keys: ${{ runner.os }}-nuget- + + - name: Clean Release + run: dotnet clean --configuration Release + shell: bash + working-directory: ${{ inputs.project-dir }} + + - name: Restore + run: dotnet restore + shell: bash + working-directory: ${{ inputs.project-dir }} + + - name: Build Release (warnings as errors) + run: dotnet build --configuration Release --no-restore -warnaserror + shell: bash + working-directory: ${{ inputs.project-dir }} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..976256208 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,31 @@ +name: Manual Deployment + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Tag to deploy' + required: true + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: goodman74/asp.net.otus + +jobs: + cd-main-deploy: + runs-on: ubuntu-latest + steps: + - name: Pull image + run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.image_tag }} + + - name: Run container + run: docker run -d -p 8080:8080 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.image_tag }} + + - name: Health check + run: | + for i in {1..20}; do + curl -f http://localhost:8080/api/v1/roles && exit 0 + sleep 1 + done + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..98f451888 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: Build, Test, Publish Image + +on: + push: + branches: [main] + paths: + - 'Homeworks/06 Настройка CI/**' + +concurrency: + group: ci-main + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + PROJECT_DIR: 'Homeworks/06 Настройка CI/src' + +permissions: + contents: read + packages: write + +jobs: + build-test-push: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ env.PROJECT_DIR }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check working directory + run: pwd + + - name: Build-dotnet + uses: ./.github/actions/build-dotnet + with: + project-dir: ${{ env.PROJECT_DIR }} + + - name: Run tests + run: | + dotnet test --configuration Release --no-build --no-restore --collect:"XPlat Code Coverage" \ + --settings "PromoCodeFactory.UnitTests/coverage.runsettings" --logger "trx" + + - name: Generate HTML Coverage Report + uses: danielpalme/ReportGenerator-GitHub-Action@5 + with: + reports: '${{ env.PROJECT_DIR }}/*/TestResults/*/coverage.cobertura.xml' + targetdir: '${{ env.PROJECT_DIR }}/coveragereport' + reporttypes: 'Html' + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-html + path: '${{ env.PROJECT_DIR }}/coveragereport/' + + - name: Publish test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: '${{ env.PROJECT_DIR }}/*/TestResults/*.trx' + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha + type=ref,event=branch + type=ref,event=tag + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: '${{ env.PROJECT_DIR }}' + file: '${{ env.PROJECT_DIR }}/Dockerfile.ci' + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/clean-ghcr.yml b/.github/workflows/clean-ghcr.yml new file mode 100644 index 000000000..5c5a4df21 --- /dev/null +++ b/.github/workflows/clean-ghcr.yml @@ -0,0 +1,22 @@ +name: Cleanup GHCR + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * 0' + +permissions: + packages: write + contents: read + +jobs: + cleanup: + runs-on: ubuntu-latest + + steps: + - name: Keep last 5 images (prune others) + uses: actions/delete-package-versions@v5 + with: + package-name: asp.net.otus + package-type: container + min-versions-to-keep: 5 diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/.dockerignore" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/.dockerignore" index f1618e19e..dd5d51f2e 100644 --- "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/.dockerignore" +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/.dockerignore" @@ -25,8 +25,6 @@ **/values.dev.yaml LICENSE README.md -!**/.gitignore -!.git/HEAD -!.git/config -!.git/packed-refs -!.git/refs/heads/** +# Исключаем локальные артефакты (если есть) +**/publish/ +**/TestResults/ diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile" index df94e514f..0ddbd59ab 100644 --- "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile" +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile" @@ -1,18 +1,42 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG DOTNET_VERSION=10.0 +# -------- base runtime -------- +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS base +WORKDIR /app +EXPOSE 8080 +# ---------- build ---------- +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src -COPY . . +# скопировать файлы, которые управляют restore (чтобы улучшить кеш restore) +COPY ./global.json ./ +COPY ./*.sln* ./ +COPY ./PromoCodeFactory.Core/PromoCodeFactory.Core.csproj ./PromoCodeFactory.Core/ +COPY ./PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj ./PromoCodeFactory.DataAccess/ +COPY ./PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj ./PromoCodeFactory.WebHost/ +COPY ./PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj ./PromoCodeFactory.UnitTests/ + RUN dotnet restore + +# теперь весь код +COPY ./PromoCodeFactory.Core/ ./PromoCodeFactory.Core/ +COPY ./PromoCodeFactory.DataAccess/ ./PromoCodeFactory.DataAccess/ +COPY ./PromoCodeFactory.WebHost/ ./PromoCodeFactory.WebHost/ +COPY ./PromoCodeFactory.UnitTests/ ./PromoCodeFactory.UnitTests/ + RUN dotnet build -c $BUILD_CONFIGURATION --no-restore -FROM build AS publish +# ---------- test ---------- +FROM build AS test +RUN dotnet test --no-build --no-restore -c Release --collect:"XPlat Code Coverage" --settings PromoCodeFactory.UnitTests/coverage.runsettings \ + --logger "trx" --results-directory /test-results + +# ---------- publish ---------- +FROM test AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish ./PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj \ - -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false --no-build + -c $BUILD_CONFIGURATION -o /app/publish --no-build --no-restore -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final -WORKDIR /app +FROM base AS final COPY --from=publish /app/publish . -EXPOSE 8080 ENTRYPOINT ["dotnet", "PromoCodeFactory.WebHost.dll"] diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile.ci" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile.ci" new file mode 100644 index 000000000..5dbbb2830 --- /dev/null +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/Dockerfile.ci" @@ -0,0 +1,36 @@ +ARG DOTNET_VERSION=10.0 +# -------- base runtime -------- +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS base +WORKDIR /app +EXPOSE 8080 + +# ---------- build ---------- +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +# скопировать файлы, которые управляют restore (чтобы улучшить кеш restore) +COPY ./global.json ./ +COPY ./*.sln* ./ +COPY ./PromoCodeFactory.Core/PromoCodeFactory.Core.csproj ./PromoCodeFactory.Core/ +COPY ./PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj ./PromoCodeFactory.DataAccess/ +COPY ./PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj ./PromoCodeFactory.WebHost/ +COPY ./PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj ./PromoCodeFactory.UnitTests/ + +RUN dotnet restore + +# теперь весь код +COPY ./PromoCodeFactory.Core/ ./PromoCodeFactory.Core/ +COPY ./PromoCodeFactory.DataAccess/ ./PromoCodeFactory.DataAccess/ +COPY ./PromoCodeFactory.WebHost/ ./PromoCodeFactory.WebHost/ +COPY ./PromoCodeFactory.UnitTests/ ./PromoCodeFactory.UnitTests/ + +RUN dotnet build -c $BUILD_CONFIGURATION --no-restore + +# ---------- publish ---------- +FROM build AS publish +RUN dotnet publish ./PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj \ + -c $BUILD_CONFIGURATION -o /app/publish --no-build --no-restore + +FROM base AS final +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PromoCodeFactory.WebHost.dll"] diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj" index 760e7496f..d765ebcdb 100644 --- "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj" +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/PromoCodeFactory.UnitTests.csproj" @@ -1,4 +1,4 @@ - + net10.0 @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/coverage.runsettings" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/coverage.runsettings" new file mode 100644 index 000000000..d314e6e88 --- /dev/null +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/PromoCodeFactory.UnitTests/coverage.runsettings" @@ -0,0 +1,23 @@ + + + + + + + + + cobertura + + + **/obj/**,**/bin/**,**/*.g.cs,**/*.generated.cs + + + + [*]Program + + + + + + + \ No newline at end of file diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/docker-compose.Development.yaml" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.Development.yml" similarity index 85% rename from "Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/docker-compose.Development.yaml" rename to "Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.Development.yml" index 2429b29db..44c6f1705 100644 --- "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/docker-compose.Development.yaml" +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.Development.yml" @@ -1,6 +1,8 @@ +name: HW06_promocode services: promocode-factory-api: - build: src/ + build: + context: . container_name: 'promocode-factory-api' restart: always ports: @@ -10,6 +12,6 @@ services: environment: - "ConnectionStrings:PromocodeFactoryDb=Filename=/app/db/PromoCodeFactoryDb.sqlite" - "ASPNETCORE_ENVIRONMENT=Development" - + volumes: - db: \ No newline at end of file + db: diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.test.yml" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.test.yml" new file mode 100644 index 000000000..c54bb5572 --- /dev/null +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/docker-compose.test.yml" @@ -0,0 +1,30 @@ +name: HW06_promocode_GHCR +services: + promocode-factory-api_ghcr: + image: ghcr.io/goodman74/asp.net.otus:latest + #container_name: 'promocode-factory-api-ghcr' + restart: always + ports: + - "8099:8080" + volumes: + - db:/app/db + environment: + - "ConnectionStrings:PromocodeFactoryDb=Filename=/app/db/PromoCodeFactoryDb.sqlite" + - "ASPNETCORE_ENVIRONMENT=Development" + + smoke: + image: curlimages/curl:latest + depends_on: + - promocode-factory-api_ghcr + command: > + sh -c " + echo 'Waiting before smoke test 5 sec ...'; + sleep 5; + for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; + do curl -f http://promocode-factory-api_ghcr:8080/api/v1/roles && exit 0; + sleep 1; + done; + exit 1" + +volumes: + db: diff --git "a/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/global.json" "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/global.json" new file mode 100644 index 000000000..8287d3875 --- /dev/null +++ "b/Homeworks/06 \320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\260 CI/src/global.json" @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestFeature" + } +}