Publish and promote the application Helm chart
Publish image, chart, and GitOps promotion / Test and publish immutable artifacts (push) Failing after 14s
Publish image, chart, and GitOps promotion / Test and publish immutable artifacts (push) Failing after 14s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: Publish and promote container image
|
||||
name: Publish image, chart, and GitOps promotion
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,69 +9,57 @@ env:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build and push
|
||||
name: Test and publish immutable artifacts
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
- name: Check out source and tags
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve and validate chart version
|
||||
id: version
|
||||
run: |
|
||||
version=$(./scripts/chart-version.sh)
|
||||
echo "version=${version}" >> "${GITEA_OUTPUT}"
|
||||
helm lint chart
|
||||
helm template immich-sync chart --set-string image.tag="${GITEA_SHA}" >/dev/null
|
||||
|
||||
- name: Log in to Quay
|
||||
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.brunner.ninja
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_TOKEN }}
|
||||
|
||||
- name: Build image
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
- name: Build and push image
|
||||
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker build \
|
||||
--load \
|
||||
--label "org.opencontainers.image.revision=${COMMIT_SHA}" \
|
||||
docker build --load \
|
||||
--label "org.opencontainers.image.revision=${GITEA_SHA}" \
|
||||
--label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/immich-sync" \
|
||||
--tag "${IMAGE}:${COMMIT_SHA}" \
|
||||
.
|
||||
|
||||
- name: Push commit tag
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: docker push "${IMAGE}:${COMMIT_SHA}"
|
||||
|
||||
- name: Push latest tag
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
docker tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest"
|
||||
docker push "${IMAGE}:latest"
|
||||
|
||||
promote:
|
||||
name: Promote image in GitOps
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check that this is still the current main commit
|
||||
id: current-main
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
git fetch --no-tags origin main
|
||||
if [ "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}" ]; then
|
||||
echo "promote=true" >> "${GITEA_OUTPUT}"
|
||||
else
|
||||
echo "This build is no longer the current main commit; skipping promotion"
|
||||
echo "promote=false" >> "${GITEA_OUTPUT}"
|
||||
--tag "${IMAGE}:${GITEA_SHA}" .
|
||||
docker push "${IMAGE}:${GITEA_SHA}"
|
||||
if [ "${GITEA_REF}" = refs/heads/main ]; then
|
||||
docker tag "${IMAGE}:${GITEA_SHA}" "${IMAGE}:latest"
|
||||
docker push "${IMAGE}:latest"
|
||||
fi
|
||||
|
||||
- name: Package and publish Helm chart
|
||||
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')
|
||||
env:
|
||||
CHART_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
mkdir -p dist
|
||||
helm package chart --destination dist \
|
||||
--version "${CHART_VERSION}" --app-version "${GITEA_SHA}"
|
||||
curl --fail-with-body --user "${{ secrets.PACKAGE_USERNAME }}:${{ secrets.PACKAGE_TOKEN }}" \
|
||||
--upload-file "dist/immich-sync-${CHART_VERSION}.tgz" \
|
||||
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
|
||||
|
||||
- name: Check out GitOps repository
|
||||
if: steps.current-main.outputs.promote == 'true'
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ env.GITOPS_REPOSITORY }}
|
||||
@@ -79,19 +67,16 @@ jobs:
|
||||
token: ${{ secrets.GITOPS_TOKEN }}
|
||||
path: homelab-gitops
|
||||
|
||||
- name: Commit image promotion
|
||||
if: steps.current-main.outputs.promote == 'true'
|
||||
- name: Promote exact chart version
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
working-directory: homelab-gitops
|
||||
env:
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
CHART_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
./scripts/update-immich-sync-image.sh "${COMMIT_SHA}"
|
||||
git config user.name "immich-sync CI"
|
||||
git config user.email "immich-sync-ci@brunner.ninja"
|
||||
git add apps/immich-sync/kustomization.yaml
|
||||
if git diff --cached --quiet; then
|
||||
echo "GitOps already selects ${COMMIT_SHA}"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Deploy immich-sync ${COMMIT_SHA}"
|
||||
./scripts/update-chart-version.sh immich-sync "${CHART_VERSION}"
|
||||
git config user.name "feedc0de"
|
||||
git config user.email "feedc0de@users.noreply.code.brunner.ninja"
|
||||
git add bootstrap/immich-sync.yaml
|
||||
if git diff --cached --quiet; then exit 0; fi
|
||||
git commit -m "Deploy immich-sync chart ${CHART_VERSION}"
|
||||
git push origin HEAD:main
|
||||
|
||||
@@ -73,14 +73,23 @@ kubectl apply -f deploy/example.yaml
|
||||
kubectl port-forward service/immich-sync 8090:80
|
||||
```
|
||||
|
||||
The maintained Helm chart is published with the exact image revision in its
|
||||
`appVersion`:
|
||||
|
||||
```sh
|
||||
helm repo add brunner https://brunner.ninja/charts
|
||||
helm repo update brunner
|
||||
helm upgrade --install immich-sync brunner/immich-sync
|
||||
```
|
||||
|
||||
`deploy/example.yaml` is a portable starting point for self-hosting. It creates one Deployment and a ClusterIP Service,
|
||||
uses the public image, and does not require an image-pull secret. Add persistent storage, an Ingress, and monitoring to
|
||||
match your cluster. Because the service can make outbound requests to user-supplied URLs, operators should monitor it
|
||||
for SSRF and bandwidth abuse.
|
||||
|
||||
The author's complete
|
||||
[`apps/immich-sync` GitOps application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/immich-sync)
|
||||
is public and can be used as an Argo CD/Kustomize deployment reference. It includes homelab-specific CephFS storage,
|
||||
[`apps/immich-sync` GitOps values](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/immich-sync)
|
||||
are public and show one Argo CD installation of the chart. They include homelab-specific CephFS storage,
|
||||
multiple replicas, an Ingress hostname, and Prometheus Operator monitoring, so fork it and adjust those assumptions
|
||||
before deploying it to another cluster. WebSockets need no sticky-session setting: after the HTTP upgrade, the
|
||||
selected pod owns that connection until the short-lived job finishes or disconnects.
|
||||
@@ -107,8 +116,9 @@ Per-process operational counters and active gauges should be summed across repli
|
||||
## Continuous delivery
|
||||
|
||||
The Gitea Actions workflow in `.gitea/workflows/container.yml` builds and pushes
|
||||
`registry.brunner.ninja/feedc0de/immich-sync`. Every pushed commit receives an immutable tag containing its full Git
|
||||
SHA. A commit pushed to `main` receives both its SHA tag and `latest` from the same image build.
|
||||
`registry.brunner.ninja/feedc0de/immich-sync`. Every release receives an immutable image tag containing its full Git
|
||||
SHA. The same run packages the chart with that SHA as `appVersion`. Chart versions come from `git describe`: tag
|
||||
`v1.0.0` produces chart `1.0.0`; a later commit produces a unique version such as `1.0.0-1-g41627ab`.
|
||||
|
||||
The workflow deliberately loads the completed image into the Docker engine and pushes it with a separate plain
|
||||
`docker push`. It does not request provenance attestations, SBOM manifests, or OCI referrers, retaining compatibility
|
||||
@@ -118,15 +128,16 @@ The workflow expects these Gitea Actions repository secrets:
|
||||
|
||||
- `QUAY_USERNAME`: the complete Quay robot account name, including the `feedc0de+` prefix.
|
||||
- `QUAY_TOKEN`: the robot account token.
|
||||
- `PACKAGE_USERNAME` and `PACKAGE_TOKEN`: credentials that can publish to the public `feedc0de` Helm repository.
|
||||
- `GITOPS_TOKEN`: a `write:repository` token belonging to a dedicated account with write access only to
|
||||
`feedc0de/homelab-gitops`.
|
||||
|
||||
Give the robot account `Write` permission only on the `feedc0de/immich-sync` repository. Public image pulls do not use
|
||||
these credentials and do not require a Gitea secret.
|
||||
|
||||
After a successful `main` build, the promotion job updates the single image tag in `homelab-gitops` and pushes a
|
||||
deployment commit. Argo CD observes that commit and reconciles Kubernetes. The workflow has no kubeconfig and no
|
||||
direct Kubernetes access. See the GitOps repository README for the token setup, bootstrap procedure, and rollback path.
|
||||
After a successful `main` build, the promotion job updates the Argo Application's exact chart `targetRevision` in
|
||||
`homelab-gitops` and pushes a deployment commit. Argo CD observes that commit and reconciles Kubernetes. The workflow
|
||||
has no kubeconfig and no direct Kubernetes access.
|
||||
|
||||
## Current scope
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: immich-sync
|
||||
description: Compare and synchronize assets between two Immich public shares
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: latest
|
||||
@@ -0,0 +1,16 @@
|
||||
{{- define "immich-sync.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "immich-sync.fullname" -}}
|
||||
{{- default (include "immich-sync.name" .) .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "immich-sync.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "immich-sync.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "immich-sync.selectorLabels" -}}
|
||||
app: immich-sync
|
||||
{{- end }}
|
||||
@@ -0,0 +1,58 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
spec:
|
||||
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "immich-sync.selectorLabels" . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- if .Values.metrics.serviceMonitor.enabled }}
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
labels:
|
||||
{{- include "immich-sync.labels" . | nindent 4 }}
|
||||
{{- with .Values.metrics.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "immich-sync.selectorLabels" . | nindent 6 }}
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}-stats
|
||||
labels:
|
||||
{{- include "immich-sync.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
|
||||
{{- with .Values.persistence.storageClassName }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,98 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
labels:
|
||||
{{- include "immich-sync.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "immich-sync.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "immich-sync.selectorLabels" . | nindent 8 }}
|
||||
{{- include "immich-sync.labels" . | nindent 8 }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
fsGroup: 65532
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: immich-sync
|
||||
image: {{ printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) | quote }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8090"
|
||||
- name: STATS_FILE
|
||||
value: /data/stats.json
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8090
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 10 }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: stats
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
sizeLimit: 20Gi
|
||||
- name: stats
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ default (printf "%s-stats" (include "immich-sync.fullname" .)) .Values.persistence.existingClaim }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "immich-sync.fullname" . }}
|
||||
labels:
|
||||
{{- include "immich-sync.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
selector:
|
||||
{{- include "immich-sync.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,44 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: registry.brunner.ninja/feedc0de/immich-sync
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
imagePullSecrets: []
|
||||
|
||||
service:
|
||||
port: 80
|
||||
|
||||
persistence:
|
||||
enabled: false
|
||||
existingClaim: ""
|
||||
storageClassName: ""
|
||||
accessModes: [ReadWriteMany]
|
||||
size: 64Mi
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
host: immich-sync.example.com
|
||||
|
||||
metrics:
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
labels: {}
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
minAvailable: 1
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
ephemeral-storage: 64Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 128Mi
|
||||
ephemeral-storage: 20Gi
|
||||
|
||||
affinity: {}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
description=$(git describe --tags --long --match 'v[0-9]*' --dirty)
|
||||
case "$description" in
|
||||
*-dirty) echo "refusing to package a dirty tree" >&2; exit 1 ;;
|
||||
esac
|
||||
version=${description#v}
|
||||
case "$version" in
|
||||
*-0-g*) version=${version%%-0-g*} ;;
|
||||
esac
|
||||
printf '%s\n' "$version"
|
||||
Reference in New Issue
Block a user