This repository contains example GitHub Actions workflows that build, publish and deploy Node.js services. Each workflow is designed to be re-used via workflow_call in other projects.
This workflow prepares the Node.js project, runs tests and builds a Docker image.
- Sets up the Node environment and caches dependencies
- Runs lint and tests (optionally publishing coverage)
- Publishes a release using semantic-release
- Builds a Docker image and uploads it as an artifact
- Outputs the service name so other workflows can use it
- Validates the workers declared in
.quero/workers/, when the service has any (see below)
Loads the Docker image artifact produced by CI-BASE and pushes it to Amazon ECR.
- Configures AWS credentials and logs in to ECR
- Tags the image with the commit SHA and environment
- Optionally tags the pull request number
- Outputs the service name and image digest
Deploys the previously built image to Kubernetes.
- Checks out the
kubemanifests repository - Loads environment variables from AWS SSM
- Renders manifests with
envsubstand applies them withkubectl - Manages deployment metadata in
.deploys/service.json - Optionally comments on the pull request with the deployed image
Removes preview environments created for pull requests by deleting the Kubernetes namespace.
Promotes an image from one environment to another.
- Reads deployment metadata from
.deploys/service.jsonor a dev artifact - Updates the target environment entry and retags the image in ECR
- Pushes the updated metadata back to the
deploy-statebranch
Deploys the workers a service declares in .quero/workers/, one job per worker, derived from the declaration — adding a worker adds no workflow line to any service.
It does not call CI-PUSH_GITOPS, which derives both the definitions folder and the SSM path from the service name — a worker needs them to differ. Worker sizing (cpu, memory, probes, replicas) stays in kube-apps-definitions, owned by DevOps.
Open point. The stg ApplicationSet sets
destination.namespace: '{{path.basename}}', so<service>-worker-<name>lands in a namespace of its own and does not see the service's ConfigMap. This workflow only writesversion-values.yaml; something still has to create<service>-worker-<name>-env-cmfrom the service's SSM path before the first deploy.
The matrix is serial on purpose: the jobs commit to the same definitions repo, and concurrent pushes turn into rebase conflicts.
Runs semantic-release on the main branch. It is mainly used locally to generate releases.
A service declares each worker in one file, .quero/workers/<name>.yaml:
name: consumers
description: Consumes the catalog Kafka topics
owner: team-merchant
entrypoint: dist/src/worker.jsThe deployed command is derived from it as node <entrypoint> <name>, and the app in kube-apps-definitions is <service>-worker-<name>.
Adopting the convention is creating the file. CI-BASE detects the directory and skips every worker job when it is absent, so a service with no worker pays nothing.
The only wiring a service adds is the deploy call, once per environment, next to the api deploy:
deploy-workers-stg:
needs: publish-ecr-stage
uses: QueroDelivery/ci/.github/workflows/ci_workers_deploy.yml@v5.7.1
with:
WF_SERVICE_NAME: ${{ needs.publish-ecr-stage.outputs.servicename }}
WF_ENV_TYPE_DEPLOY: staging
WF_IMAGE_TAG: ${{ needs.publish-ecr-stage.outputs.version_tag }}
secrets:
WF_GITHUB_TOKEN: ${{ secrets.GH_TOKEN_CI }}CI-WORKERS-DEPLOY reads .quero/workers/ from the calling repository, so the list is never repeated in the service — a worker added or removed changes one file. WF_WORKERS remains for a caller that already holds the list, such as the workers output of CI-BASE, and skips the extra checkout.
Three things are checked, in this order:
- The declaration, before
yarn release. Schema,namematching the filename, a single entrypoint across the service, and an existingsrc/workers/<name>.worker.ts. Cheap and fails fast — it runs before the release precisely because the authoritative check cannot. - The declaration against the built image (
validate-workers). Runs<entrypoint> --listinside the image that is about to ship and compares the set of workers it accepts with the declared one. This is the authoritative check, and it can only run afterdocker build— which happens afteryarn release. Hence the split. - The declaration against
kube-apps-definitions(validate-workers-defs). Read-only: DevOps keeps the pen. A declared worker with no folder never deploys; a folder with no declaration means a pod running a name the entrypoint does not know, which isCrashLoopBackOffforever. Blocking on push, warning on pull request, becausedefs/mainis a moving target and a developer's PR should not go red over someone else's pending change.
The --list contract lives in @querodelivery/qd-packages (WorkerBootstrap): it prints the registry as JSON and exits before touching Mongo, Redis or the broker, so it answers with no environment configured.
sequenceDiagram
participant Dev as Developer
participant CI_BASE as CI-BASE
participant CI_ECR as CI-ECR-BASE
participant Deploy as CI-K8-BASE
Dev->>CI_BASE: workflow_call
CI_BASE->>CI_ECR: upload image artifact
CI_ECR->>Deploy: push image & output digest
Deploy->>Kubernetes: apply manifests
flowchart TD
A[Deploy metadata] --> B[promote.yml]
B --> C{FROM_ENV}
C -->|dev| D[Download artifact]
C -->|stg/prod| E[Read .deploys/service.json]
D --> F[Update metadata for TO_ENV]
E --> F
F --> G[Retag image in ECR]
G --> H[Push deploy-state]
The ci_local.yml workflow is used only for running semantic-release locally