diff --git a/.github/workflows/_container-smoke.yml b/.github/workflows/_container-smoke.yml new file mode 100644 index 0000000..ee67bcc --- /dev/null +++ b/.github/workflows/_container-smoke.yml @@ -0,0 +1,71 @@ +name: Reusable Container Smoke Test + +on: + workflow_call: + inputs: + image-name: + description: Local Docker image tag used for the smoke test. + required: false + type: string + default: thesexy6bot:ci + dockerfile-path: + description: Dockerfile path relative to the repository root. + required: false + type: string + default: Dockerfile + build-context: + description: Docker build context. + required: false + type: string + default: . + git-sha: + description: Commit SHA passed into the image build. + required: true + type: string + commit-message: + description: Commit message passed into the image build. + required: false + type: string + default: ci-smoke + smoke-test-argument: + description: App argument that runs the smoke-test path. + required: false + type: string + default: --smoke-test + +jobs: + smoke: + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Checkout app files only + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + sparse-checkout: | + Dockerfile + src/dotnet + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Build container image + run: | + short_sha="${GIT_SHA:0:7}" + docker build "$BUILD_CONTEXT" \ + --file "$DOCKERFILE_PATH" \ + --build-arg GIT_SHA="$short_sha" \ + --build-arg GIT_COMMIT_MSG="$COMMIT_MESSAGE" \ + -t "$IMAGE_NAME" + env: + BUILD_CONTEXT: ${{ inputs.build-context }} + COMMIT_MESSAGE: ${{ inputs.commit-message }} + DOCKERFILE_PATH: ${{ inputs.dockerfile-path }} + GIT_SHA: ${{ inputs.git-sha }} + IMAGE_NAME: ${{ inputs.image-name }} + + - name: Run container smoke test + run: docker run --rm "$IMAGE_NAME" "$SMOKE_TEST_ARGUMENT" + env: + IMAGE_NAME: ${{ inputs.image-name }} + SMOKE_TEST_ARGUMENT: ${{ inputs.smoke-test-argument }} diff --git a/.github/workflows/_dotnet-test.yml b/.github/workflows/_dotnet-test.yml new file mode 100644 index 0000000..3e461c5 --- /dev/null +++ b/.github/workflows/_dotnet-test.yml @@ -0,0 +1,52 @@ +name: Reusable .NET Test + +on: + workflow_call: + inputs: + dotnet-version: + description: .NET SDK version to install. + required: false + type: string + default: 9.0.x + solution: + description: Solution file to restore, build, and test. + required: false + type: string + default: TheSexy6BotWorker.slnx + test-filter: + description: Test filter passed to dotnet test. + required: false + type: string + default: Category!=Integration + +jobs: + test: + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: ${{ inputs.dotnet-version }} + + - name: Restore dependencies + run: dotnet restore "$SOLUTION" + env: + SOLUTION: ${{ inputs.solution }} + + - name: Build + run: dotnet build "$SOLUTION" --no-restore + env: + SOLUTION: ${{ inputs.solution }} + + - name: Test + run: dotnet test "$SOLUTION" --no-build --verbosity normal --filter "$TEST_FILTER" + env: + SOLUTION: ${{ inputs.solution }} + TEST_FILTER: ${{ inputs.test-filter }} diff --git a/.github/workflows/_terraform.yml b/.github/workflows/_terraform.yml new file mode 100644 index 0000000..5b148b3 --- /dev/null +++ b/.github/workflows/_terraform.yml @@ -0,0 +1,113 @@ +name: Reusable Terraform + +on: + workflow_call: + inputs: + apply: + description: Whether to apply the Terraform plan. + required: false + type: boolean + default: false + read-outputs: + description: Whether to expose selected Terraform outputs. + required: false + type: boolean + default: false + github-environment: + description: GitHub environment that provides Terraform vars and Azure secrets. + required: false + type: string + default: stg + terraform-version: + description: Terraform version to install. + required: false + type: string + default: 1.9.8 + + outputs: + resource_group_name: + description: Terraform resource_group_name output. + value: ${{ jobs.terraform.outputs.resource_group_name }} + container_app_name: + description: Terraform container_app_name output. + value: ${{ jobs.terraform.outputs.container_app_name }} + registry_url: + description: Terraform container_registry_login_server output. + value: ${{ jobs.terraform.outputs.registry_url }} + +jobs: + terraform: + permissions: + id-token: write + contents: read + environment: ${{ inputs.github-environment }} + runs-on: ubuntu-latest + env: + ARM_USE_OIDC: true + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + outputs: + resource_group_name: ${{ steps.tf.outputs.resource_group_name }} + container_app_name: ${{ steps.tf.outputs.container_app_name }} + registry_url: ${{ steps.tf.outputs.registry_url }} + steps: + - name: Checkout Terraform files only + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + sparse-checkout: | + src/terraform + sparse-checkout-cone-mode: true + persist-credentials: false + + - name: Token replace Terraform env vars + env: + ENVIRONMENT: ${{ vars.ENV_SHORT }} + run: | + sed "s|__ENVIRONMENT__|${ENVIRONMENT}|g" \ + src/terraform/environment.tfvars \ + > src/terraform/environment.auto.tfvars + + - name: Azure Login + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Setup Terraform + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd + with: + terraform_version: ${{ inputs.terraform-version }} + + - name: Terraform Init + run: | + terraform -chdir=src/terraform init -input=false \ + -backend-config="resource_group_name=${VARS_TFSTATE_RESOURCE_GROUP}" \ + -backend-config="storage_account_name=${VARS_TFSTATE_STORAGE_ACCOUNT}" \ + -backend-config="container_name=${VARS_TFSTATE_CONTAINER}" \ + -backend-config="key=${VARS_TFSTATE_KEY}" \ + -backend-config="client_id=${AZURE_CLIENT_ID}" \ + -backend-config="tenant_id=${AZURE_TENANT_ID}" + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + VARS_TFSTATE_RESOURCE_GROUP: ${{ vars.TFSTATE_RESOURCE_GROUP }} + VARS_TFSTATE_STORAGE_ACCOUNT: ${{ vars.TFSTATE_STORAGE_ACCOUNT }} + VARS_TFSTATE_CONTAINER: ${{ vars.TFSTATE_CONTAINER }} + VARS_TFSTATE_KEY: ${{ vars.TFSTATE_KEY }} + + - name: Terraform Plan + run: terraform -chdir=src/terraform plan -input=false + + - name: Terraform Apply + if: inputs.apply + run: terraform -chdir=src/terraform apply -input=false -auto-approve + + - name: Read Terraform outputs + id: tf + if: inputs.read-outputs + run: | + echo "resource_group_name=$(terraform -chdir=src/terraform output -raw resource_group_name)" >> "$GITHUB_OUTPUT" + echo "container_app_name=$(terraform -chdir=src/terraform output -raw container_app_name)" >> "$GITHUB_OUTPUT" + echo "registry_url=$(terraform -chdir=src/terraform output -raw container_registry_login_server)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 847d8c3..09bef9e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -27,99 +27,36 @@ jobs: terraform: - 'src/terraform/**' dotnet: + - 'Dockerfile' - 'src/dotnet/**' test-dotnet: permissions: contents: read - runs-on: ubuntu-latest - steps: - - name: 'Checkout' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: 'Setup .NET' - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: '9.0.x' - - - name: 'Restore dependencies' - run: dotnet restore TheSexy6BotWorker.slnx - - - name: 'Build' - run: dotnet build TheSexy6BotWorker.slnx --no-restore - - - name: 'Test' - run: dotnet test TheSexy6BotWorker.slnx --no-build --verbosity normal --filter "Category!=Integration" + uses: ./.github/workflows/_dotnet-test.yml + + container-smoke: + permissions: + contents: read + needs: + - changes + - test-dotnet + if: needs.changes.outputs.dotnet == 'true' || needs.changes.outputs.terraform == 'true' + uses: ./.github/workflows/_container-smoke.yml + with: + git-sha: ${{ github.sha }} terraform-plan-apply: permissions: id-token: write contents: read - environment: stg - runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.terraform == 'true' - env: - ARM_USE_OIDC: true - ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - steps: - - name: Checkout Terraform files only - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - sparse-checkout: | - src/terraform - sparse-checkout-cone-mode: true - persist-credentials: false - - - name: Token replace Terraform env vars - env: - ENVIRONMENT: ${{ vars.ENV_SHORT }} - run: | - sed "s|__ENVIRONMENT__|${ENVIRONMENT}|g" \ - src/terraform/environment.tfvars \ - > src/terraform/environment.auto.tfvars - - - name: Azure Login - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - name: Setup Terraform - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd - with: - terraform_version: 1.9.8 - - name: Terraform Init - run: | - terraform -chdir=src/terraform init -input=false \ - -backend-config="resource_group_name=${VARS_TFSTATE_RESOURCE_GROUP}" \ - -backend-config="storage_account_name=${VARS_TFSTATE_STORAGE_ACCOUNT}" \ - -backend-config="container_name=${VARS_TFSTATE_CONTAINER}" \ - -backend-config="key=${VARS_TFSTATE_KEY}" \ - -backend-config="client_id=${{ secrets.AZURE_CLIENT_ID }}" \ - -backend-config="tenant_id=${{ secrets.AZURE_TENANT_ID }}" \ - env: - VARS_TFSTATE_RESOURCE_GROUP: ${{ vars.TFSTATE_RESOURCE_GROUP}} - VARS_TFSTATE_STORAGE_ACCOUNT: ${{ vars.TFSTATE_STORAGE_ACCOUNT}} - VARS_TFSTATE_CONTAINER: ${{ vars.TFSTATE_CONTAINER}} - VARS_TFSTATE_KEY: ${{ vars.TFSTATE_KEY}} - - - name: Terraform Plan - run: terraform -chdir=src/terraform plan -input=false - - - name: Terraform Apply - run: terraform -chdir=src/terraform apply -input=false -auto-approve - - - name: Read Terraform outputs - id: tf - run: | - echo "resource_group_name=$(terraform -chdir=src/terraform output -raw resource_group_name)" >> "$GITHUB_OUTPUT" - echo "container_app_name=$(terraform -chdir=src/terraform output -raw container_app_name)" >> "$GITHUB_OUTPUT" - echo "registry_url=$(terraform -chdir=src/terraform output -raw container_registry_login_server)" >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/_terraform.yml + with: + apply: true + github-environment: stg + read-outputs: true build-and-deploy-dotnet: runs-on: ubuntu-latest @@ -127,11 +64,13 @@ jobs: - changes - test-dotnet - terraform-plan-apply + - container-smoke if: >- ${{ always() && (needs.changes.outputs.dotnet == 'true' || needs.changes.outputs.terraform == 'true') && needs.test-dotnet.result == 'success' && + needs.container-smoke.result == 'success' && (needs.terraform-plan-apply.result == 'success' || needs.terraform-plan-apply.result == 'skipped') }} permissions: @@ -144,7 +83,7 @@ jobs: ENVIRONMENT: ${{ vars.ENV_SHORT }} steps: - - name: Checkout Terraform files only + - name: Checkout app files only uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 4d5a4d1..190b889 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -14,6 +14,7 @@ jobs: outputs: terraform: ${{ steps.filter.outputs.terraform }} github: ${{ steps.filter.outputs.github }} + dotnet: ${{ steps.filter.outputs.dotnet }} steps: - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d id: filter @@ -23,6 +24,11 @@ jobs: - 'src/terraform/**' github: - '.github/**' + dotnet: + - 'Dockerfile' + - '.github/workflows/_container-smoke.yml' + - '.github/workflows/_dotnet-test.yml' + - 'src/dotnet/**' zizmor-scan: @@ -54,96 +60,41 @@ jobs: permissions: id-token: write contents: read - environment: stg - runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.terraform == 'true' - env: - ARM_USE_OIDC: true - ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - steps: - - name: Checkout Terraform files only - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - sparse-checkout: | - src/terraform - sparse-checkout-cone-mode: true - persist-credentials: false - - - name: Token replace Terraform env vars - env: - ENVIRONMENT: ${{ vars.ENV_SHORT }} - run: | - sed "s|__ENVIRONMENT__|${ENVIRONMENT}|g" \ - src/terraform/environment.tfvars \ - > src/terraform/environment.auto.tfvars - - - name: Azure Login - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - name: Setup Terraform - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd - with: - terraform_version: 1.9.8 - - - name: Terraform Init - run: | - terraform -chdir=src/terraform init -input=false \ - -backend-config="resource_group_name=${VARS_TFSTATE_RESOURCE_GROUP}" \ - -backend-config="storage_account_name=${VARS_TFSTATE_STORAGE_ACCOUNT}" \ - -backend-config="container_name=${VARS_TFSTATE_CONTAINER}" \ - -backend-config="key=${VARS_TFSTATE_KEY}" \ - -backend-config="client_id=${{ secrets.AZURE_CLIENT_ID }}" \ - -backend-config="tenant_id=${{ secrets.AZURE_TENANT_ID }}" \ - env: - VARS_TFSTATE_RESOURCE_GROUP: ${{ vars.TFSTATE_RESOURCE_GROUP}} - VARS_TFSTATE_STORAGE_ACCOUNT: ${{ vars.TFSTATE_STORAGE_ACCOUNT}} - VARS_TFSTATE_CONTAINER: ${{ vars.TFSTATE_CONTAINER}} - VARS_TFSTATE_KEY: ${{ vars.TFSTATE_KEY}} - - - name: Terraform Plan - run: terraform -chdir=src/terraform plan -input=false + uses: ./.github/workflows/_terraform.yml + with: + github-environment: stg test-dotnet: permissions: contents: read - runs-on: ubuntu-latest - steps: - - name: 'Checkout' - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: 'Setup .NET' - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: '9.0.x' - - - name: 'Restore dependencies' - run: dotnet restore TheSexy6BotWorker.slnx - - - name: 'Build' - run: dotnet build TheSexy6BotWorker.slnx --no-restore - - - name: 'Test' - run: dotnet test TheSexy6BotWorker.slnx --no-build --verbosity normal --filter "Category!=Integration" + uses: ./.github/workflows/_dotnet-test.yml + + container-smoke: + permissions: + contents: read + needs: + - changes + - test-dotnet + if: needs.changes.outputs.dotnet == 'true' + uses: ./.github/workflows/_container-smoke.yml + with: + git-sha: ${{ github.sha }} status-check: if: ${{ always() }} - needs: [test-dotnet, terraform-plan, zizmor-scan] + needs: [test-dotnet, terraform-plan, zizmor-scan, container-smoke] runs-on: ubuntu-latest steps: - name: Fail if required jobs failed run: | echo "dotnet=${{ needs.test-dotnet.result }}" + echo "container-smoke=${{ needs.container-smoke.result }}" echo "terraform=${{ needs.terraform-plan.result }}" echo "zizmor=${{ needs.zizmor-scan.result }}" test "${{ needs.test-dotnet.result }}" = "success" + test "${{ needs.container-smoke.result }}" = "success" -o "${{ needs.container-smoke.result }}" = "skipped" test "${{ needs.terraform-plan.result }}" = "success" -o "${{ needs.terraform-plan.result }}" = "skipped" test "${{ needs.zizmor-scan.result }}" = "success" -o "${{ needs.zizmor-scan.result }}" = "skipped" diff --git a/Dockerfile b/Dockerfile index 4ce557d..f030efd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,11 @@ WORKDIR /app # This stage is used to build the service project FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj", "TheSexy6BotWorker/"] -RUN dotnet restore "./TheSexy6BotWorker/TheSexy6BotWorker.csproj" +WORKDIR /repo +COPY ["src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj", "src/dotnet/TheSexy6BotWorker/"] +RUN dotnet restore "./src/dotnet/TheSexy6BotWorker/TheSexy6BotWorker.csproj" COPY . . -WORKDIR "/src/TheSexy6BotWorker" +WORKDIR "/repo/src/dotnet/TheSexy6BotWorker" RUN dotnet build "./TheSexy6BotWorker.csproj" -c $BUILD_CONFIGURATION -o /app/build # This stage is used to publish the service project to be copied to the final stage @@ -29,4 +29,4 @@ ARG GIT_SHA=local ARG GIT_COMMIT_MSG=unknown ENV APP_VERSION=$GIT_SHA ENV APP_COMMIT_MSG=$GIT_COMMIT_MSG -ENTRYPOINT ["dotnet", "TheSexy6BotWorker.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "TheSexy6BotWorker.dll"] diff --git a/src/dotnet/TheSexy6BotWorker/Program.cs b/src/dotnet/TheSexy6BotWorker/Program.cs index 6337cc8..d0abef8 100644 --- a/src/dotnet/TheSexy6BotWorker/Program.cs +++ b/src/dotnet/TheSexy6BotWorker/Program.cs @@ -5,18 +5,37 @@ namespace TheSexy6BotWorker { public class Program { - public static void Main(string[] args) + private const string SmokeTestArgument = "--smoke-test"; + + public static int Main(string[] args) { - var builder = Host.CreateApplicationBuilder(args); + var isSmokeTest = args.Contains(SmokeTestArgument, StringComparer.OrdinalIgnoreCase); + var hostArgs = args + .Where(arg => !string.Equals(arg, SmokeTestArgument, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + var builder = Host.CreateApplicationBuilder(hostArgs); builder.Configuration.AddUserSecrets(); - builder.Services - .AddHostedService(); + if (!isSmokeTest) + { + builder.Services + .AddHostedService(); + } + + using var host = builder.Build(); + if (isSmokeTest) + { + Console.WriteLine("Container smoke test passed."); + Console.WriteLine($"APP_VERSION={Environment.GetEnvironmentVariable("APP_VERSION") ?? "unset"}"); + Console.WriteLine($"APP_COMMIT_MSG={Environment.GetEnvironmentVariable("APP_COMMIT_MSG") ?? "unset"}"); + return 0; + } - var host = builder.Build(); host.Run(); + return 0; } } }