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
55 changes: 54 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ name: "Build and Push Docker Images"
# - Platform strategy: beta/rc builds amd64 only, release builds amd64+arm64
# - Semantic versioning tags
# - GitOps artifacts upload for downstream gitops-update workflow
# - Helm chart dispatch for automatic version updates in Helm repositories

on:
workflow_call:
Expand Down Expand Up @@ -58,6 +59,35 @@ on:
description: 'Enable GitOps artifacts upload for downstream gitops-update workflow'
type: boolean
default: false
# Helm dispatch configuration
enable_helm_dispatch:
description: 'Enable dispatching to Helm repository for chart updates'
type: boolean
default: false
helm_repository:
description: 'Helm repository to dispatch to (org/repo format, e.g., LerianStudio/helm)'
type: string
default: 'LerianStudio/helm'
helm_chart:
description: 'Helm chart name to update'
type: string
default: ''
helm_target_ref:
description: 'Target branch in Helm repository (e.g., develop, main)'
type: string
default: 'main'
helm_components_base_path:
description: 'Base path for components in source repo (default: components)'
type: string
default: 'components'
helm_env_file:
description: 'Env example file name relative to component path (default: .env.example)'
type: string
default: '.env.example'
helm_detect_env_changes:
description: 'Whether to detect new environment variables for Helm'
type: boolean
default: true

permissions:
contents: read
Expand Down Expand Up @@ -105,7 +135,7 @@ jobs:
id: set-platforms
run: |
TAG="${GITHUB_REF#refs/tags/}"

if [[ "$TAG" == *"-beta"* ]] || [[ "$TAG" == *"-rc"* ]]; then
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -249,3 +279,26 @@ jobs:
failed_jobs: ${{ needs.build.result == 'failure' && 'Build' || '' }}
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

# Dispatch to Helm repository for chart updates
dispatch-helm:
name: Dispatch Helm Update
needs: [prepare, build]
if: |
inputs.enable_helm_dispatch &&
needs.prepare.outputs.has_builds == 'true' &&
needs.build.result == 'success' &&
inputs.helm_chart != ''
uses: ./.github/workflows/dispatch-helm.yml
with:
helm_repository: ${{ inputs.helm_repository }}
chart: ${{ inputs.helm_chart }}
target_ref: ${{ inputs.helm_target_ref }}
version: ${{ github.ref_name }}
components_json: ${{ needs.prepare.outputs.matrix }}
components_base_path: ${{ inputs.helm_components_base_path }}
env_file: ${{ inputs.helm_env_file }}
detect_env_changes: ${{ inputs.helm_detect_env_changes }}
runner_type: ${{ inputs.runner_type }}
secrets:
helm_repo_token: ${{ secrets.HELM_REPO_TOKEN }}
318 changes: 318 additions & 0 deletions .github/workflows/dispatch-helm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
name: "Dispatch to Helm Repository"

# Reusable workflow for dispatching workflow_dispatch events to Helm chart repositories
# Sends a SINGLE dispatch with ALL components to enable single commit updates
# Uses workflow_dispatch instead of repository_dispatch to allow targeting specific branches
#
# Usage (with path mapping - recommended):
# env:
# PATH_MAPPING: |
# {
# "src": {"name": "my-api", "context": "."},
# "console": {"name": "my-console", "context": "console"}
# }
# ...
# dispatch-helm:
# uses: LerianStudio/github-actions-shared-workflows/.github/workflows/dispatch-helm.yml@main
# with:
# helm_repository: org/helm-repo
# chart: my-app
# target_ref: develop
# version: ${{ github.ref_name }}
# paths_matrix: ${{ needs.detect.outputs.matrix }}
# path_mapping: ${{ env.PATH_MAPPING }}
# secrets:
# helm_repo_token: ${{ secrets.HELM_REPO_TOKEN }}
#
# Usage (advanced - with full components_json):
# dispatch-helm:
# uses: LerianStudio/github-actions-shared-workflows/.github/workflows/dispatch-helm.yml@main
# with:
# helm_repository: org/helm-repo
# chart: my-app
# components_json: '[{"name":"backend","version":"1.0.0"},{"name":"frontend","version":"1.0.0"}]'
# secrets:
# helm_repo_token: ${{ secrets.HELM_REPO_TOKEN }}

on:
workflow_call:
inputs:
helm_repository:
description: 'Helm repository to dispatch to (org/repo format)'
type: string
required: true
target_ref:
description: 'Target branch/ref to trigger the workflow on (e.g., develop, main)'
type: string
default: 'develop'
workflow_file:
description: 'Workflow file name to trigger (e.g., app-sync.yml)'
type: string
default: 'app-sync.yml'
chart:
description: 'Helm chart name'
type: string
required: true
version:
description: 'Version to apply to all components (e.g., from git tag). Removes "v" prefix automatically.'
type: string
required: true
paths_matrix:
description: 'Raw paths matrix from changed-paths action (e.g., ["src","console"]). Use with path_mapping.'
type: string
required: false
path_mapping:
description: 'JSON mapping of paths to app names (e.g., {"src":"my-api","console":"my-console"})'
type: string
required: false
components_json:
description: 'JSON array of components (alternative to paths_matrix): [{"name":"backend","version":"1.0.0"},...]'
type: string
required: false
components_base_path:
description: 'Base path for components (default: components)'
type: string
default: 'components'
env_file:
description: 'Env example file name relative to component path (default: .env.example)'
type: string
default: '.env.example'
detect_env_changes:
description: 'Whether to detect new environment variables'
type: boolean
default: true
runner_type:
description: 'GitHub runner type to use'
type: string
default: 'ubuntu-latest'
secrets:
helm_repo_token:
description: 'GitHub token with access to Helm repository (needs repo scope)'
required: true

jobs:
prepare-and-dispatch:
name: Prepare and Dispatch
runs-on: ${{ inputs.runner_type }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Process all components
id: process
run: |
BASE_PATH="${{ inputs.components_base_path }}"
ENV_FILE="${{ inputs.env_file }}"
DETECT_ENV="${{ inputs.detect_env_changes }}"

# Determine BEFORE_SHA for comparison
# For tags, github.event.before is 0000..., so we need to find the previous tag
BEFORE_SHA="${{ github.event.before }}"
if [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ] || [ -z "$BEFORE_SHA" ]; then
# For tags, get the previous tag to compare against
CURRENT_TAG="${{ github.ref_name }}"
# Get list of tags sorted by version, find the one before current
PREVIOUS_TAG=$(git tag --sort=-v:refname | grep -A1 "^${CURRENT_TAG}$" | tail -1)
if [ -n "$PREVIOUS_TAG" ] && [ "$PREVIOUS_TAG" != "$CURRENT_TAG" ]; then
BEFORE_SHA=$(git rev-parse "${PREVIOUS_TAG}^{}" 2>/dev/null || echo "")
echo "Using previous tag for comparison: $PREVIOUS_TAG ($BEFORE_SHA)"
else
echo "No previous tag found, will treat all env vars as new"
BEFORE_SHA=""
fi
fi

# Get version from input (remove 'v' prefix if present)
INPUT_VERSION="${{ inputs.version }}"
INPUT_VERSION="${INPUT_VERSION#v}"

# Build components JSON from paths_matrix + path_mapping OR use components_json directly
if [ -n "${{ inputs.paths_matrix }}" ] && [ -n "${{ inputs.path_mapping }}" ]; then
echo "Using paths_matrix + path_mapping mode"
PATHS_MATRIX='${{ inputs.paths_matrix }}'
PATH_MAPPING='${{ inputs.path_mapping }}'

# Build components_json from paths and mapping
# Supports both simple format: {"src": "app-name"}
# and extended format: {"src": {"name": "app-name", "context": "."}}
COMPONENTS_JSON=$(echo "$PATHS_MATRIX" | jq -c --argjson mapping "$PATH_MAPPING" '
[.[] | . as $path | $mapping[$path] |
if type == "object" then
{name: .name, path: $path}
elif type == "string" then
{name: ., path: $path}
else
{name: $path, path: $path}
end
]
')
echo "Built components from paths: $COMPONENTS_JSON"
else
echo "Using components_json mode"
# Normalize components_json: handle both 'path' and 'working_dir' fields
RAW_COMPONENTS='${{ inputs.components_json }}'
echo "Raw components_json: $RAW_COMPONENTS"

# Normalize to use 'path' field (convert working_dir to path if needed)
COMPONENTS_JSON=$(echo "$RAW_COMPONENTS" | jq -c '[.[] | {name: .name, path: (.path // .working_dir // null), version: (.version // null)}]')
echo "Normalized components: $COMPONENTS_JSON"
fi

echo "Processing components: $COMPONENTS_JSON"

# Initialize output array
PROCESSED_COMPONENTS="["
FIRST=true
HAS_NEW_ENV_VARS=false

# Process each component
for row in $(echo "$COMPONENTS_JSON" | jq -c '.[]'); do
COMP_NAME=$(echo "$row" | jq -r '.name')
COMP_PATH=$(echo "$row" | jq -r '.path // empty')
COMP_VERSION=$(echo "$row" | jq -r '.version // empty')

# Use input version if provided and component doesn't have its own
if [ -z "$COMP_VERSION" ] && [ -n "$INPUT_VERSION" ]; then
COMP_VERSION="$INPUT_VERSION"
fi

# Determine component path
if [ -z "$COMP_PATH" ]; then
COMP_PATH="${BASE_PATH}/${COMP_NAME}"
fi

echo "Processing component: $COMP_NAME (path: $COMP_PATH)"

# Use component version or input version
VERSION="$COMP_VERSION"
echo " Version: $VERSION"

# Detect new env vars
ENV_VARS_JSON="{}"
if [ "$DETECT_ENV" = "true" ]; then
FULL_ENV_FILE="${COMP_PATH}/${ENV_FILE}"

if [ -f "$FULL_ENV_FILE" ]; then
CURRENT_VARS=$(grep -E "^[A-Z_][A-Z0-9_]*=" "$FULL_ENV_FILE" | cut -d'=' -f1 | sort || echo "")

PREVIOUS_VARS=""
if [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
PREVIOUS_VARS=$(git show "${BEFORE_SHA}:${FULL_ENV_FILE}" 2>/dev/null | grep -E "^[A-Z_][A-Z0-9_]*=" | cut -d'=' -f1 | sort || echo "")
fi

# Find new variables
NEW_VARS=""
if [ -n "$CURRENT_VARS" ]; then
while IFS= read -r var; do
if [ -n "$var" ] && ! echo "$PREVIOUS_VARS" | grep -q "^${var}$"; then
NEW_VARS="${NEW_VARS} ${var}"
fi
done <<< "$CURRENT_VARS"
fi

# Build JSON for new vars
if [ -n "$(echo "$NEW_VARS" | tr -d '[:space:]')" ]; then
HAS_NEW_ENV_VARS=true
ENV_VARS_JSON="{"
FIRST_VAR=true
for var in $NEW_VARS; do
# Extract value, remove surrounding quotes, then escape internal quotes
DEFAULT_VALUE=$(grep "^${var}=" "$FULL_ENV_FILE" | cut -d'=' -f2- | sed 's/^"//;s/"$//' | sed 's/"/\\"/g' || echo "")
if [ "$FIRST_VAR" = "true" ]; then
ENV_VARS_JSON="${ENV_VARS_JSON}\"${var}\":\"${DEFAULT_VALUE}\""
FIRST_VAR=false
else
ENV_VARS_JSON="${ENV_VARS_JSON},\"${var}\":\"${DEFAULT_VALUE}\""
fi
done
ENV_VARS_JSON="${ENV_VARS_JSON}}"
echo " New env vars: $NEW_VARS"
fi
fi
fi

# Build component object
COMP_OBJ="{\"name\":\"${COMP_NAME}\",\"version\":\"${VERSION}\",\"env_vars\":${ENV_VARS_JSON}}"

if [ "$FIRST" = "true" ]; then
PROCESSED_COMPONENTS="${PROCESSED_COMPONENTS}${COMP_OBJ}"
FIRST=false
else
PROCESSED_COMPONENTS="${PROCESSED_COMPONENTS},${COMP_OBJ}"
fi
done

PROCESSED_COMPONENTS="${PROCESSED_COMPONENTS}]"

echo "Processed components: $PROCESSED_COMPONENTS"
echo "has_new_env_vars=$HAS_NEW_ENV_VARS" >> $GITHUB_OUTPUT

# Save to file to avoid escaping issues
echo "$PROCESSED_COMPONENTS" > /tmp/components_payload.json

- name: Dispatch to Helm repository
run: |
COMPONENTS=$(cat /tmp/components_payload.json)

# Build the full payload
PAYLOAD=$(jq -n \
--arg chart "${{ inputs.chart }}" \
--argjson components "$COMPONENTS" \
--arg has_new_env_vars "${{ steps.process.outputs.has_new_env_vars }}" \
--arg source_repo "${{ github.repository }}" \
--arg source_sha "${{ github.sha }}" \
--arg source_ref "${{ github.ref_name }}" \
--arg source_actor "${{ github.actor }}" \
'{
chart: $chart,
components: $components,
has_new_env_vars: ($has_new_env_vars == "true"),
source_repo: $source_repo,
source_sha: $source_sha,
source_ref: $source_ref,
source_actor: $source_actor
}')

echo "Dispatching payload:"
echo "$PAYLOAD" | jq .

# Convert payload to compact JSON string for workflow_dispatch input
PAYLOAD_STRING=$(echo "$PAYLOAD" | jq -c .)

# Send workflow_dispatch (allows targeting specific branch)
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${{ secrets.helm_repo_token }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ inputs.helm_repository }}/actions/workflows/${{ inputs.workflow_file }}/dispatches" \
-d "{\"ref\":\"${{ inputs.target_ref }}\",\"inputs\":{\"payload\":$(echo "$PAYLOAD_STRING" | jq -R .)}}")

HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n1)

echo "HTTP Status: $HTTP_CODE"

if [ "$HTTP_CODE" != "204" ]; then
echo "::error::Failed to dispatch workflow. HTTP $HTTP_CODE"
echo "$HTTP_BODY" | jq . 2>/dev/null || echo "$HTTP_BODY"
exit 1
fi

echo "Workflow dispatched successfully"

- name: Summary
run: |
COMPONENTS=$(cat /tmp/components_payload.json)

echo "### Dispatch Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Repository:** \`${{ inputs.helm_repository }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Chart:** \`${{ inputs.chart }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Components:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Component | Version | New Env Vars |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|---------|--------------|" >> $GITHUB_STEP_SUMMARY

echo "$COMPONENTS" | jq -r '.[] | "| \(.name) | \(.version) | \(.env_vars | if . == {} then "-" else (. | keys | join(", ")) end) |"' >> $GITHUB_STEP_SUMMARY
Loading