chore(main): release 0.18.3 (#25) #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Tag-triggered publish flow for agents-api. | |
| # | |
| # push of a v*.*.* tag 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. | |
| name: Publish | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to publish (e.g. v0.3.0)' | |
| required: true | |
| type: string | |
| permissions: {} | |
| jobs: | |
| # -- 1. Build and push the container image -------------------------------- | |
| publish-image: | |
| uses: JorisJonkers-dev/github-workflows/.github/workflows/container-publish.yml@4b444f9a50edf76e4aea4148d73407c34a1e43a4 # v0.12.1 | |
| with: | |
| image-name: agents-api/agents-api | |
| version: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || 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.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} | |
| - name: resolve-image-digest | |
| id: digest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| IMAGE: ghcr.io/jorisjonkers-dev/agents-api/agents-api | |
| TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.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 '{"agents-api":"ghcr.io/jorisjonkers-dev/agents-api/agents-api@%s"}\n' "$IMAGE_DIGEST" \ | |
| > platform/images.lock.json | |
| echo "Locked image: ghcr.io/jorisjonkers-dev/agents-api/agents-api@${IMAGE_DIGEST}" | |
| - name: verify-lock | |
| run: | | |
| set -euo pipefail | |
| lock_ref=$(jq -r '.["agents-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@4b444f9a50edf76e4aea4148d73407c34a1e43a4 # v0.12.1 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} | |
| artifact-name: agents-api | |
| artifact-version: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || 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.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || 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: agents-api | |
| REPO_OWNER: ${{ github.repository_owner }} | |
| SOURCE_REPOSITORY: ${{ github.repository }} | |
| TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.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 |