diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 03f7cab2eb862a044bda634b6d05033226beb535..e13d9b496c605ec7e50660e97dd027ed9f9d45fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,23 +9,23 @@ default: - 1cpu-4gb # build on smaller machine variables: - API_DOMAIN_PATH: "$CI_PROJECT_DIR/api_domain" + API_DOMAIN_PATH: '$CI_PROJECT_DIR/api_domain' # job templates .deploy: - image: + image: name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.6.6' # default entrypoint is terraform command, but we want to run shell scripts - entrypoint: ["/bin/sh", "-c"] + entrypoint: ['/bin/sh', '-c'] variables: ENVIRONMENT_ID: $CI_ENVIRONMENT_SLUG artifacts: paths: - - "terraform/environments/crash.log" # optional, only available in case of a crash/panic - - "terraform/environments/terraform-*.log" # separate log for every step/command + - 'terraform/environments/crash.log' # optional, only available in case of a crash/panic + - 'terraform/environments/terraform-*.log' # separate log for every step/command - $API_DOMAIN_PATH - name: "${CI_JOB_NAME}_${CI_JOB_ID}" + name: '${CI_JOB_NAME}_${CI_JOB_ID}' #when: on_failure expire_in: 1 week script: @@ -34,12 +34,13 @@ variables: resource_group: $ENVIRONMENT_ID # never execute terraform in parallel on the same environment interruptible: false -.e2e: - image: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/archlinux:latest' +.smoketest: + image: 'europe-north1-docker.pkg.dev/holi-shared/docker/holi-docker/holi-k6-builder' script: - - API_DOMAIN=`cat $API_DOMAIN_PATH` - - echo "e2e tests against $CI_ENVIRONMENT_SLUG environment go here and against $API_DOMAIN" + - API_DOMAIN=$(cat $API_DOMAIN_PATH) - terraform/environments/scripts/wait-for-ssl.sh "https://${API_DOMAIN}" + - BASE_URL="https://${API_DOMAIN}/graphql" k6 run smoketest/main.js + # TODO should/could we roll back the service to the last working revision on test failure? # end job templates @@ -48,13 +49,13 @@ variables: ## common steps cache_lint_test: - image: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/denoland/deno:alpine-1.45.2' + image: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/denoland/deno:2.0.0' stage: 'test' script: - - deno cache --lock=deno.lock app/deps.ts app/dev_deps.ts + - deno cache --allow-import --lock=deno.lock app/deps.ts app/dev_deps.ts - deno fmt --check - deno lint - - deno test + - deno test --allow-import # You can override the included template(s) by including variable overrides # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings @@ -67,7 +68,7 @@ sast: needs: ['cache_lint_test'] stage: test include: -- template: Security/SAST.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml build_docker: needs: ['cache_lint_test'] @@ -83,7 +84,7 @@ build_docker: - docker push $GCR_IMAGE:$CI_COMMIT_SHA # this is the tag that is used for deployment - docker push $GCR_IMAGE:$CI_COMMIT_REF_SLUG # just for easyly knowing which is the last image for a branch -## review environments +## review environments review_deploy: extends: .deploy @@ -98,8 +99,8 @@ review_deploy: - production - /^noenv\/.*/ -review_e2e: - extends: .e2e +review_smoketest: + extends: .smoketest needs: ['review_deploy'] except: - main @@ -111,7 +112,7 @@ review_destroy: image: name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.6.6' # default entrypoint is terraform command, but we want to run shell scripts - entrypoint: ["/bin/sh", "-c"] + entrypoint: ['/bin/sh', '-c'] variables: # has to be set to none for auto stop GIT_STRATEGY: none @@ -122,9 +123,9 @@ review_destroy: dependencies: [] # explicitly disable artifact usage artifacts: paths: - - "terraform/environments/crash.log" # optional, only available in case of a crash/panic - - "terraform/environments/terraform-*.log" # separate log for every step/command - name: "${CI_JOB_NAME}_${CI_JOB_ID}" + - 'terraform/environments/crash.log' # optional, only available in case of a crash/panic + - 'terraform/environments/terraform-*.log' # separate log for every step/command + name: '${CI_JOB_NAME}_${CI_JOB_ID}' when: on_failure expire_in: 1 week script: @@ -134,7 +135,7 @@ review_destroy: - terraform/environments/scripts/destroy-env.sh $CI_ENVIRONMENT_SLUG # can't use rules here: https://gitlab.com/gitlab-org/gitlab/-/issues/34077 when: manual - allow_failure: false + allow_failure: true except: - main - production @@ -156,19 +157,18 @@ staging_deploy: only: - main -staging_trigger_unified-api_redeployment: +staging_smoketest: + extends: .smoketest needs: ['staging_deploy'] - trigger: - project: 'app/holi-unified-api' - branch: 'main' only: - main + resource_group: unified-api-staging -staging_e2e: - extends: .e2e - # if staging_e2e would actually run tests (other than ensuring SSL works), we'd have to - # wait for the unified-api pipeline to finish. - needs: ['staging_deploy'] +staging_trigger_unified-api_redeployment: + needs: ['staging_smoketest'] + trigger: + project: 'app/holi-unified-api' + branch: 'main' only: - main @@ -187,10 +187,17 @@ production_deploy: only: - production -production_trigger_unified-api_redeployment: +production_smoketest: + extends: .smoketest needs: ['production_deploy'] + only: + - production + +production_trigger_unified-api_redeployment: + needs: ['production_smoketest'] trigger: project: 'app/holi-unified-api' branch: 'production' only: - production + resource_group: unified-api-production diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f34ebc58adb3df395e52bedee0d670532274122..f4b830c8175179542b86e58cc479dbe443a5adbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: -- repo: local + - repo: local hooks: - - id: gitleaks + - id: gitleaks name: gitleaks language: system entry: gitleaks protect --staged -v -c ../.gitleaks.toml diff --git a/Dockerfile b/Dockerfile index 429280d4105036a87fc22f11ab4fde42ccc05c88..1af108e2fbed72747a2e494ce9460922f7562a4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/denoland/deno:alpine-1.45.2 +FROM europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/denoland/deno:2.0.0 # The port that your application listens to. EXPOSE 8001 @@ -10,7 +10,7 @@ COPY deno.lock . COPY deno.json . COPY ./app ./app -RUN deno cache --lock=deno.lock app/deps.ts -RUN deno cache app/main.ts +RUN deno cache --allow-import --lock=deno.lock app/deps.ts +RUN deno cache --allow-import app/main.ts -CMD ["deno", "run", "--allow-net", "--allow-env", "--allow-read", "--lock=deno.lock", "--cached-only", "app/main.ts"] +CMD ["deno", "run", "--allow-import", "--allow-net", "--allow-env", "--allow-read", "--lock=deno.lock", "--cached-only", "app/main.ts"] diff --git a/app/betterplace.ts b/app/betterplace.ts index 189edf31c5e8f38906309a0f1f6fe2d4d0b48e8e..285a4135913b46fafed4aecea08e10583737b208 100644 --- a/app/betterplace.ts +++ b/app/betterplace.ts @@ -149,7 +149,8 @@ export const fetchCategories = async (projectId: number): Promise<string[]> => { const json = await response.json() const categories = json.data as Array<ApiProjectCategory> return categories.map(transformProjectCategory) - } catch (e) { + // deno-lint-ignore no-explicit-any + } catch (e: any) { logger.warn( `Error while fetching project categories from '${href}', using empty array to handle this (somewhat) gracefully`, e, @@ -171,7 +172,8 @@ export const fetchNews = async ( const json = await response.json() const news = json.data as ApiBlogPost[] return news.map(transformNews) - } catch (e) { + // deno-lint-ignore no-explicit-any + } catch (e: any) { logger.warn( `Error while fetching blog posts from '${href}', using empty array to handle this (somewhat) gracefully`, e, @@ -190,7 +192,8 @@ export const fetchInitiativeUrl = async ( const response = await fetch(initiative.url) const organisation: ApiOrganisation = await response.json() return organisation.links.find((l) => l.rel === 'platform')?.href - } catch (e) { + // deno-lint-ignore no-explicit-any + } catch (e: any) { logger.warn( `Error while fetching organisation url from '${initiative.url}', using api url to handle this (somewhat) gracefully`, e, @@ -239,7 +242,8 @@ const fetchPageOfProjects = async ( const fetchResponse = await fetch(url) const jsonResponse = await fetchResponse.json() return transformProjectsResponse(jsonResponse) - } catch (e) { + // deno-lint-ignore no-explicit-any + } catch (e: any) { logger.error( `Error performing request to ${url}: ${e.message}`, ) @@ -297,7 +301,8 @@ export const fetchProject = async ( } const json = await response.json() return transformProject(json) - } catch (e) { + // deno-lint-ignore no-explicit-any + } catch (e: any) { if (e.extensions?.code !== ERROR_CODE_NOT_FOUND) { logger.error( `Error performing request to ${url}: ${e.message}`, diff --git a/deno.json b/deno.json index 4ffff98c81635a268f7ae97151213f6335534e3a..5b58ef88c0865f8c65fa018956afe86fafffd98b 100644 --- a/deno.json +++ b/deno.json @@ -4,14 +4,14 @@ "lint": "deno lint", "fmt": "deno fmt", "fmt:check": "deno fmt --check", - "test": "deno test", - "updateDeps": "deno cache --lock=deno.lock --lock-write app/deps.ts app/dev_deps.ts", - "install": "deno cache --reload --lock=deno.lock app/deps.ts app/dev_deps.ts", - "cache": "deno cache app/main.ts", - "dev": "deno run --allow-net --allow-env --allow-read --lock=deno.lock --cached-only --watch app/main.ts", - "start": "deno run --allow-net --allow-env --allow-read --lock=deno.lock --cached-only app/main.ts", + "test": "deno test --allow-import ", + "updateDeps": "deno cache --allow-import --lock=deno.lock --lock-write app/deps.ts app/dev_deps.ts", + "install": "deno cache --allow-import --reload --lock=deno.lock app/deps.ts app/dev_deps.ts", + "cache": "deno cache --allow-import app/main.ts", + "dev": "deno run --allow-import --allow-net --allow-env --allow-read --lock=deno.lock --cached-only --watch app/main.ts", + "start": "deno run --allow-import --allow-net --allow-env --allow-read --lock=deno.lock --cached-only app/main.ts", "docker": "docker build -t donations-api . && docker run -it --init -p 8001:8001 donations-api", - "coverage": "deno test --coverage=coverage && deno coverage coverage", + "coverage": "deno test --allow-import --coverage=coverage && deno coverage coverage", "pre-commit": { "cmd": "vr lint && vr fmt:check", "gitHook": "pre-commit" diff --git a/smoketest/main.js b/smoketest/main.js new file mode 100644 index 0000000000000000000000000000000000000000..fb5892d9a6621f44085e78323d65bfee38be739a --- /dev/null +++ b/smoketest/main.js @@ -0,0 +1,40 @@ +import http from 'k6/http' +import { check } from 'k6' + +// You don't need to change anything in this section, it's k6 glue code. +// See the default function at the end of the file for defining your smoketest. +// This configuration only executes 1 test, enough for a smoketest. The smoketest will fail on any check failing. +const allChecksNeedToPassTreshold = { checks: [{ threshold: 'rate==1', abortOnFail: true }] } +export const options = { + vus: 1, + iterations: 1, + thresholds: allChecksNeedToPassTreshold, +} + +/** + * Performs a GraphQL query and checks the response using the provided function. Fails if any of the provided expectations are not met. + * @param {string} query The GraphQL query to perform + * @param {(response: http.Response) => Array<boolean>} checkFunction + * A function that takes the HTTP response as an argument and returns an array + * of boolean values, each indicating success or failure of a test. + */ +function forQuery(query, checkFunction) { + const response = http.post(`${__ENV.BASE_URL}`, JSON.stringify({ query }), { + headers: { 'Content-Type': 'application/json' }, + }) + checkFunction(response) +} + +// Define your smoketest(s) here. +export default () => { + forQuery(`{projects(limit:1){data{id}}}`, (response) => { + check(response, { + 'is status 200': (r) => r.status === 200, + }) + check(JSON.parse(response.body), { + // there can be multiple tests here, e.g. + //"contains topics object": (r) => typeof r.data.topics != null, + 'contains projects': (r) => Array.isArray(r.data.projects.data) && r.data.projects.data.length > 0, + }) + }) +}