diff --git a/.gitea/workflows/chart.yml b/.gitea/workflows/chart.yml index 44af96e..1e41530 100644 --- a/.gitea/workflows/chart.yml +++ b/.gitea/workflows/chart.yml @@ -19,8 +19,36 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Derive immutable chart version - run: echo "CHART_VERSION=$(./scripts/chart-version.sh)" >> "$GITHUB_ENV" + - name: Derive release versions + run: | + set -euo pipefail + echo "CHART_VERSION=$(./scripts/chart-version.sh)" >> "$GITHUB_ENV" + echo "APP_VERSION=$(sed -n 's/^appVersion: \"\(.*\)\"$/\1/p' chart/Chart.yaml)" >> "$GITHUB_ENV" + - name: Build, test and publish Loops image + env: + QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} + QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$QUAY_USERNAME" && test -n "$QUAY_TOKEN" + source_dir="$RUNNER_TEMP/loops-source" + git clone --depth 1 --branch "$APP_VERSION" https://github.com/joinloops/loops-server.git "$source_dir" + git -C "$source_dir" apply "$GITHUB_WORKSPACE/patches/loops-frontend.patch" + docker build --pull \ + --label "org.opencontainers.image.revision=$COMMIT_SHA" \ + --label "org.opencontainers.image.version=$APP_VERSION" \ + --label "org.opencontainers.image.source=https://github.com/joinloops/loops-server" \ + --tag "registry.brunner.ninja/feedc0de/loops:$APP_VERSION" \ + "$source_dir" + docker run --rm --entrypoint php "registry.brunner.ninja/feedc0de/loops:$APP_VERSION" -r ' + if (PHP_VERSION_ID < 80512) { fwrite(STDERR, "PHP security floor 8.5.12 not met\\n"); exit(1); } + echo PHP_VERSION, "\n"; + ' + docker run --rm --entrypoint ffmpeg "registry.brunner.ninja/feedc0de/loops:$APP_VERSION" -version | head -n 1 | grep -F 'ffmpeg version 9.0.2' + docker run --rm --entrypoint sh "registry.brunner.ninja/feedc0de/loops:$APP_VERSION" -ec 'grep -R -l "recommended-feed" /var/www/html/public/build/assets >/dev/null; grep -R -l "fyfeed" /var/www/html/public/build/assets >/dev/null' + printf '%s' "$QUAY_TOKEN" | docker login registry.brunner.ninja --username "$QUAY_USERNAME" --password-stdin + docker push "registry.brunner.ninja/feedc0de/loops:$APP_VERSION" - name: Package and upload env: PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} diff --git a/README.md b/README.md index 9a4b891..2979fbb 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,8 @@ helm repo update brunner.ninja helm upgrade --install loops brunner.ninja/loops --namespace loops --create-namespace --wait --timeout 20m ``` -The deployed image is built from Loops source commit f614b34f. The chart's frontend, backend and Following-feed overlays are tied to that exact image; review and rebase them before changing `appVersion` or `image.tag`. The upstream project does not issue SemVer releases for this commit, so Git tag `f614b34f` maps reversibly to chart version `0.0.0-f614b34f`; chart-only commits append their Git-describe count and hash. +The chart and image are released together. The Git tag and image tag match the upstream Loops release (for example `v1.0.0-beta.15`), `Chart.yaml.appVersion` is that exact tag, and chart versions come from `git describe --tags --long`. A chart-only commit after a release tag therefore gets a unique version such as `1.0.0-beta.15-1-g`. + +CI checks out the matching upstream release, applies `patches/loops-frontend.patch`, builds the upstream Dockerfile, verifies PHP 8.5.12+ and FFmpeg 9.0.2, publishes the image to `registry.brunner.ninja`, publishes the chart to the public `brunner.ninja/charts` repository, and promotes the chart version to `homelab-gitops`. The frontend fix is now built into the immutable image; it is not a hash-specific ConfigMap overlay. The backend and Following-feed patches remain chart-managed and must be reviewed against the next upstream release before updating `appVersion`. + +Argo CD runs `php artisan migrate --force` as a sync hook after the database and configuration resources are ready, and before the application Deployments roll out. Application pods keep automatic migrations disabled to avoid concurrent migration runs. diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 786eef7..e894e7e 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: loops description: Kubernetes deployment contract for loops type: application -version: 0.0.0-f614b34f -appVersion: "f614b34f" +version: 1.0.0-beta.15 +appVersion: "v1.0.0-beta.15" diff --git a/chart/templates/application.yaml b/chart/templates/application.yaml index 3feb923..58424ed 100644 --- a/chart/templates/application.yaml +++ b/chart/templates/application.yaml @@ -109,6 +109,8 @@ kind: Deployment metadata: name: loops-web namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "2" labels: app.kubernetes.io/name: loops-web app.kubernetes.io/part-of: loops @@ -203,10 +205,6 @@ spec: - name: filesystem-config mountPath: /var/www/html/config/filesystems.php subPath: filesystems.php - - name: frontend-hotfix - mountPath: /var/www/html/public/build/assets/index-Cng6wq5r.js - subPath: index-Cng6wq5r.js - readOnly: true - name: backend-hotfix mountPath: /var/www/html/app/Services/ForYouFeedService.php subPath: ForYouFeedService.php @@ -238,9 +236,6 @@ spec: - name: filesystem-config configMap: name: loops-filesystem-config - - name: frontend-hotfix - configMap: - name: loops-frontend-hotfix - name: backend-hotfix configMap: name: loops-backend-hotfix @@ -270,6 +265,8 @@ kind: Deployment metadata: name: loops-horizon namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "2" labels: app.kubernetes.io/name: loops-horizon app.kubernetes.io/part-of: loops @@ -343,6 +340,8 @@ kind: Deployment metadata: name: loops-transcoder namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "2" labels: app.kubernetes.io/name: loops-transcoder app.kubernetes.io/component: transcoder @@ -466,6 +465,8 @@ kind: Deployment metadata: name: loops-scheduler namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "2" labels: app.kubernetes.io/name: loops-scheduler app.kubernetes.io/part-of: loops diff --git a/chart/templates/frontend-hotfix.yaml b/chart/templates/frontend-hotfix.yaml deleted file mode 100644 index 02ce48a..0000000 --- a/chart/templates/frontend-hotfix.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -data: - index-Cng6wq5r.js: | - import{_ as h}from"./HideCommentConfirmModal-QPWnMLxt.js";import{u as k,b as y,a as g,S as v}from"./LoopLink.vue_vue_type_script_setup_true_lang-D1dGRmzo.js";import{u as x}from"./usePublicFeed-BqqecCdM.js";import{c as P}from"./app--YWhAMOJ.js";import{V}from"./VideoPlayer-DP4y4yM4.js";import{y as b,d as F,w as I,a as S,c as C,e as r,u as w,q,ay as B}from"./vendor-core-C7C5leYv.js";import"./Comments-ByHWOGrH.js";import"./KlipyPicker-CbkaMsji.js";import"./comments-CRjqfgs4.js";import"./useHashids-DT1SW4G7.js";import"./AutolinkedText-BAfi_gt-.js";import"./CommentMediaAttachment-DfTLtvaG.js";import"./__vite-browser-external-ke9ZaMpT.js";/* empty css *//* empty css */import"./ShareModal-Bb3hVgNV.js";import"./DmQuickShare-C1LTf34j.js";import"./DmShareModal-DB3TK55A.js";const H=()=>k({queryKey:["fyfeed"],queryFn:({pageParam:a})=>y({cursor:a}),getNextPageParam:a=>a.meta?.next_cursor??void 0,initialPageParam:null,enabled:!0,staleTime:0,retry:2}),X={__name:"index",setup(a){const s=S("authStore"),n=H(),j=x(),{hasInteracted:o,handleFirstInteraction:m,globalMuted:c}=g(),u=(e,t)=>({"video-id":e.id,"video-url":e.media.src_url,"share-url":e.url,"profile-id":e.account.id,username:e.account.username,"profile-image":e.account.avatar,caption:e.caption,hashtags:e.tags,mentions:e.mentions,likes:e.likes,hasLiked:e.has_liked,hasBookmarked:e.has_bookmarked,bookmarks:e.bookmarks,shares:e.shares,comments:[],canComment:e.permissions?.can_comment,"comment-count":e.comments,index:t,isSensitive:e?.is_sensitive,altText:e?.media.alt_text,autoPlay:o.value,muted:c.value}),l=e=>e.id,_=e=>{},f=e=>{},p=()=>{m()};return(e,t)=>{const i=h;return C(),F(P,null,{default:I(()=>[r(v,{key:s.authenticated?"recommended-feed":"public-feed","feed-data":s.authenticated?n:j,"item-component":V,"get-item-props":u,"get-item-key":l,"auto-play":w(o),"scroll-threshold":1.5,"snap-sensitivity":50,onItemVisible:_,onItemHidden:f,onInteraction:p},null,8,["feed-data","auto-play"]),r(i)]),_:1})}}};export{X as default}; -kind: ConfigMap -metadata: - labels: - app.kubernetes.io/part-of: loops - name: loops-frontend-hotfix - namespace: {{ .Release.Namespace }} diff --git a/chart/templates/infrastructure.yaml b/chart/templates/infrastructure.yaml index 25aa188..0492936 100644 --- a/chart/templates/infrastructure.yaml +++ b/chart/templates/infrastructure.yaml @@ -3,6 +3,8 @@ kind: Service metadata: name: mysql namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-1" labels: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: loops @@ -21,6 +23,8 @@ kind: StatefulSet metadata: name: mysql namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-1" labels: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: loops @@ -111,6 +115,8 @@ kind: Service metadata: name: valkey namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-1" labels: app.kubernetes.io/name: valkey app.kubernetes.io/part-of: loops @@ -129,6 +135,8 @@ kind: StatefulSet metadata: name: valkey namespace: {{ .Release.Namespace }} + annotations: + argocd.argoproj.io/sync-wave: "-1" labels: app.kubernetes.io/name: valkey app.kubernetes.io/part-of: loops diff --git a/chart/templates/migrations.yaml b/chart/templates/migrations.yaml new file mode 100644 index 0000000..91185b1 --- /dev/null +++ b/chart/templates/migrations.yaml @@ -0,0 +1,36 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: loops-migrate + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: loops-migrate + app.kubernetes.io/part-of: loops + annotations: + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/sync-wave: "1" + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded +spec: + backoffLimit: 2 + activeDeadlineSeconds: 900 + template: + metadata: + labels: + app.kubernetes.io/name: loops-migrate + app.kubernetes.io/part-of: loops + spec: + restartPolicy: Never + containers: + - name: migrate + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: IfNotPresent + command: + - php + - artisan + - migrate + - --force + envFrom: + - configMapRef: + name: loops-env + - secretRef: + name: {{ .Values.runtimeSecret }} diff --git a/patches/loops-frontend.patch b/patches/loops-frontend.patch new file mode 100644 index 0000000..62a68e8 --- /dev/null +++ b/patches/loops-frontend.patch @@ -0,0 +1,62 @@ +diff --git a/resources/js/composables/useFeed.js b/resources/js/composables/useFeed.js +--- a/resources/js/composables/useFeed.js ++++ b/resources/js/composables/useFeed.js +@@ -4,9 +4,9 @@ + export const useFeed = () => { + return useInfiniteQuery({ +- queryKey: ['feed'], ++ queryKey: ['fyfeed'], + queryFn: ({ pageParam }) => fetchFeedPage({ cursor: pageParam }), + getNextPageParam: (lastPage) => lastPage.meta?.next_cursor ?? undefined, + initialPageParam: null, + enabled: true, +- staleTime: 1000 * 60 * 5, // 5 minutes ++ staleTime: 0, + retry: 2 + }) + } +diff --git a/resources/js/pages/index.vue b/resources/js/pages/index.vue +--- a/resources/js/pages/index.vue ++++ b/resources/js/pages/index.vue +@@ -1,7 +1,7 @@ +