diff --git a/.github/ISSUE_TEMPLATE/new-change-in-functionality.md b/.github/ISSUE_TEMPLATE/new-change-in-functionality.md index 9b88e66b7..f328236c3 100644 --- a/.github/ISSUE_TEMPLATE/new-change-in-functionality.md +++ b/.github/ISSUE_TEMPLATE/new-change-in-functionality.md @@ -26,8 +26,8 @@ If there are guidelines on architecture or other implementation choices, they ar ``` ```[tasklist] -### Threat modelling -- [ ] Does this change introduce any potential security issues? +### Threat Modelling +- [ ] I have considered potential security risks (if risks were found, please list them below) ``` ### Acceptance criteria diff --git a/.github/workflows/ci-cd-prod-dry-run.yml b/.github/workflows/ci-cd-prod-dry-run.yml new file mode 100644 index 000000000..f848be912 --- /dev/null +++ b/.github/workflows/ci-cd-prod-dry-run.yml @@ -0,0 +1,89 @@ +name: CI/CD Production + +run-name: CI/CD Production Dry Run ${{ github.event.client_payload.version && format('({0})', github.event.client_payload.version) || '' }} + +on: + workflow_dispatch: + repository_dispatch: + types: [release_created] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + +jobs: + get-versions-from-github: + name: Get Latest Deployed Version Info from GitHub + uses: ./.github/workflows/workflow-get-latest-deployed-version-info-from-github.yml + with: + environment: prod + secrets: + GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} + + check-for-changes: + name: Check for changes + needs: [get-versions-from-github] + uses: ./.github/workflows/workflow-check-for-changes.yml + with: + infra_base_sha: ${{ needs.get-versions-from-github.outputs.infra_version_sha }} + apps_base_sha: ${{ needs.get-versions-from-github.outputs.apps_version_sha }} + + get-current-version: + name: Get current version + uses: ./.github/workflows/workflow-get-current-version.yml + + dry-run-deploy-infra: + name: Dry run deploy infra to prod + if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasInfraChanges == 'true' }} + needs: [get-current-version, check-for-changes] + uses: ./.github/workflows/workflow-deploy-infra.yml + secrets: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} + AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} + AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} + AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_PUBLIC_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_PUBLIC_KEY }} + with: + environment: prod + region: norwayeast + version: ${{ needs.get-current-version.outputs.version }} + dryRun: true + + dry-run-deploy-apps: + name: Dry run deploy apps to prod + needs: + [get-current-version, check-for-changes, dry-run-deploy-infra] + # we want deployment of apps to be dependent on deployment of infrastructure, but if infrastructure is skipped, we still want to dry-run deploy the apps + if: ${{ always() && !failure() && !cancelled() && (github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasBackendChanges == 'true') }} + uses: ./.github/workflows/workflow-deploy-apps.yml + secrets: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + # todo: consider resolving these in another way since they are created in the infra-step + AZURE_RESOURCE_GROUP_NAME: ${{ secrets.AZURE_RESOURCE_GROUP_NAME }} + AZURE_ENVIRONMENT_KEY_VAULT_NAME: ${{ secrets.AZURE_ENVIRONMENT_KEY_VAULT_NAME }} + AZURE_CONTAINER_APP_ENVIRONMENT_NAME: ${{ secrets.AZURE_CONTAINER_APP_ENVIRONMENT_NAME }} + AZURE_APP_INSIGHTS_CONNECTION_STRING: ${{ secrets.AZURE_APP_INSIGHTS_CONNECTION_STRING }} + AZURE_APP_CONFIGURATION_NAME: ${{ secrets.AZURE_APP_CONFIGURATION_NAME }} + AZURE_SERVICE_BUS_NAMESPACE_NAME: ${{ secrets.AZURE_SERVICE_BUS_NAMESPACE_NAME }} + with: + environment: prod + region: norwayeast + version: ${{ needs.get-current-version.outputs.version }} + dryRun: true + runMigration: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasMigrationChanges == 'true' }} + + send-slack-message-on-failure: + name: Send Slack message on failure + needs: [dry-run-deploy-infra, dry-run-deploy-apps] + if: ${{ always() && failure() && !cancelled() }} + uses: ./.github/workflows/workflow-send-ci-cd-status-slack-message.yml + with: + environment: prod + infra_status: ${{ needs.dry-run-deploy-infra.result }} + apps_status: ${{ needs.dry-run-deploy-apps.result }} + secrets: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID_FOR_CI_CD_STATUS }} diff --git a/.github/workflows/ci-cd-prod.yml b/.github/workflows/ci-cd-prod.yml index 3d54104d3..27ee16d48 100644 --- a/.github/workflows/ci-cd-prod.yml +++ b/.github/workflows/ci-cd-prod.yml @@ -2,10 +2,7 @@ on: workflow_dispatch: - push: - tags: - - "v*.*.*" - + concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} @@ -30,29 +27,10 @@ jobs: name: Get current version uses: ./.github/workflows/workflow-get-current-version.yml - dry-run-deploy-infra: - name: Dry run deploy infra to prod - if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasInfraChanges == 'true' }} - needs: [get-current-version, check-for-changes] - uses: ./.github/workflows/workflow-deploy-infra.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_SOURCE_KEY_VAULT_NAME: ${{ secrets.AZURE_SOURCE_KEY_VAULT_NAME }} - AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SUBSCRIPTION_ID }} - AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP: ${{ secrets.AZURE_SOURCE_KEY_VAULT_RESOURCE_GROUP }} - AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_PUBLIC_KEY: ${{ secrets.AZURE_SOURCE_KEY_VAULT_SSH_JUMPER_SSH_PUBLIC_KEY }} - with: - environment: prod - region: norwayeast - version: ${{ needs.get-current-version.outputs.version }} - dryRun: true - deploy-infra: name: Deploy infra to prod if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasInfraChanges == 'true' }} - needs: [get-current-version, check-for-changes, dry-run-deploy-infra] + needs: [get-current-version, check-for-changes] uses: ./.github/workflows/workflow-deploy-infra.yml secrets: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -79,35 +57,10 @@ jobs: secrets: GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} - dry-run-deploy-apps: - name: Dry run deploy apps to prod - needs: - [get-current-version, check-for-changes, deploy-infra] - # we want deployment of apps to be dependent on deployment of infrastructure, but if infrastructure is skipped, we still want to dry-run deploy the apps - if: ${{ always() && !failure() && !cancelled() && (github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasBackendChanges == 'true') }} - uses: ./.github/workflows/workflow-deploy-apps.yml - secrets: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - # todo: consider resolving these in another way since they are created in the infra-step - AZURE_RESOURCE_GROUP_NAME: ${{ secrets.AZURE_RESOURCE_GROUP_NAME }} - AZURE_ENVIRONMENT_KEY_VAULT_NAME: ${{ secrets.AZURE_ENVIRONMENT_KEY_VAULT_NAME }} - AZURE_CONTAINER_APP_ENVIRONMENT_NAME: ${{ secrets.AZURE_CONTAINER_APP_ENVIRONMENT_NAME }} - AZURE_APP_INSIGHTS_CONNECTION_STRING: ${{ secrets.AZURE_APP_INSIGHTS_CONNECTION_STRING }} - AZURE_APP_CONFIGURATION_NAME: ${{ secrets.AZURE_APP_CONFIGURATION_NAME }} - AZURE_SERVICE_BUS_NAMESPACE_NAME: ${{ secrets.AZURE_SERVICE_BUS_NAMESPACE_NAME }} - with: - environment: prod - region: norwayeast - version: ${{ needs.get-current-version.outputs.version }} - dryRun: true - runMigration: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasMigrationChanges == 'true' }} - deploy-apps: name: Deploy apps to prod needs: - [get-current-version, check-for-changes, dry-run-deploy-apps] + [get-current-version, check-for-changes] if: ${{ github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasBackendChanges == 'true' }} uses: ./.github/workflows/workflow-deploy-apps.yml secrets: @@ -139,23 +92,6 @@ jobs: secrets: GH_TOKEN: ${{ secrets.RELEASE_VERSION_STORAGE_PAT }} - # run-e2e-tests: - # name: "Run K6 functional end-to-end tests" - # # we want the end-to-end tests to be dependent on deployment of infrastructure and apps, but if infrastructure is skipped, we still want to run the tests - # if: ${{ always() && !failure() && !cancelled() && (github.event_name == 'workflow_dispatch' || needs.check-for-changes.outputs.hasBackendChanges == 'true') }} - # needs: [deploy-apps, check-for-changes] - # uses: ./.github/workflows/workflow-run-k6-tests.yml - # secrets: - # TOKEN_GENERATOR_USERNAME: ${{ secrets.TOKEN_GENERATOR_USERNAME }} - # TOKEN_GENERATOR_PASSWORD: ${{ secrets.TOKEN_GENERATOR_PASSWORD }} - # with: - # environment: prod - # apiVersion: v1 - # testSuitePath: tests/k6/suites/all-single-pass.js - # permissions: - # checks: write - # pull-requests: write - send-slack-message-on-failure: name: Send Slack message on failure needs: [deploy-infra, deploy-apps] diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2b0d67f7d..2ae1d080c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.48.1" + ".": "1.48.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a4f5072..8926cfb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.48.2](https://github.com/Altinn/dialogporten/compare/v1.48.1...v1.48.2) (2025-02-04) + + +### Miscellaneous Chores + +* Add DelayedShutdownHostLifetime to GraphQL and Service ([#1785](https://github.com/Altinn/dialogporten/issues/1785)) ([34dea8c](https://github.com/Altinn/dialogporten/commit/34dea8c08790278dc8872ef84de92bb6b6ecf857)) +* Reduce CPU load threshold, up initialDelays ([#1789](https://github.com/Altinn/dialogporten/issues/1789)) ([26abb48](https://github.com/Altinn/dialogporten/commit/26abb48aad2797558cf74bd76475bfe6537dac36)) +* refactor production deployment flow ([#1771](https://github.com/Altinn/dialogporten/issues/1771)) ([1b79f01](https://github.com/Altinn/dialogporten/commit/1b79f0107a9893d22981e18ddd30423808b8b663)) +* Simplify 404 NotFound swagger text ([#1791](https://github.com/Altinn/dialogporten/issues/1791)) ([1d4bc9a](https://github.com/Altinn/dialogporten/commit/1d4bc9ac552d3f15eae82e520a488987c824f8b7)) + ## [1.48.1](https://github.com/Altinn/dialogporten/compare/v1.48.0...v1.48.1) (2025-02-03) diff --git a/docs/schema/V1/swagger.verified.json b/docs/schema/V1/swagger.verified.json index 6811c47a6..ee359b42f 100644 --- a/docs/schema/V1/swagger.verified.json +++ b/docs/schema/V1/swagger.verified.json @@ -5070,7 +5070,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." } }, "security": [ @@ -6007,7 +6007,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6081,7 +6081,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." } }, "security": [ @@ -6276,7 +6276,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6355,7 +6355,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "412": { "content": { @@ -6586,7 +6586,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6732,7 +6732,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6798,7 +6798,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6858,7 +6858,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -6958,7 +6958,7 @@ } } }, - "description": "The given dialog ID was not found or is already deleted." + "description": "The given dialog ID was not found." }, "410": { "description": "Entity with the given key(s) is removed." @@ -7067,4 +7067,4 @@ "url": "https://altinn-dev-api.azure-api.net/dialogporten" } ] -} \ No newline at end of file +} diff --git a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs index 0dc823c97..f6fc6f1a0 100644 --- a/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs +++ b/src/Digdir.Domain.Dialogporten.GraphQL/Program.cs @@ -62,6 +62,11 @@ static void BuildAndRun(string[] args) .ValidateFluently() .ValidateOnStart(); + builder.Services.AddSingleton(sp => new DelayedShutdownHostLifetime( + sp.GetRequiredService(), + TimeSpan.FromSeconds(10) + )); + var thisAssembly = Assembly.GetExecutingAssembly(); builder.Services diff --git a/src/Digdir.Domain.Dialogporten.Service/Program.cs b/src/Digdir.Domain.Dialogporten.Service/Program.cs index 21a76fa1d..18725a02a 100644 --- a/src/Digdir.Domain.Dialogporten.Service/Program.cs +++ b/src/Digdir.Domain.Dialogporten.Service/Program.cs @@ -50,6 +50,11 @@ static void BuildAndRun(string[] args) .Enrich.FromLogContext() .WriteTo.OpenTelemetryOrConsole(context)); + builder.Services.AddSingleton(sp => new DelayedShutdownHostLifetime( + sp.GetRequiredService(), + TimeSpan.FromSeconds(10) + )); + builder.Services .AddDialogportenTelemetry(builder.Configuration, builder.Environment) .AddAzureAppConfiguration() diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs index 0a301b730..b34a4d67b 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs @@ -18,7 +18,7 @@ internal static class SwaggerSummary internal const string DomainError = "Domain error occurred. See problem details for a list of errors."; internal const string ServiceOwnerAuthenticationFailure = "Missing or invalid authentication token. Requires a Maskinporten-token with the scope \"{0}\"."; internal const string EndUserAuthenticationFailure = "Missing or invalid authentication token. Requires a Maskinporten-token with the scope \"digdir:dialogporten\"."; - internal const string DialogNotFound = "The given dialog ID was not found or is already deleted."; + internal const string DialogNotFound = "The given dialog ID was not found."; internal const string DialogDeleted = $"Entity with the given key(s) is removed."; internal const string DialogActivityNotFound = "The given dialog ID was not found or was deleted, or the given activity ID was not found."; internal const string DialogTransmissionNotFound = "The given dialog ID was not found or was deleted, or the given transmission ID was not found."; diff --git a/src/Digdir.Domain.Dialogporten.WebApi/DelayedShutdownHostLifetime.cs b/src/Digdir.Library.Utils.AspNet/DelayedShutdownHostLifetime.cs similarity index 78% rename from src/Digdir.Domain.Dialogporten.WebApi/DelayedShutdownHostLifetime.cs rename to src/Digdir.Library.Utils.AspNet/DelayedShutdownHostLifetime.cs index 762aef54a..3f0a1e628 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/DelayedShutdownHostLifetime.cs +++ b/src/Digdir.Library.Utils.AspNet/DelayedShutdownHostLifetime.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; +using Microsoft.Extensions.Hosting; -namespace Digdir.Domain.Dialogporten.WebApi; +namespace Digdir.Library.Utils.AspNet; public sealed class DelayedShutdownHostLifetime : IHostLifetime, IDisposable { @@ -14,19 +15,16 @@ public DelayedShutdownHostLifetime(IHostApplicationLifetime applicationLifetime, _delay = delay; } - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task WaitForStartAsync(CancellationToken cancellationToken) { - _disposables = new IDisposable[] - { + _disposables = + [ PosixSignalRegistration.Create(PosixSignal.SIGINT, HandleSignal), PosixSignalRegistration.Create(PosixSignal.SIGQUIT, HandleSignal), PosixSignalRegistration.Create(PosixSignal.SIGTERM, HandleSignal) - }; + ]; return Task.CompletedTask; } @@ -38,10 +36,10 @@ private void HandleSignal(PosixSignalContext ctx) public void Dispose() { - foreach (var disposable in _disposables ?? Enumerable.Empty()) + foreach (var disposable in _disposables ?? []) { disposable.Dispose(); } GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/version.txt b/version.txt index 5525f03fa..4b12da2cb 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.48.1 +1.48.2