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
7 changes: 7 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
self-hosted-runner:
labels:
- shared-arm64-activeviam
- activeviam-shared-arm64-small

config-variables:
- SYMPHONY_DEPLOY_APP_ID
4 changes: 4 additions & 0 deletions .github/workflows/make-all.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
name: make-all

on:
workflow_dispatch:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
make-all:
runs-on: ubuntu-latest
Expand Down
334 changes: 334 additions & 0 deletions .github/workflows/publish-shared.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
name: publish-shared

on:
workflow_run:
workflows:
- make-all
types:
- completed
workflow_dispatch:
inputs:
source_sha:
description: Full commit SHA from main to rebuild and redeploy; defaults to current main
required: false
type: string

permissions:
contents: read

concurrency:
group: publish-symphony-shared
cancel-in-progress: false

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: symphony
DEPLOYMENT_REPOSITORY: activeviam/shared-infrastructure
DEPLOYMENT_MANIFEST: services/symphony/envs/shared/kustomization.yaml

jobs:
select-source:
if: >-
${{
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.head_repository.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/main')
}}
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
source_sha: ${{ steps.source.outputs.source_sha }}

steps:
- name: Checkout main history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: main
fetch-depth: 0

- name: Select a tested main commit
id: source
env:
EVENT_NAME: ${{ github.event_name }}
MANUAL_SHA: ${{ inputs.source_sha }}
WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
git fetch --no-tags origin main

if [[ "$EVENT_NAME" == "workflow_run" ]]; then
candidate="$WORKFLOW_SHA"
elif [[ -n "$MANUAL_SHA" ]]; then
candidate="$MANUAL_SHA"
else
candidate="origin/main"
fi

candidate="$(git rev-parse "${candidate}^{commit}")"
if ! git merge-base --is-ancestor "$candidate" origin/main; then
echo "Refusing to publish a commit that is not in origin/main: $candidate" >&2
exit 1
fi

echo "source_sha=$candidate" >> "$GITHUB_OUTPUT"
echo "Selected source commit $candidate"

verify-manual:
if: ${{ github.event_name == 'workflow_dispatch' }}
needs:
- select-source
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: elixir

steps:
- name: Checkout selected source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.select-source.outputs.source_sha }}

- name: Set up mise tools
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3
with:
install: true
cache: true
working_directory: elixir

- name: Cache deps and build
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
elixir/deps
elixir/_build
key: ${{ runner.os }}-mix-${{ hashFiles('elixir/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-

- name: Re-run the full source gate
run: make all

build-image:
if: >-
${{
always() &&
needs.select-source.result == 'success' &&
(needs.verify-manual.result == 'success' ||
needs.verify-manual.result == 'skipped')
}}
needs:
- select-source
- verify-manual
runs-on: shared-arm64-activeviam
timeout-minutes: 45
outputs:
digest: ${{ steps.image.outputs.digest }}
image_tag: ${{ steps.image.outputs.image_tag }}
registry: ${{ steps.login-ecr.outputs.registry }}

steps:
- name: Checkout selected source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.select-source.outputs.source_sha }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6
with:
aws-region: ${{ env.AWS_REGION }}
allowed-account-ids: "333996703325"
use-existing-credentials: true

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2

- name: Build or reuse immutable image
id: image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
SOURCE_SHA: ${{ needs.select-source.outputs.source_sha }}
run: |
set -euo pipefail
image_tag="git-${SOURCE_SHA}-arm64"
image="${REGISTRY}/${ECR_REPOSITORY}:${image_tag}"
describe_image=(
aws ecr describe-images
--region "$AWS_REGION"
--repository-name "$ECR_REPOSITORY"
--image-ids "imageTag=${image_tag}"
--query 'imageDetails[0].imageDigest'
--output text
)
digest="$("${describe_image[@]}" 2>/dev/null || true)"

if [[ "$digest" != sha256:* ]]; then
docker build --platform linux/arm64 --tag "$image" elixir
docker push "$image"

for _ in {1..12}; do
digest="$("${describe_image[@]}" 2>/dev/null || true)"
[[ "$digest" == sha256:* ]] && break
sleep 5
done
else
echo "Reusing existing immutable image $image"
fi

if [[ "$digest" != sha256:* ]]; then
echo "Unable to resolve the ECR digest for $image" >&2
exit 1
fi

echo "image_tag=$image_tag" >> "$GITHUB_OUTPUT"
echo "digest=$digest" >> "$GITHUB_OUTPUT"
echo "Published ${REGISTRY}/${ECR_REPOSITORY}@${digest}"

open-deployment-pr:
needs:
- select-source
- build-image
runs-on: activeviam-shared-arm64-small
timeout-minutes: 15

steps:
- name: Mint deployment App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.SYMPHONY_DEPLOY_APP_ID }}
private-key: ${{ secrets.SYMPHONY_DEPLOY_APP_PRIVATE_KEY }}
owner: activeviam
repositories: shared-infrastructure

- name: Checkout deployment repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: ${{ env.DEPLOYMENT_REPOSITORY }}
ref: main
token: ${{ steps.app-token.outputs.token }}
path: deployment

- name: Ensure GitHub CLI is available
run: |
set -euo pipefail
if command -v gh >/dev/null 2>&1; then
gh --version
exit 0
fi

version="2.96.0"
archive="gh_${version}_linux_arm64.tar.gz"
archive_path="${RUNNER_TEMP}/${archive}"
curl --fail --silent --show-error --location \
"https://github.com/cli/cli/releases/download/v${version}/${archive}" \
--output "$archive_path"
echo "06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909 $archive_path" | sha256sum --check -
tar --extract --gzip --file "$archive_path" --directory "$RUNNER_TEMP"
echo "${RUNNER_TEMP}/gh_${version}_linux_arm64/bin" >> "$GITHUB_PATH"

- name: Update the pinned image digest
id: manifest
working-directory: deployment
env:
DIGEST: ${{ needs.build-image.outputs.digest }}
run: |
set -euo pipefail
manifest="$DEPLOYMENT_MANIFEST"
digest_lines="$(grep --count --extended-regexp '^ digest: sha256:[0-9a-f]{64}$' "$manifest" || true)"
if [[ "$digest_lines" != "1" ]]; then
echo "Expected exactly one pinned Symphony digest in $manifest" >&2
exit 1
fi

current_digest="$(sed --silent 's/^ digest: //p' "$manifest")"
if [[ "$current_digest" == "$DIGEST" ]]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Shared infrastructure already pins $DIGEST"
exit 0
fi

sed --in-place --extended-regexp "s/^ digest: sha256:[0-9a-f]{64}$/ digest: ${DIGEST}/" "$manifest"
git diff --check

changed_files="$(git diff --name-only)"
if [[ "$changed_files" != "$manifest" ]]; then
echo "Refusing to continue because files outside $manifest changed:" >&2
printf '%s\n' "$changed_files" >&2
exit 1
fi

echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Commit and open deployment pull request
if: ${{ steps.manifest.outputs.changed == 'true' }}
working-directory: deployment
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DIGEST: ${{ needs.build-image.outputs.digest }}
IMAGE_TAG: ${{ needs.build-image.outputs.image_tag }}
REGISTRY: ${{ needs.build-image.outputs.registry }}
SOURCE_SHA: ${{ needs.select-source.outputs.source_sha }}
run: |
set -euo pipefail
short_sha="${SOURCE_SHA:0:12}"
branch="automation/symphony-${short_sha}"

existing_pr="$(gh pr list \
--repo "$DEPLOYMENT_REPOSITORY" \
--head "$branch" \
--state open \
--json url \
--jq '.[0].url // ""')"
if [[ -n "$existing_pr" ]]; then
echo "Deployment pull request already exists: $existing_pr"
exit 0
fi

if git ls-remote --exit-code --heads origin "refs/heads/$branch" >/dev/null 2>&1; then
branch="${branch}-retry-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
fi

git config user.name "activeviam-symphony-deployer[bot]"
git config user.email "activeviam-symphony-deployer[bot]@users.noreply.github.com"
git switch --create "$branch"
git add "$DEPLOYMENT_MANIFEST"
git commit --message "deploy: Symphony ${short_sha}"
git push --set-upstream origin "$branch"

body_file="${RUNNER_TEMP}/symphony-deployment-pr.md"
printf '%s\n' \
'## Context' \
'' \
"Publish tested Symphony source ${SOURCE_SHA} to Shared EKS through GitOps." \
'' \
'## Change' \
'' \
"- Source: https://github.com/${GITHUB_REPOSITORY}/commit/${SOURCE_SHA}" \
"- Immutable tag: ${REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}" \
"- Pinned image: ${REGISTRY}/${ECR_REPOSITORY}@${DIGEST}" \
"- Manifest: ${DEPLOYMENT_MANIFEST}" \
'' \
'## Validation' \
'' \
'- The source make-all workflow completed successfully.' \
'- Manual dispatches re-run make all before publishing.' \
'- The deployment diff is restricted to the Symphony image digest.' \
'' \
'## Rollout' \
'' \
'After human review and merge, Argo CD will reconcile the Shared EKS deployment.' \
'This automation does not merge the pull request.' \
> "$body_file"

gh pr create \
--repo "$DEPLOYMENT_REPOSITORY" \
--base main \
--head "$branch" \
--title "deploy: Symphony ${short_sha}" \
--body-file "$body_file"
Loading
Loading