Skip to content

Commit c1ff321

Browse files
committed
Add progressive delivery, build enhancements
1 parent e97ddb8 commit c1ff321

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1978
-365
lines changed

.github/workflows/docker-build.yml

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
name: Build and Release Docker Images
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'apps/**'
9+
# Only trigger on app changes or workflow changes for the starter
10+
- '.github/workflows/docker-build.yml'
11+
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: write # Needed for creating tags/releases
16+
packages: write # Needed for publishing packages to GHCR
17+
18+
jobs:
19+
discover-services:
20+
name: Discover Services
21+
runs-on: ubuntu-latest
22+
outputs:
23+
services: ${{ steps.set-services.outputs.services }}
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 2 # Need at least 2 commits to detect changes
29+
30+
- name: Find services with Dockerfiles and changes
31+
id: set-services
32+
run: |
33+
# Determine if this is a manual trigger
34+
IS_MANUAL="${{ github.event_name == 'workflow_dispatch' }}"
35+
36+
# Get list of changed files (compare with previous commit)
37+
if [[ "$IS_MANUAL" == "true" ]]; then
38+
echo "Manual workflow run - will build all services with Dockerfiles"
39+
CHANGED_FILES=$(find . -type f) # Consider all files as changed for manual runs
40+
else
41+
echo "Detecting changed files since previous commit"
42+
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
43+
fi
44+
45+
# For debugging
46+
echo "Changed files:"
47+
echo "$CHANGED_FILES"
48+
49+
# Function to check if a service has changes
50+
function has_changes() {
51+
local service=$1
52+
local directory=$2
53+
# Check for changes in the specific app directory, shared packages, or the workflow file itself
54+
echo "$CHANGED_FILES" | grep -q "^$directory/$service/" || \
55+
echo "$CHANGED_FILES" | grep -q "^packages/" || \
56+
echo "$CHANGED_FILES" | grep -q "^.github/workflows/"
57+
}
58+
59+
# Find all services in 'apps' with Dockerfiles
60+
APPS_WITH_DOCKERFILE=$(find apps -maxdepth 2 -name "Dockerfile" -type f | sed 's|apps/\([^/]*\)/Dockerfile|\1|')
61+
62+
# Filter services with changes
63+
APPS_SERVICES="[]"
64+
for APP in $APPS_WITH_DOCKERFILE; do
65+
if [[ "$IS_MANUAL" == "true" ]] || has_changes "$APP" "apps"; then
66+
APP_JSON=$(echo "$APP" | jq -R '{"name": ., "directory": "apps"}')
67+
APPS_SERVICES=$(echo "$APPS_SERVICES" | jq ". + [$APP_JSON]")
68+
echo "App $APP has changes and will be built"
69+
else
70+
echo "App $APP has no changes and will be skipped"
71+
fi
72+
done
73+
74+
# Output the services to build
75+
echo "services<<EOF" >> $GITHUB_OUTPUT
76+
echo "$APPS_SERVICES" >> $GITHUB_OUTPUT
77+
echo "EOF" >> $GITHUB_OUTPUT
78+
echo "Services to build: $APPS_SERVICES"
79+
80+
version:
81+
name: Generate Version
82+
runs-on: ubuntu-latest
83+
outputs:
84+
new_version: ${{ steps.set-version.outputs.new_version }}
85+
steps:
86+
- name: Checkout code
87+
uses: actions/checkout@v4
88+
with:
89+
fetch-depth: 0 # Fetch all history for version calculation
90+
91+
- name: Get current version from Git tags
92+
id: get-version
93+
run: |
94+
# Find the latest tag matching v*.*.*, default to 0.0.0 if none exist
95+
if git tag -l | grep -q "^v"; then
96+
LATEST_TAG=$(git tag -l "v*" | sort -V | tail -n 1)
97+
CURRENT_VERSION=${LATEST_TAG#v}
98+
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
99+
echo "Current version: $CURRENT_VERSION"
100+
else
101+
echo "current_version=0.0.0" >> $GITHUB_OUTPUT
102+
echo "No version tags found, defaulting to 0.0.0"
103+
fi
104+
105+
- name: Increment patch version
106+
id: set-version
107+
run: |
108+
CURRENT_VERSION=${{ steps.get-version.outputs.current_version }}
109+
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
110+
111+
MAJOR=${VERSION_PARTS[0]:-0}
112+
MINOR=${VERSION_PARTS[1]:-0}
113+
PATCH=${VERSION_PARTS[2]:-0}
114+
115+
# Increment patch version number
116+
NEW_PATCH=$((PATCH + 1))
117+
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
118+
119+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
120+
echo "New version: $NEW_VERSION"
121+
122+
- name: Create and push new Git tag
123+
run: |
124+
git config user.name "GitHub Actions"
125+
git config user.email "[email protected]"
126+
git tag -a "v${{ steps.set-version.outputs.new_version }}" -m "Release v${{ steps.set-version.outputs.new_version }}"
127+
git push origin "v${{ steps.set-version.outputs.new_version }}"
128+
129+
build-docker-images:
130+
name: Build Docker Images
131+
needs: [version, discover-services]
132+
runs-on: ubuntu-latest
133+
strategy:
134+
matrix:
135+
# Build one image per discovered service
136+
service: ${{ fromJson(needs.discover-services.outputs.services) }}
137+
steps:
138+
- name: Checkout code
139+
uses: actions/checkout@v4
140+
141+
- name: Set up QEMU for multi-platform builds
142+
uses: docker/setup-qemu-action@v3
143+
144+
- name: Set up Docker Buildx
145+
uses: docker/setup-buildx-action@v3
146+
147+
- name: Login to GitHub Container Registry
148+
uses: docker/login-action@v3
149+
with:
150+
registry: ghcr.io
151+
username: ${{ github.actor }}
152+
password: ${{ secrets.GITHUB_TOKEN }}
153+
154+
- name: Prepare repository name (lowercase)
155+
id: repo-name
156+
run: |
157+
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
158+
echo "lowercased=$REPO_NAME" >> $GITHUB_OUTPUT
159+
160+
- name: Build and push multi-platform image
161+
uses: docker/build-push-action@v5
162+
with:
163+
context: .
164+
# Dynamically set the Dockerfile path based on matrix service info
165+
file: ./${{ matrix.service.directory }}/${{ matrix.service.name }}/Dockerfile
166+
push: true
167+
platforms: linux/amd64,linux/arm64/v8
168+
tags: |
169+
ghcr.io/${{ steps.repo-name.outputs.lowercased }}/${{ matrix.service.name }}:${{ needs.version.outputs.new_version }}
170+
ghcr.io/${{ steps.repo-name.outputs.lowercased }}/${{ matrix.service.name }}:latest
171+
# Enable build cache for faster builds
172+
cache-from: type=gha
173+
cache-to: type=gha,mode=max
174+
175+
create-release:
176+
name: Create GitHub Release
177+
needs: [version, build-docker-images, discover-services]
178+
runs-on: ubuntu-latest
179+
steps:
180+
- name: Checkout code
181+
uses: actions/checkout@v4
182+
183+
- name: Prepare repository name (lowercase)
184+
id: repo-name
185+
run: |
186+
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
187+
echo "lowercased=$REPO_NAME" >> $GITHUB_OUTPUT
188+
189+
- name: Generate image list for release notes
190+
id: image-list
191+
run: |
192+
# Read services JSON from previous job output
193+
echo '${{ needs.discover-services.outputs.services }}' > services.json
194+
VERSION=${{ needs.version.outputs.new_version }}
195+
REPO=${{ steps.repo-name.outputs.lowercased }}
196+
197+
IMAGE_LIST=""
198+
# Loop through service names and create markdown list items
199+
while read -r SERVICE_NAME; do
200+
IMAGE_LIST="${IMAGE_LIST}- ghcr.io/${REPO}/${SERVICE_NAME}:${VERSION}\n"
201+
done < <(jq -r '.[].name' services.json)
202+
203+
# Store the list with escaped newlines for use in the release body
204+
echo "images<<EOF" >> $GITHUB_OUTPUT
205+
echo -e "$IMAGE_LIST" >> $GITHUB_OUTPUT
206+
echo "EOF" >> $GITHUB_OUTPUT
207+
208+
- name: Create GitHub release with generated notes
209+
uses: softprops/action-gh-release@v1
210+
with:
211+
name: Release v${{ needs.version.outputs.new_version }}
212+
tag_name: v${{ needs.version.outputs.new_version }}
213+
generate_release_notes: true # Auto-generate release notes from commits
214+
body: |
215+
# Release v${{ needs.version.outputs.new_version }}
216+
217+
Docker images built and published:
218+
${{ steps.image-list.outputs.images }}
219+
env:
220+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ yarn-error.log*
4343

4444
# Performance tests
4545
**/performance-tests/report/results.json
46+
47+
helm/**/charts/**
48+
helm/**/charts

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Turborepo Starter for Bun, Go, and Python Microservices on K8s
22

3-
This template is, in my opinion (although I haven't checked), probably the best developer experience for a performant cloud-native application available.
4-
5-
It includes examples for three types of services with private packages included.
3+
This repo includes examples for three types of services with private packages included.
64

75
These include:
86

apps/go-api/helm/go-api/Chart.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v2
2+
name: go-api
3+
description: A Helm chart for the Go API service
4+
type: application
5+
version: 0.1.0
6+
appVersion: "1.0.0" # Align with overall app version or manage independently

apps/go-api/helm/go-api/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Go API Helm Chart
2+
3+
This Helm chart deploys the Go API service. It's designed to be used as a subchart within the `turbo-k8s-starter` umbrella chart.
4+
5+
## Configuration Parameters
6+
7+
| Parameter | Description | Default |
8+
| :-------------------------------------------- | :-------------------------------------------------------------------------- | :------------------------ |
9+
| `replicaCount` | Initial number of replicas | `1` |
10+
| `image.repository` | Container image repository (overridden by umbrella/CI) | `go-api` |
11+
| `image.tag` | Container image tag (overridden by umbrella/CI) | `Chart.AppVersion` |
12+
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
13+
| `imagePullSecrets` | Secrets for pulling images from private repositories | `[]` |
14+
| `podLabels` | Additional labels for Pods | `{}` |
15+
| `annotations` | Additional annotations for Deployment | `{}` |
16+
| `service.type` | Kubernetes Service type | `ClusterIP` |
17+
| `service.port` | Kubernetes Service port | `8080` |
18+
| `service.portName` | Name for the service port | `http` |
19+
| `autoscaling.enabled` | Enable HorizontalPodAutoscaler | `false` |
20+
| `autoscaling.minReplicas` | Minimum HPA replicas | `1` |
21+
| `autoscaling.maxReplicas` | Maximum HPA replicas | `10` |
22+
| `autoscaling.targetCPUUtilizationPercentage` | Target CPU utilization for HPA | `80` |
23+
| `autoscaling.targetMemoryUtilizationPercentage` | Target Memory utilization for HPA (optional) | `nil` |
24+
| `resources` | Pod resource requests and limits | `{}` |
25+
| `livenessProbe` | Liveness probe configuration | `{httpGet: {path: /health, port: http}, initialDelaySeconds: 5, periodSeconds: 10}` |
26+
| `readinessProbe` | Readiness probe configuration | `{httpGet: {path: /health, port: http}, initialDelaySeconds: 5, periodSeconds: 10}` |
27+
| `serviceAccount.create` | Create a ServiceAccount | `true` |
28+
| `serviceAccount.automount` | Automount ServiceAccount token | `true` |
29+
| `serviceAccount.annotations` | Additional annotations for ServiceAccount | `{}` |
30+
| `serviceAccount.name` | Name of the ServiceAccount (defaults to fullname) | `""` |
31+
| `podSecurityContext` | Security context for the pod | `{}` |
32+
| `securityContext` | Security context for the container | `{}` |
33+
| `nodeSelector` | Node selector for pod assignment | `{}` |
34+
| `tolerations` | Tolerations for pod assignment | `[]` |
35+
| `affinity` | Affinity for pod assignment | `{}` |
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{{/* vim: set filetype=mustache: */}}
2+
{{/*
3+
Expand the name of the chart.
4+
*/}}
5+
{{- define "go-api.name" -}}
6+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
7+
{{- end }}
8+
9+
{{/*
10+
Create a default fully qualified app name.
11+
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12+
If release name contains chart name it will be used as a full name.
13+
*/}}
14+
{{- define "go-api.fullname" -}}
15+
{{- if .Values.fullnameOverride }}
16+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
17+
{{- else }}
18+
{{- $name := default .Chart.Name .Values.nameOverride }}
19+
{{- if contains $name .Release.Name }}
20+
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
21+
{{- else }}
22+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
23+
{{- end }}
24+
{{- end }}
25+
{{- end }}
26+
27+
{{/*
28+
Create chart name and version as used by the chart label.
29+
*/}}
30+
{{- define "go-api.chart" -}}
31+
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
32+
{{- end }}
33+
34+
{{/*
35+
Common labels
36+
*/}}
37+
{{- define "go-api.labels" -}}
38+
helm.sh/chart: {{ include "go-api.chart" . }}
39+
{{ include "go-api.selectorLabels" . }}
40+
{{- if .Chart.AppVersion }}
41+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
42+
{{- end }}
43+
app.kubernetes.io/managed-by: {{ .Release.Service }}
44+
{{- end }}
45+
46+
{{/*
47+
Selector labels used by Deployments, etc.
48+
*/}}
49+
{{- define "go-api.selectorLabels" -}}
50+
app.kubernetes.io/name: {{ include "go-api.name" . }}
51+
app.kubernetes.io/instance: {{ .Release.Name }}
52+
{{- end }}
53+
54+
{{/*
55+
Create the name of the service account to use.
56+
*/}}
57+
{{- define "go-api.serviceAccountName" -}}
58+
{{- if .Values.serviceAccount.create -}}
59+
{{- default (include "go-api.fullname" .) .Values.serviceAccount.name -}}
60+
{{- else -}}
61+
{{- default "default" .Values.serviceAccount.name -}}
62+
{{- end -}}
63+
{{- end }}

0 commit comments

Comments
 (0)