Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ jobs:
name: CI
uses: ./.github/workflows/ci.yml

package:
name: Empacotar
migrations:
name: Bundle de migrations
needs: ci
runs-on: ubuntu-latest
timeout-minutes: 15
Expand All @@ -49,13 +49,8 @@ jobs:
- name: Restaurar ferramentas locais
run: dotnet tool restore

- name: Publicar a API
run: >
dotnet publish src/Backend/PolicyDesk.Api/PolicyDesk.Api.csproj
--configuration Release
--output artifacts/api
-p:UseAppHost=false
-p:SourceRevisionId=${{ github.sha }}
- name: Restaurar a infraestrutura para linux-x64
run: dotnet restore src/Backend/PolicyDesk.Infrastructure/PolicyDesk.Infrastructure.csproj --runtime linux-x64

- name: Gerar o bundle de migrations
run: >
Expand All @@ -67,23 +62,61 @@ jobs:
--runtime linux-x64
--output artifacts/migrations/efbundle

- name: Anexar a API
uses: actions/upload-artifact@v4
with:
name: api
path: artifacts/api
retention-days: 7

- name: Anexar as migrations
uses: actions/upload-artifact@v4
with:
name: migrations
path: artifacts/migrations
retention-days: 7

image:
name: Imagem Docker
needs: ci
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.name.outputs.image }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Definir o nome da imagem
id: name
run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}-api" >> "$GITHUB_OUTPUT"

- name: Preparar o Buildx
uses: docker/setup-buildx-action@v3

- name: Login no GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Gerar e publicar a imagem
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
push: true
tags: |
${{ steps.name.outputs.image }}:${{ github.sha }}
${{ steps.name.outputs.image }}:latest
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=MIT
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
name: Deploy em produção
needs: package
needs: [migrations, image]
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
Expand All @@ -94,12 +127,6 @@ jobs:
id-token: write

steps:
- name: Baixar a API
uses: actions/download-artifact@v4
with:
name: api
path: api

- name: Baixar as migrations
uses: actions/download-artifact@v4
with:
Expand All @@ -120,16 +147,16 @@ jobs:
chmod +x migrations/efbundle
migrations/efbundle --connection "$DB_CONNECTION"

- name: Deploy no Azure App Service
- name: Deploy da imagem no Azure App Service
uses: azure/webapps-deploy@v3
with:
app-name: ${{ vars.AZURE_WEBAPP_NAME }}
package: api
images: ${{ needs.image.outputs.image }}:${{ github.sha }}

- name: Smoke test (/health)
run: >
curl --fail --silent --show-error
--retry 12 --retry-delay 10 --retry-all-errors
--retry 18 --retry-delay 10 --retry-all-errors
--max-time 30
"https://${{ vars.AZURE_WEBAPP_NAME }}.azurewebsites.net/health"

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,22 +328,24 @@ O pipeline roda no **GitHub Actions** e publica a API num **Azure App Service Li
flowchart LR
PR["Pull request<br/>ou push em developer"] --> CI
MAIN["Push em main"] --> CI
CI["<b>CI</b><br/>restore · vulnerabilidades<br/>build Release · testes · cobertura"] --> PKG
PKG["<b>Empacotar</b><br/>dotnet publish<br/>bundle de migrations"] --> DEP
DEP["<b>Deploy</b> (environment production)<br/>login OIDC · migrations<br/>App Service · smoke test /health"]
CI["<b>CI</b><br/>restore · vulnerabilidades<br/>build Release · testes · cobertura"] --> MIG
CI --> IMG
MIG["<b>Bundle de migrations</b>"] --> DEP
IMG["<b>Imagem Docker</b><br/>docker/Dockerfile → ghcr.io"] --> DEP
DEP["<b>Deploy</b> (environment production)<br/>login OIDC · migrations<br/>imagem no App Service · smoke test /health"]
```

| Workflow | Quando roda | O que faz |
| --- | --- | --- |
| `ci.yml` | PRs para `main`/`developer`, push em `developer` | Build com analisadores como erro, auditoria de pacotes vulneráveis, todos os testes (com Testcontainers) e relatório de cobertura no resumo da execução |
| `cd.yml` | Push em `main` ou manual | Reaproveita o CI, gera os artefatos e faz o deploy no environment `production` |
| `cd.yml` | Push em `main` ou manual | Reaproveita o CI, gera o bundle de migrations e a imagem Docker (publicada no `ghcr.io`) e faz o deploy no environment `production` |
| `codeql.yml` | PRs, pushes e semanalmente | Análise estática de segurança do código C# |
| `dependabot.yml` | Semanalmente | PRs de atualização de NuGet, Actions e imagens Docker, agrupados |

**Decisões:**

- **Sem segredos de longa duração**: o login na Azure usa OIDC (federated credentials); a identidade só tem `Website Contributor` no próprio Web App.
- **Build once, deploy many**: o artefato testado é o mesmo que vai para produção.
- **Build once, deploy many**: a imagem testada é a mesma que vai para produção, gerada pelo mesmo `docker/Dockerfile` usado localmente e identificada pelo SHA do commit.
- **Migrations fora da inicialização**: um *migrations bundle* do EF Core é aplicado antes do deploy, então a API em produção nunca altera o schema ao subir.
- **Infraestrutura como código**: o banco, o plano F1 e o Web App estão descritos em [`infra/main.bicep`](infra/main.bicep).

Expand Down
18 changes: 14 additions & 4 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ param databaseAdminPassword string
@description('Nome do banco usado pela API.')
param databaseName string = 'policydesk'

@description('Imagem da API. O pipeline de CD troca a tag pela do commit publicado.')
param containerImage string = 'ghcr.io/rodrigosdk/policydesk-api:latest'

@description('Endpoint OTLP opcional para exportar traces, métricas e logs.')
param otlpEndpoint string = ''

Expand Down Expand Up @@ -96,14 +99,13 @@ resource plan 'Microsoft.Web/serverfarms@2024-04-01' = {
resource app 'Microsoft.Web/sites@2024-04-01' = {
name: appName
location: location
kind: 'app,linux'
kind: 'app,linux,container'
properties: {
serverFarmId: plan.id
httpsOnly: true
clientAffinityEnabled: false
siteConfig: {
linuxFxVersion: 'DOTNETCORE|10.0'
appCommandLine: 'dotnet PolicyDesk.Api.dll'
linuxFxVersion: 'DOCKER|${containerImage}'
alwaysOn: false
http20Enabled: true
minTlsVersion: '1.2'
Expand All @@ -119,9 +121,17 @@ resource app 'Microsoft.Web/sites@2024-04-01' = {
value: 'true'
}
{
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
name: 'WEBSITES_PORT'
value: '8080'
}
{
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
value: 'false'
}
{
name: 'DOCKER_REGISTRY_SERVER_URL'
value: 'https://ghcr.io'
}
], otlpSettings)
connectionStrings: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ public Task AddAsync(Policy policy)
public async Task<PagedResult<Policy>> GetPageAsync(Guid? policyholderId, PolicyStatus? status, int page, int pageSize, CancellationToken cancellationToken = default)
{
var query = dbContext.Policies.AsNoTracking();
if (policyholderId.HasValue)
query = query.Where(policy => policy.PolicyholderId == policyholderId.Value);
if (status.HasValue)
query = query.Where(policy => policy.Status == status.Value);
if (policyholderId is { } policyholderIdFilter)
query = query.Where(policy => policy.PolicyholderId == policyholderIdFilter);
if (status is { } statusFilter)
query = query.Where(policy => policy.Status == statusFilter);

var totalCount = await query.CountAsync(cancellationToken);
var items = await query
Expand Down
6 changes: 2 additions & 4 deletions tests/src/PolicyDesk.Domain.Tests/Entities/ClaimsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ public void Deve_AprovarERegistrarData_Quando_SinistroEstiverEmAnalise()

var after = DateTime.UtcNow;
claim.Status.ShouldBe(ClaimStatus.Approved);
claim.ApprovedAt.ShouldNotBeNull();
claim.ApprovedAt.Value.ShouldBeInRange(before, after);
claim.ApprovedAt.ShouldNotBeNull().ShouldBeInRange(before, after);
claim.UpdatedAt.ShouldBe(claim.ApprovedAt);
}

Expand Down Expand Up @@ -141,8 +140,7 @@ public void Deve_RegistrarPagamento_Quando_SinistroEstiverAprovado()

var after = DateTime.UtcNow;
claim.Status.ShouldBe(ClaimStatus.Paid);
claim.PaidAt.ShouldNotBeNull();
claim.PaidAt.Value.ShouldBeInRange(before, after);
claim.PaidAt.ShouldNotBeNull().ShouldBeInRange(before, after);
claim.UpdatedAt.ShouldBe(claim.PaidAt);
}

Expand Down
6 changes: 2 additions & 4 deletions tests/src/PolicyDesk.Domain.Tests/Entities/PoliciesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ public void Deve_CancelarERegistrarDatas_Quando_Solicitado()
policy.Status.ShouldBe(PolicyStatus.Cancelled);
policy.IsCancelled.ShouldBeTrue();
policy.IsInForce(BusinessClockBuilder.DefaultToday).ShouldBeFalse();
policy.CancelledAt.ShouldNotBeNull();
policy.CancelledAt.Value.ShouldBeInRange(beforeCancellation, afterCancellation);
policy.CancelledAt.ShouldNotBeNull().ShouldBeInRange(beforeCancellation, afterCancellation);
policy.UpdatedAt.ShouldBe(policy.CancelledAt);
}

Expand Down Expand Up @@ -139,8 +138,7 @@ public void Deve_AceitarSinistroEAtualizarApolice_Quando_ValorCouberNoSaldo(deci

policy.RegisterClaim(amount, committedAmount);

policy.UpdatedAt.ShouldNotBeNull();
policy.UpdatedAt.Value.ShouldBeGreaterThanOrEqualTo(beforeRegistration);
policy.UpdatedAt.ShouldNotBeNull().ShouldBeGreaterThanOrEqualTo(beforeRegistration);
}

[Theory(DisplayName = nameof(Deve_LancarExcecaoSemAtualizar_Quando_ValorExcederSaldo))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ public void Deve_AtualizarOsDados_Quando_DadosForemValidos()
policyholder.BirthDate.ShouldBe(NewBirthDate);
policyholder.Email.ShouldBe("jane.doe@example.com");
policyholder.Phone.ShouldBe("+5583912345678");
policyholder.UpdatedAt.ShouldNotBeNull();
policyholder.UpdatedAt.Value.ShouldBeInRange(beforeUpdate, afterUpdate);
policyholder.UpdatedAt.ShouldNotBeNull().ShouldBeInRange(beforeUpdate, afterUpdate);
}

[Fact(DisplayName = nameof(Deve_ManterCpf_Quando_Atualizar))]
Expand Down Expand Up @@ -256,8 +255,7 @@ public void Deve_RegistrarAlteracao_Quando_ApoliceForCadastrada()

policyholder.RegisterPolicy();

policyholder.UpdatedAt.ShouldNotBeNull();
policyholder.UpdatedAt.Value.ShouldBeInRange(before, DateTime.UtcNow);
policyholder.UpdatedAt.ShouldNotBeNull().ShouldBeInRange(before, DateTime.UtcNow);
policyholder.Active.ShouldBeTrue();
}

Expand Down
Loading