docs: add readme badges #27
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
| name: Build and Push Docker Images | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "apps/**" | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| apps: ${{ steps.set-matrix.outputs.apps }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: set-matrix | |
| env: | |
| HEAD_SHA: ${{ github.sha }} | |
| BASE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ] || [ -z "$BASE_SHA" ]; then | |
| BASE_SHA="HEAD^" | |
| fi | |
| if ! git rev-parse --verify "$BASE_SHA" >/dev/null 2>&1; then | |
| echo "::error::Invalid BASE_SHA: $BASE_SHA" | |
| exit 1 | |
| fi | |
| DIFF=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- apps/ \ | |
| | cut -d/ -f2 \ | |
| | sort -u) | |
| if [ -z "$DIFF" ]; then | |
| echo "apps=[]" >> "$GITHUB_OUTPUT" | |
| else | |
| SAFE_APPS="" | |
| while IFS= read -r app; do | |
| if echo "$app" | grep -qE '^[a-zA-Z0-9_-]+$'; then | |
| SAFE_APPS="${SAFE_APPS}${app}"$'\n' | |
| else | |
| echo "::warning::Skipping app with unsafe name: $app" | |
| fi | |
| done <<< "$DIFF" | |
| if [ -z "$SAFE_APPS" ]; then | |
| echo "apps=[]" >> "$GITHUB_OUTPUT" | |
| else | |
| APPS_JSON=$(echo "$SAFE_APPS" | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| echo "apps=$APPS_JSON" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| build-and-push: | |
| needs: detect-changes | |
| if: ${{ needs.detect-changes.outputs.apps != '[]' && needs.detect-changes.outputs.apps != '' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| app: ${{ fromJSON(needs.detect-changes.outputs.apps) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: nhedger/setup-sops@v2 | |
| - name: Decrypt SOPS Secrets and Setup OCI | |
| id: setup_oci | |
| env: | |
| SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| TMPKEY=$(mktemp) | |
| trap 'rm -f "$TMPKEY"' EXIT | |
| echo "$SOPS_AGE_KEY" > "$TMPKEY" | |
| export SOPS_AGE_KEY_FILE="$TMPKEY" | |
| SECRETS_JSON=$(sops -d --output-type json secrets/prod/secrets.yaml) | |
| OCI_USER=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_user_ocid') | |
| OCI_TENANCY=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_tenancy_ocid') | |
| OCI_FINGERPRINT=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_fingerprint') | |
| OCI_REGION=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_region') | |
| OCI_COMPARTMENT=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_compartment_ocid') | |
| OCI_PRIVATE_KEY=$(echo "$SECRETS_JSON" | jq -r '.TF_VAR_private_key_content') | |
| echo "::add-mask::$OCI_USER" | |
| echo "::add-mask::$OCI_TENANCY" | |
| echo "::add-mask::$OCI_FINGERPRINT" | |
| echo "::add-mask::$OCI_COMPARTMENT" | |
| while IFS= read -r line; do | |
| [ -n "$line" ] && echo "::add-mask::$line" | |
| done <<< "$OCI_PRIVATE_KEY" | |
| echo "OCI_CLI_USER=$OCI_USER" >> "$GITHUB_ENV" | |
| echo "OCI_CLI_TENANCY=$OCI_TENANCY" >> "$GITHUB_ENV" | |
| echo "OCI_CLI_FINGERPRINT=$OCI_FINGERPRINT" >> "$GITHUB_ENV" | |
| echo "OCI_CLI_REGION=$OCI_REGION" >> "$GITHUB_ENV" | |
| echo "OCI_COMPARTMENT_ID=$OCI_COMPARTMENT" >> "$GITHUB_ENV" | |
| { | |
| echo "OCI_CLI_KEY_CONTENT<<EOF_OCI_KEY" | |
| echo "$OCI_PRIVATE_KEY" | |
| echo "EOF_OCI_KEY" | |
| } >> "$GITHUB_ENV" | |
| - name: Setup OCI Config | |
| id: setup_oci_config | |
| run: | | |
| set -euo pipefail | |
| # Create OCI config directory | |
| mkdir -p ~/.oci | |
| chmod 700 ~/.oci | |
| # Write private key to file | |
| echo "$OCI_CLI_KEY_CONTENT" > ~/.oci/key.pem | |
| chmod 600 ~/.oci/key.pem | |
| # Create OCI config file | |
| cat > ~/.oci/config <<EOF | |
| [DEFAULT] | |
| user=$OCI_CLI_USER | |
| fingerprint=$OCI_CLI_FINGERPRINT | |
| tenancy=$OCI_CLI_TENANCY | |
| region=$OCI_CLI_REGION | |
| key_file=~/.oci/key.pem | |
| EOF | |
| chmod 600 ~/.oci/config | |
| echo "✓ OCI CLI configured" | |
| - name: Install OCI CLI | |
| run: | | |
| set -euo pipefail | |
| python3 -m pip install --user oci-cli | |
| echo "$HOME/.local/bin" >> $GITHUB_PATH | |
| - name: List Vault Secrets | |
| id: list_secrets | |
| run: | | |
| set -euo pipefail | |
| SECRETS_JSON=$(oci vault secret list \ | |
| --config-file ~/.oci/config \ | |
| --compartment-id "$OCI_COMPARTMENT_ID" \ | |
| --all \ | |
| --lifecycle-state ACTIVE \ | |
| --query 'data[].{"name": "secret-name", "id": id}' \ | |
| --output json) | |
| { | |
| echo "secrets<<EOF_SECRETS_JSON" | |
| echo "$SECRETS_JSON" | |
| echo "EOF_SECRETS_JSON" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "✓ Found $(echo "$SECRETS_JSON" | jq '. | length') secrets" | |
| - name: Resolve Vault Secrets | |
| id: vault_secrets | |
| run: | | |
| set -euo pipefail | |
| VAULT_SECRETS='${{ steps.list_secrets.outputs.secrets }}' | |
| SECRET_FILE="$RUNNER_TEMP/vault-secrets-${{ github.run_id }}.env" | |
| > "$SECRET_FILE" | |
| chmod 600 "$SECRET_FILE" | |
| if [ -n "$VAULT_SECRETS" ] && [ "$VAULT_SECRETS" != "[]" ]; then | |
| while IFS= read -r secret; do | |
| NAME=$(echo "$secret" | jq -r '.name') | |
| ID=$(echo "$secret" | jq -r '.id') | |
| if ! echo "$NAME" | grep -qE '^[a-zA-Z0-9_-]+$'; then | |
| echo "::warning::Skipping secret with unsafe name: $NAME" | |
| continue | |
| fi | |
| echo "Fetching secret: $NAME" | |
| BUNDLE=$(oci secrets secret-bundle get \ | |
| --config-file ~/.oci/config \ | |
| --secret-id "$ID" \ | |
| --stage CURRENT 2>&1) || { | |
| echo "::warning::Could not fetch secret: $NAME" >&2 | |
| continue | |
| } | |
| VAL=$(echo "$BUNDLE" | jq -r '.data."secret-bundle-content".content' 2>/dev/null | base64 -d 2>/dev/null) || { | |
| echo "::warning::Could not decode secret: $NAME" >&2 | |
| continue | |
| } | |
| if [ -n "$VAL" ]; then | |
| echo "::add-mask::$VAL" | |
| while IFS= read -r line; do | |
| [ -n "$line" ] && echo "::add-mask::$line" | |
| done <<< "$VAL" | |
| ARG_NAME=$(echo "$NAME" | tr '-' '_' | tr '[:lower:]' '[:upper:]') | |
| # Write to file | |
| echo "${ARG_NAME}=${VAL}" >> "$SECRET_FILE" | |
| echo "✓ Added secret: $ARG_NAME" | |
| fi | |
| done < <(echo "$VAULT_SECRETS" | jq -c '.[]') | |
| else | |
| echo "::warning::No secrets found or empty list" | |
| fi | |
| echo "secret_file=$SECRET_FILE" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Secrets written to secure file ($(wc -l < "$SECRET_FILE") lines)" | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and Push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: apps/${{ matrix.app }} | |
| push: true | |
| tags: | | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.app }}:latest | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.app }}:${{ github.sha }} | |
| secret-files: | | |
| secrets=${{ steps.vault_secrets.outputs.secret_file }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Cleanup Secrets File | |
| if: always() | |
| run: | | |
| SECRET_FILE="${{ steps.vault_secrets.outputs.secret_file }}" | |
| if [ -f "$SECRET_FILE" ]; then | |
| shred -vfz -n 3 "$SECRET_FILE" 2>/dev/null || rm -f "$SECRET_FILE" | |
| fi |