Skip to content
Merged
136 changes: 41 additions & 95 deletions .github/workflows/aws_dev_release_gitops.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,37 @@ on:
push:
branches:
- dev
# Image Updater's write-back lands on this same branch. paths-ignore is
# what stops build -> push -> digest write-back -> build looping: the
# write-back only ever touches argo/ (its writeBackTarget is pinned to
# kustomization:/argo/overlays/dev), so it can never match this trigger.
# A push touching both argo/ and app code still builds, which is correct.
#
# This replaces the old [skip ci] guard. [skip ci] worked, but GitHub
# applies it to pull_request events too, keyed on the HEAD commit of the
# PR - so once a write-back commit was the tip of dev, every dev -> main
# release PR was silently skipped and Auto Release never fired. Guarding
# on paths instead of on a magic string keeps that blast radius out of PRs.
paths-ignore:
- 'argo/**'
workflow_dispatch:

# GitOps deploy, git as the source of truth. Build pushes to the shared dev ECR
# repo under the mutable tag sdk-example:<repo> (kept mutable so the ECR
# lifecycle policy manages it; the commit sha is baked into the image via
# COMMIT_HASH, not the tag). The sdk-apps ApplicationSet (in
# metaversecloud-com/sdk-gitops) creates one Application per env from this repo's
# argo/envs/<env>/config.json, named "<app>-<env>", tracking the deploy branch
# (automated + selfHeal). The deploy job does NOT touch the cluster: it pins the
# env overlay's images[].digest to the digest just built (immutable, changes
# every build even though the tag is reused) and commits it to the deploy branch
# - that manifest change is what rolls the workload - then triggers `argocd app
# sync` via the Argo CD API with a token. No kubectl, no EKS/AWS auth, no
# config/secret seeding (kustomize ConfigMap + SealedSecret are Argo-managed).
# Adding an env = committing argo/envs/<env>/config.json on the inventory ref.
# GitOps deploy, git as the source of truth. This workflow only BUILDS: it
# pushes to the shared dev ECR repo under the mutable tag sdk-example:<repo>
# (kept mutable so the ECR lifecycle policy manages it; the commit sha is baked
# into the image via COMMIT_HASH, not the tag). Nothing here deploys.
#
# Argo CD Image Updater owns the deploy half. It watches that tag, and when the
# digest behind it changes it commits images[].digest into this repo's
# argo/overlays/<env>/kustomization.yaml on the deploy branch. That manifest
# change is what rolls the workload - the sdk-apps ApplicationSet's Applications
# are automated + selfHeal, so no explicit `argocd app sync` is needed. Config
# lives in metaversecloud-com/sdk-gitops (bootstrap/imageupdater-sdk-apps.yaml).
#
# The sdk-apps ApplicationSet creates one Application per env from this repo's
# argo/envs/<env>/config.json, named "<app>-<env>". Adding an env = committing
# argo/envs/<env>/config.json on the inventory ref. No kubectl, no EKS/AWS auth,
# no config/secret seeding (kustomize ConfigMap + SealedSecret are Argo-managed).
env:
REPOSITORY: 'sdk-example'
ENV: 'dev'
Expand All @@ -29,13 +45,11 @@ concurrency:

permissions:
id-token: write # Build: OIDC -> AWS role for ECR push
contents: write # deploy: commit the image-tag bump to the deploy branch
contents: read # checkout only; the deploy-branch commit is Image Updater's

jobs:
Build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -56,6 +70,13 @@ jobs:
echo "::error::Build failed"
exit 1
fi
# Emulation for the arm64 target. The images differ only by their
# node/alpine base (npm/node binaries); node_modules is ADDed from
# the runner and is arch-neutral (these apps have no native runtime
# deps), so a single buildx multi-arch push is correct. NOTE: if any
# app adds a native RUNTIME dependency, this pattern would ship the
# runner's arch into both variants - switch that app to a per-arch
# native build or a Dockerfile `RUN npm ci`.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
Expand Down Expand Up @@ -88,6 +109,9 @@ jobs:
images: ${{ steps.login-ecr.outputs.registry }}/${{ env.REPOSITORY }}
tags: |
type=raw,value=${{ github.event.repository.name }}
- name: Compute image build timestamp
id: buildtime
run: echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Build and push multi-platform images to ECR
id: build
uses: docker/build-push-action@v5
Expand All @@ -99,86 +123,8 @@ jobs:
build-args: |
REF=${{ github.ref }}
COMMIT_HASH=${{ github.sha }}
BUILD_TIME=${{ steps.buildtime.outputs.value }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
sbom: false

deploy:
# In-VPC self-hosted runner (reaches the internal Argo CD API). This job
# never talks to the cluster: it commits the new image tag to the deploy
# branch (git = source of truth) and asks Argo CD to sync. Auth is an Argo CD
# API token (secret ARGOCD_AUTH_TOKEN) whose account needs applications
# get+sync on sdk-apps/* in argocd-rbac-cm - widen that policy to cover all
# apps. No EKS access entry / k8s RBAC / AWS creds needed here.
runs-on: topia-dev-sdk-apps-metaversecloud-com
needs: Build
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install argocd + yq
run: |
set -euo pipefail
# Arch-aware; gate on executing the binary so a stale wrong-arch binary
# from a prior run is replaced (avoids "Exec format error").
case "$(uname -m)" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
*) echo "::error::Unsupported arch $(uname -m)"; exit 1 ;;
esac
if ! yq --version &>/dev/null; then
curl -sSLo /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$ARCH
sudo install -m0755 /tmp/yq /usr/local/bin/yq
fi
# Pinned to the Argo CD server version (argo.topia.io reports v3.4.2);
# bump this when the server is upgraded.
ARGOCD_VERSION=v3.4.2
if [ "$(argocd version --client --short 2>/dev/null | grep -o 'v[0-9.]*' | head -1)" != "$ARGOCD_VERSION" ]; then
curl -sSLo /tmp/argocd https://github.com/argoproj/argo-cd/releases/download/$ARGOCD_VERSION/argocd-linux-$ARCH
sudo install -m0755 /tmp/argocd /usr/local/bin/argocd
fi
- name: Pin image digest in the env overlay and commit
env:
DIGEST: ${{ needs.Build.outputs.digest }}
run: |
set -euo pipefail
[ -n "$DIGEST" ] || { echo "::error::empty image digest from Build"; exit 1; }
k="argo/overlays/$ENV/kustomization.yaml"
# Pin every image that tracks the shared dev ECR repo (newName .../sdk-example)
# to the digest just built - immutable and unique per build even though the
# tag is reused - and drop any mutable newTag. Images pinned elsewhere are
# left untouched. This manifest change is what makes Argo roll the workload.
DIGEST="$DIGEST" yq -i \
'(.images[] | select(.newName | test("/sdk-example$")) | .digest) = strenv(DIGEST)' "$k"
yq -i 'del(.images[] | select(.newName | test("/sdk-example$")) | .newTag)' "$k"
git config user.email devops@topia.io
git config user.name Devops
if git diff --quiet -- "$k"; then
echo "No image-digest change in $k"
else
git add "$k"
# [skip ci] + the GITHUB_TOKEN push both keep this commit from
# retriggering the workflow.
git commit -m "chore(deploy): ${GITHUB_REPOSITORY##*/} image $DIGEST [skip ci]"
git pull --rebase origin "$GITHUB_REF_NAME"
git push origin HEAD:"$GITHUB_REF_NAME"
fi
- name: Trigger Argo CD sync
env:
ARGOCD_SERVER: ${{ vars.ARGOCD_SERVER }}
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
run: |
set -euo pipefail
app="${GITHUB_REPOSITORY##*/}-$ENV"
export ARGOCD_SERVER ARGOCD_AUTH_TOKEN
# argocd-server runs with server.insecure=true (plaintext HTTP/2; TLS
# is terminated upstream, not on the pod), so the CLI must use
# --plaintext (no TLS) rather than --insecure (which still does a TLS
# handshake and gets reset by the plaintext port). --grpc-web over
# cleartext. Refresh so Argo sees the commit just pushed, then sync and
# wait for health.
argocd app get "$app" --refresh --grpc-web --plaintext >/dev/null
argocd app sync "$app" --grpc-web --plaintext --timeout 300
argocd app wait "$app" --health --grpc-web --plaintext --timeout 300
4 changes: 4 additions & 0 deletions .github/workflows/aws_prod_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ jobs:
type=raw,value=${{ github.event.repository.name }}-${{ github.event.release.tag_name }}
type=raw,value=${{ github.event.repository.name }}-latest

- name: Compute image build timestamp
id: buildtime
run: echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Build and push multi-platform images to ECR
uses: docker/build-push-action@v5
with:
Expand All @@ -112,6 +115,7 @@ jobs:
build-args: |
REF=${{ github.ref }}
COMMIT_HASH=${{ github.sha }}
BUILD_TIME=${{ steps.buildtime.outputs.value }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
FROM node:22-alpine
WORKDIR /app
ARG COMMIT_HASH
ENV COMMIT_HASH=$COMMIT_HASH
ARG BUILD_TIME
ENV BUILD_TIME=$BUILD_TIME
ADD . ./
EXPOSE 3000
ENTRYPOINT [ "npm", "start" ]
3 changes: 2 additions & 1 deletion argo/overlays/dev/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ patches:
images:
- name: jukebox0
newName: 368076259134.dkr.ecr.us-east-1.amazonaws.com/sdk-example
digest: sha256:db6f5e0364e86c14731d3937e37031dff7f23391fed5fe34eab10450ef97976e
digest: sha256:9250689f0d0f8417f30bf80bfdecee35841165160a79edccf94fea5ac993d5df
newTag: jukebox
2 changes: 2 additions & 0 deletions server/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ router.get("/system/health", (req, res) => {
status: "OK",
serverStartDate: SERVER_START_DATE,
envs: {
COMMIT_HASH: process.env.COMMIT_HASH ?? "NOT SET",
BUILD_TIME: process.env.BUILD_TIME ?? "NOT SET",
API_KEY: process.env.API_KEY ? "SET" : "NOT SET",
PORT: process.env.PORT ? process.env.PORT : "NOT SET",
NODE_ENV: process.env.NODE_ENV ? process.env.NODE_ENV : "NOT SET",
Expand Down
Loading