Skip to content
Merged
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
27 changes: 27 additions & 0 deletions .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Deploy Preview for deploy-platform service repos.
#
# Any PR touching platform/ gets a full render + validation and a sticky
# "Deploy Preview" comment with the SC-11 readiness scorecard.
name: Deploy Preview

on:
pull_request:
paths:
- 'platform/**'

permissions: {}

jobs:
deploy-preview:
uses: JorisJonkers-dev/github-workflows/.github/workflows/deploy-validate.yml@f6c2969d7f1f4555da3b2cf46ce6a9b364c471b3 # v0.12.0
with:
deploy-dir: platform
schema-version: 0.16.0
image-lock-path: platform/images.lock.json
context-ref: ghcr.io/jorisjonkers-dev/cluster-deploy-context-public@sha256:64d00fe03a271dbd03a48005d0a0cc6cc5fe43df23c0e97b649c2f8b3e78b418
environments: production
comment: true
permissions:
contents: read
packages: read
pull-requests: write
236 changes: 236 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Tag-triggered publish flow for deploy-platform service repos.
#
# push of a v*.*.* tag (authored by release.yml via the App token) drives:
# publish-image -> resolve-image-lock -> publish-deploy-artifact -> register-service
#
# Boundary rule (R1-2): the image lock crosses the reusable-workflow boundary
# as an UPLOADED ARTIFACT (image-lock-artifact), never as a runner-local path.
# deploy-artifact.yml downloads the artifact itself; there is no intermediate
# download job and no image-lock-path input on the deploy-artifact call.
name: Publish

on:
push:
tags:
- 'v*.*.*'

permissions: {}

jobs:
# -- 1. Build and push the container image --------------------------------
publish-image:
uses: JorisJonkers-dev/github-workflows/.github/workflows/container-publish.yml@f6c2969d7f1f4555da3b2cf46ce6a9b364c471b3 # v0.12.0
with:
image-name: auth-api/auth-api
version: ${{ github.ref_name }}
permissions:
contents: read
packages: write

# -- 2. Resolve the image lock (upload artifact, never path) --------------
resolve-image-lock:
needs: [publish-image]
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
images-lock-artifact: images-lock-${{ github.run_id }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.ref }}

- name: resolve-image-digest
id: digest
env:
GH_TOKEN: ${{ github.token }}
IMAGE: ghcr.io/jorisjonkers-dev/auth-api/auth-api
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
printf '%s' "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
digest="$(docker buildx imagetools inspect "${IMAGE}:${TAG}" --format '{{json .Manifest}}' | jq -r '.digest')"
if [ -z "$digest" ] || [ "$digest" = "null" ]; then
echo "E_FLOATING_IMAGE: could not resolve a digest for ${IMAGE}:${TAG}" >&2
exit 1
fi
echo "image-digest=$digest" >> "$GITHUB_OUTPUT"

- name: lock-image
env:
IMAGE_DIGEST: ${{ steps.digest.outputs.image-digest }}
run: |
set -euo pipefail
if [ -z "$IMAGE_DIGEST" ] || [ "$IMAGE_DIGEST" = "null" ]; then
echo "E_FLOATING_IMAGE: IMAGE_DIGEST is empty" >&2
exit 1
fi
case "$IMAGE_DIGEST" in
sha256:*) ;;
*) echo "E_FLOATING_IMAGE: IMAGE_DIGEST is not a digest ref (got: $IMAGE_DIGEST)" >&2; exit 1 ;;
esac
printf '{"auth-api":"ghcr.io/jorisjonkers-dev/auth-api/auth-api@%s"}\n' "$IMAGE_DIGEST" \
> platform/images.lock.json
echo "Locked image: ghcr.io/jorisjonkers-dev/auth-api/auth-api@${IMAGE_DIGEST}"

- name: verify-lock
run: |
set -euo pipefail
lock_ref=$(jq -r '.["auth-api"]' platform/images.lock.json)
case "$lock_ref" in
*@sha256:*) echo "Digest pin verified: $lock_ref" ;;
*) echo "E_FLOATING_IMAGE: lock ref is not digest-pinned: $lock_ref" >&2; exit 1 ;;
esac

- name: upload-image-lock
uses: actions/upload-artifact@v5
with:
name: images-lock-${{ github.run_id }}
path: platform/images.lock.json
retention-days: 1

# -- 3. Render and publish the deploy artifact ----------------------------
publish-deploy-artifact:
needs: [resolve-image-lock]
uses: JorisJonkers-dev/github-workflows/.github/workflows/deploy-artifact.yml@f6c2969d7f1f4555da3b2cf46ce6a9b364c471b3 # v0.12.0
with:
ref: ${{ github.ref }}
artifact-name: auth-api
artifact-version: ${{ github.ref_name }}
schema-version: 0.16.0
context-ref: ghcr.io/jorisjonkers-dev/cluster-deploy-context-public@sha256:64d00fe03a271dbd03a48005d0a0cc6cc5fe43df23c0e97b649c2f8b3e78b418
deploy-dir: platform
image-lock-artifact: ${{ needs.resolve-image-lock.outputs.images-lock-artifact }}
environments: production
permissions:
contents: read
packages: write
id-token: write
attestations: write

# -- 4. Open/refresh the homelab-deploy registry PR ------------------------
register-service:
needs: [publish-deploy-artifact]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: mint-app-token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: homelab-deploy

- name: assert-token-minted
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
if [ -z "$APP_TOKEN" ]; then
echo "E_APP_TOKEN_MISSING: App token was not minted for register-service." >&2
exit 1
fi

- uses: actions/checkout@v7
with:
ref: ${{ github.ref }}

- name: build-registry-payload
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
ARTIFACT_REF: ${{ needs.publish-deploy-artifact.outputs.artifact-ref }}
ARTIFACT_DIGEST: ${{ needs.publish-deploy-artifact.outputs.artifact-digest }}
SERVICE_NAME: auth-api
REPO_OWNER: ${{ github.repository_owner }}
SOURCE_REPOSITORY: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
branch="registry-update/${SERVICE_NAME}/${TAG}"
registry_path="registry/${SERVICE_NAME}.yaml"
repo="${REPO_OWNER}/homelab-deploy"

if ! gh api "repos/${repo}/git/refs/heads/${branch}" >/dev/null 2>&1; then
main_sha="$(gh api "repos/${repo}/git/refs/heads/main" --jq '.object.sha')"
gh api --method POST "repos/${repo}/git/refs" \
-f ref="refs/heads/${branch}" -f sha="$main_sha" >/dev/null
fi

current_sha=""
if gh api "repos/${repo}/contents/${registry_path}?ref=${branch}" > current.json 2>/dev/null; then
current_sha="$(jq -r '.sha' current.json)"
jq -r '.content' current.json | base64 -d > current.yaml
fi

python3 - "$SERVICE_NAME" <<'PY'
import os, sys, yaml

service = sys.argv[1]
artifact = {
"ref": os.environ["ARTIFACT_REF"],
"digest": os.environ["ARTIFACT_DIGEST"],
"contractPath": "artifact-contract.yaml",
}

if os.path.exists("current.yaml"):
doc = yaml.safe_load(open("current.yaml"))
doc["spec"]["artifact"] = artifact
else:
dep = yaml.safe_load(open("platform/deployment.yml"))
spec = dep.get("spec", {})
platform = spec.get("platform") or {}
layer = platform.get("layer")
if not layer:
sys.exit("E_UNKNOWN_LAYER: deployment.yml spec.platform.layer is "
"required for first registration")
workloads = spec.get("workloads") or []
health = (workloads[0].get("health") if workloads else None) or {}
doc = {
"apiVersion": "deployment.jorisjonkers.dev/deploy-unit-registration/v1",
"kind": "DeployUnitRegistration",
"metadata": {
"name": service,
"owner": os.environ["REPO_OWNER"],
},
"spec": {
"artifact": artifact,
"namespace": spec["namespace"],
"layer": layer,
"sourceRepository": os.environ["SOURCE_REPOSITORY"],
"environments": spec.get("environments") or ["production"],
"healthClass": health.get("class")
or health.get("timeoutClass")
or "stateless",
"prune": {"default": True},
"allowedClusterScope": [],
},
}

yaml.safe_dump(doc, open("registration.yaml", "w"),
sort_keys=False, default_flow_style=False)
PY

if [ -n "$current_sha" ]; then
gh api --method PUT "repos/${repo}/contents/${registry_path}" \
-f message="chore(registry): update ${SERVICE_NAME} to ${TAG}" \
-f content="$(base64 -w0 registration.yaml)" \
-f sha="$current_sha" -f branch="$branch" >/dev/null
else
gh api --method PUT "repos/${repo}/contents/${registry_path}" \
-f message="chore(registry): register ${SERVICE_NAME}" \
-f content="$(base64 -w0 registration.yaml)" \
-f branch="$branch" >/dev/null
fi

open_prs="$(gh api "repos/${repo}/pulls?head=${REPO_OWNER}:${branch}&state=open" --jq 'length')"
if [ "$open_prs" -eq 0 ]; then
gh api --method POST "repos/${repo}/pulls" \
-f title="chore(registry): update ${SERVICE_NAME} to ${TAG}" \
-f body="Automated registry update from publish.yml. Artifact: ${ARTIFACT_REF}" \
-f head="$branch" -f base="main" >/dev/null
else
echo "Updated existing registry PR for branch ${branch}"
fi
Loading
Loading