Skip to content
Open
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
108 changes: 108 additions & 0 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# CI Build — Reusable Workflow
#
# Runs the production build for Aperim Node.js/pnpm product repos.
# Calls aperim/.github/.github/actions/setup-node-pnpm for consistent setup.
#
# Usage in product repo:
#
# jobs:
# build:
# uses: aperim/.github/.github/workflows/ci-build.yml@main
# needs: [quality, test]
# with:
# runner: '["self-hosted","ubuntu-latest","aperim"]'
# build-command: 'pnpm build'
#
# For Turborepo monorepos that need token builds or custom pre-build steps,
# extend via build-pre-command / build-post-command inputs.

name: CI Build

on:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout. Defaults to the triggering ref.'
required: false
type: string
default: ''
runner:
description: >
runs-on value as a JSON string. Use '"ubuntu-latest"' for GitHub-hosted
or '["self-hosted","ubuntu-latest","aperim"]' for Aperim self-hosted.
required: false
type: string
default: '"ubuntu-latest"'
node-version-file:
description: 'File containing Node.js version (e.g. .nvmrc).'
required: false
type: string
default: '.nvmrc'
build-pre-command:
description: 'Optional command to run before the main build (e.g. token/codegen generation). Set to empty string to skip.'
required: false
type: string
default: ''
build-command:
description: 'Command to run the production build.'
required: false
type: string
default: 'pnpm build'
build-post-command:
description: 'Optional command to run after the main build (e.g. bundle validation). Set to empty string to skip.'
required: false
type: string
default: ''
timeout-minutes:
description: 'Job timeout in minutes.'
required: false
type: number
default: 20
upload-artifact:
description: 'Set to true to upload the build artifact.'
required: false
type: boolean
default: false
artifact-path:
description: 'Path to the build artifact to upload. Only used when upload-artifact is true.'
required: false
type: string
default: 'dist'
artifact-name:
description: 'Name for the uploaded artifact. Only used when upload-artifact is true.'
required: false
type: string
default: 'build'
secrets:
inherit: false

jobs:
build:
name: Build
runs-on: ${{ fromJSON(inputs.runner) }}
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- name: Setup Node.js + pnpm
uses: aperim/.github/.github/actions/setup-node-pnpm@main
with:
ref: ${{ inputs.ref }}
node-version-file: ${{ inputs.node-version-file }}

- name: Pre-build step
if: inputs.build-pre-command != ''
run: ${{ inputs.build-pre-command }}

- name: Build
run: ${{ inputs.build-command }}

- name: Post-build step
if: inputs.build-post-command != ''
run: ${{ inputs.build-post-command }}

- name: Upload build artifact
if: inputs.upload-artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.artifact-path }}
retention-days: 7
76 changes: 76 additions & 0 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# CI Test — Reusable Workflow
#
# Runs pnpm tests (Vitest / Jest) for Aperim Node.js/pnpm product repos.
# Calls aperim/.github/.github/actions/setup-node-pnpm for consistent setup.
#
# Usage in product repo:
#
# jobs:
# test:
# uses: aperim/.github/.github/workflows/ci-test.yml@main
# with:
# runner: '["self-hosted","ubuntu-latest","aperim"]'
# test-command: 'pnpm test:run'
#
# For repos with DB service containers (e.g. aegis-command, budget),
# keep per-repo CI rather than wrapping this — service container setup
# is not expressible in reusable workflow inputs.

name: CI Test

on:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout. Defaults to the triggering ref.'
required: false
type: string
default: ''
runner:
description: >
runs-on value as a JSON string. Use '"ubuntu-latest"' for GitHub-hosted
or '["self-hosted","ubuntu-latest","aperim"]' for Aperim self-hosted.
required: false
type: string
default: '"ubuntu-latest"'
node-version-file:
description: 'File containing Node.js version (e.g. .nvmrc).'
required: false
type: string
default: '.nvmrc'
test-command:
description: 'Command to run tests.'
required: false
type: string
default: 'pnpm test:run'
coverage-command:
description: 'Command to run tests with coverage. Set to empty string to skip.'
required: false
type: string
default: ''
timeout-minutes:
description: 'Job timeout in minutes.'
required: false
type: number
default: 30
secrets:
inherit: false

jobs:
test:
name: Test
runs-on: ${{ fromJSON(inputs.runner) }}
timeout-minutes: ${{ inputs.timeout-minutes }}
steps:
- name: Setup Node.js + pnpm
uses: aperim/.github/.github/actions/setup-node-pnpm@main
with:
ref: ${{ inputs.ref }}
node-version-file: ${{ inputs.node-version-file }}

- name: Run tests
run: ${{ inputs.test-command }}

- name: Run tests with coverage
if: inputs.coverage-command != ''
run: ${{ inputs.coverage-command }}
150 changes: 150 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Deploy Staging — Reusable Workflow
#
# Builds a Docker image, pushes to GHCR, and rolls out to the Aperim DOKS
# (DigitalOcean Kubernetes, syd1) staging namespace via kubectl.
#
# Usage in product repo:
#
# jobs:
# deploy-staging:
# uses: aperim/.github/.github/workflows/deploy-staging.yml@main
# needs: [build]
# if: github.ref == 'refs/heads/main'
# with:
# image-name: itsacademic-api
# k8s-namespace: itsacademic-staging
# secrets:
# KUBECONFIG_STAGING: ${{ secrets.KUBECONFIG_STAGING }}
#
# Secrets required in the calling repo:
# KUBECONFIG_STAGING — base64-encoded kubeconfig for DOKS staging cluster.
# Generate with: kubectl config view --minify --raw | base64

name: Deploy Staging

on:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout. Defaults to the triggering ref.'
required: false
type: string
default: ''
runner:
description: >
runs-on value as a JSON string. Use '"ubuntu-latest"' for GitHub-hosted
or '["self-hosted","ubuntu-latest","aperim"]' for Aperim self-hosted.
required: false
type: string
default: '"ubuntu-latest"'
image-name:
description: 'Docker image name (without registry prefix). E.g. itsacademic-api.'
required: true
type: string
image-tag:
description: 'Tag to apply to the image. Defaults to the short SHA.'
required: false
type: string
default: ''
dockerfile:
description: 'Path to the Dockerfile.'
required: false
type: string
default: 'Dockerfile'
build-context:
description: 'Docker build context path.'
required: false
type: string
default: '.'
k8s-namespace:
description: 'Kubernetes namespace to deploy into.'
required: true
type: string
k8s-deployment:
description: 'Kubernetes deployment name to roll out. Defaults to image-name.'
required: false
type: string
default: ''
k8s-container:
description: 'Container name within the deployment. Defaults to image-name.'
required: false
type: string
default: ''
timeout-minutes:
description: 'Job timeout in minutes.'
required: false
type: number
default: 15
secrets:
KUBECONFIG_STAGING:
description: 'Base64-encoded kubeconfig for the DOKS staging cluster.'
required: true

env:
REGISTRY: ghcr.io
IMAGE_REPO: aperim

jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ${{ fromJSON(inputs.runner) }}
timeout-minutes: ${{ inputs.timeout-minutes }}
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}
clean: true

- name: Log in to GHCR
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Resolve image tag
id: tag
run: |
TAG="${{ inputs.image-tag }}"
if [ -z "$TAG" ]; then
TAG="${GITHUB_SHA::7}"
fi
echo "value=$TAG" >> "$GITHUB_OUTPUT"

- name: Build and push Docker image
uses: docker/build-push-action@1dc0e8c6c5ab11e66a12e3f37a2c7e80a36de8f8 # v6
with:
context: ${{ inputs.build-context }}
file: ${{ inputs.dockerfile }}
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/${{ inputs.image-name }}:${{ steps.tag.outputs.value }}
${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/${{ inputs.image-name }}:staging-latest

- name: Setup kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 --decode > ~/.kube/config
chmod 600 ~/.kube/config

- name: Install kubectl
uses: azure/setup-kubectl@776c3a21bd6bb6a5862c2dd87de47f06fbee855c # v4

- name: Roll out to staging
run: |
DEPLOYMENT="${{ inputs.k8s-deployment != '' && inputs.k8s-deployment || inputs.image-name }}"
CONTAINER="${{ inputs.k8s-container != '' && inputs.k8s-container || inputs.image-name }}"
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_REPO }}/${{ inputs.image-name }}:${{ steps.tag.outputs.value }}"

kubectl set image deployment/"$DEPLOYMENT" \
"$CONTAINER"="$IMAGE" \
--namespace="${{ inputs.k8s-namespace }}"

kubectl rollout status deployment/"$DEPLOYMENT" \
--namespace="${{ inputs.k8s-namespace }}" \
--timeout=5m