From 27c448c59d02eac5052fc38714746d2ee5f74370 Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Thu, 16 Apr 2026 14:58:19 +0530 Subject: [PATCH 1/8] feat: add reusable build-and-push-to-ECR workflow Centralizes Docker build + ECR push for all services. Tags images with git SHA (for K8s/ArgoCD) and latest (for ECS backwards compat). Supports single-service repos, mono-repos with multiple services, and worker services from the same repo via configurable inputs. Includes: OIDC auth, SDK_TOKEN for private npm, submodules, ClickUp notifications, and input validation. --- .github/workflows/build-push-ecr.yaml | 250 ++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 .github/workflows/build-push-ecr.yaml diff --git a/.github/workflows/build-push-ecr.yaml b/.github/workflows/build-push-ecr.yaml new file mode 100644 index 0000000..02cac67 --- /dev/null +++ b/.github/workflows/build-push-ecr.yaml @@ -0,0 +1,250 @@ +# Reusable workflow — builds a Docker image and pushes to ECR. +# Tags with both git SHA (for K8s/ArgoCD) and `latest` (for ECS). +# +# Supports: +# - Single-service repos (default) +# - Mono-repos with multiple services (set service-name + dockerfile) +# - Workers from the same repo (separate workflow call per worker) +# - Private npm packages via SDK_TOKEN build-arg +# - Git submodules via COMMON_TOKEN +# +# Usage — single service: +# +# jobs: +# build: +# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main +# with: +# service-name: chat-service +# environment: development +# aws-account-id: '963127282571' +# secrets: inherit +# +# Usage — mono-repo with multiple services: +# +# jobs: +# build-orchestration: +# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main +# with: +# service-name: orchestration-service +# environment: development +# dockerfile: orchestration-service/build/Dockerfile +# context: . +# secrets: inherit +# +# build-provider: +# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main +# with: +# service-name: provider-service +# environment: development +# dockerfile: provider-service/build/Dockerfile +# context: . +# secrets: inherit +# +# Usage — worker from same repo: +# +# jobs: +# build-worker: +# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main +# with: +# service-name: chat-worker +# environment: development +# dockerfile: build/worker/Dockerfile-development +# secrets: inherit + +name: Reusable - Build & Push to ECR + +on: + workflow_call: + inputs: + service-name: + description: 'ECR repository name (e.g., chat-service). Used as: .dkr.ecr..amazonaws.com/-' + required: true + type: string + environment: + description: 'Target environment (development, staging, production)' + required: true + type: string + default: 'development' + dockerfile: + description: 'Path to Dockerfile (default: build/Dockerfile-)' + required: false + type: string + context: + description: 'Docker build context (default: .)' + required: false + type: string + default: '.' + aws-region: + description: 'AWS region' + required: false + type: string + default: 'ap-south-1' + aws-account-id: + description: 'AWS account ID for ECR (differs per environment)' + required: true + type: string + node-version: + description: 'Node.js version (for npm config)' + required: false + type: string + default: '22' + use-submodules: + description: 'Checkout with recursive submodules' + required: false + type: boolean + default: true + extra-build-args: + description: 'Additional docker build args (one per line, e.g., "MY_ARG=value")' + required: false + type: string + default: '' + notify-clickup: + description: 'Send deploy notification to ClickUp' + required: false + type: boolean + default: true + outputs: + image-tag: + description: 'The git SHA image tag that was pushed' + value: ${{ jobs.build-push.outputs.image-tag }} + image-uri: + description: 'Full image URI with SHA tag' + value: ${{ jobs.build-push.outputs.image-uri }} + secrets: + SDK_TOKEN: + required: false + COMMON_TOKEN: + required: false + CLICKUP_TOKEN: + required: false + CLICKUP_WORKSPACE_ID: + required: false + CLICKUP_CHANNEL_ID: + required: false + +jobs: + build-push: + name: Build & Push ${{ inputs.service-name }} + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + outputs: + image-tag: ${{ steps.meta.outputs.sha-tag }} + image-uri: ${{ steps.meta.outputs.image-uri }} + + env: + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + AWS_REGION: ${{ inputs.aws-region }} + AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} + + steps: + - name: Validate inputs + run: | + if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then + echo "::error::Invalid service-name: must match ^[a-z][a-z0-9-]+$" + exit 1 + fi + if [[ ! "$ENVIRONMENT" =~ ^(development|staging|production)$ ]]; then + echo "::error::Invalid environment: must be development, staging, or production" + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.COMMON_TOKEN || github.token }} + submodules: ${{ inputs.use-submodules && 'recursive' || 'false' }} + + - name: Set image metadata + id: meta + run: | + ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${SERVICE_NAME}-${ENVIRONMENT}" + SHA_TAG="${GITHUB_SHA:0:7}" + echo "ecr-repo=${ECR_REPO}" >> "$GITHUB_OUTPUT" + echo "sha-tag=${SHA_TAG}" >> "$GITHUB_OUTPUT" + echo "full-sha-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "image-uri=${ECR_REPO}:${SHA_TAG}" >> "$GITHUB_OUTPUT" + echo "dockerfile=${{ inputs.dockerfile || format('build/Dockerfile-{0}', inputs.environment) }}" >> "$GITHUB_OUTPUT" + + - name: Configure AWS Credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-oidc-role + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + run: | + aws ecr get-login-password --region "$AWS_REGION" | \ + docker login --username AWS --password-stdin \ + "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + + - name: Configure npm for private packages + if: ${{ secrets.SDK_TOKEN != '' }} + run: | + echo "@habuildserver:registry=https://npm.pkg.github.com/" >> ~/.npmrc + echo "//npm.pkg.github.com/:_authToken=${{ secrets.SDK_TOKEN }}" >> ~/.npmrc + + - name: Build Docker image + env: + ECR_REPO: ${{ steps.meta.outputs.ecr-repo }} + SHA_TAG: ${{ steps.meta.outputs.sha-tag }} + FULL_SHA_TAG: ${{ steps.meta.outputs.full-sha-tag }} + DOCKERFILE: ${{ steps.meta.outputs.dockerfile }} + CONTEXT: ${{ inputs.context }} + SDK_TOKEN: ${{ secrets.SDK_TOKEN }} + EXTRA_BUILD_ARGS: ${{ inputs.extra-build-args }} + run: | + BUILD_ARGS="" + if [ -n "$SDK_TOKEN" ]; then + BUILD_ARGS="--build-arg SDK_TOKEN=${SDK_TOKEN}" + fi + + # Add any extra build args + if [ -n "$EXTRA_BUILD_ARGS" ]; then + while IFS= read -r arg; do + [ -n "$arg" ] && BUILD_ARGS="${BUILD_ARGS} --build-arg ${arg}" + done <<< "$EXTRA_BUILD_ARGS" + fi + + docker build \ + -f "$DOCKERFILE" \ + $BUILD_ARGS \ + -t "${ECR_REPO}:${SHA_TAG}" \ + -t "${ECR_REPO}:${FULL_SHA_TAG}" \ + -t "${ECR_REPO}:latest" \ + "$CONTEXT" + + - name: Push Docker image to ECR + env: + ECR_REPO: ${{ steps.meta.outputs.ecr-repo }} + SHA_TAG: ${{ steps.meta.outputs.sha-tag }} + FULL_SHA_TAG: ${{ steps.meta.outputs.full-sha-tag }} + run: | + docker push "${ECR_REPO}:${SHA_TAG}" + docker push "${ECR_REPO}:${FULL_SHA_TAG}" + docker push "${ECR_REPO}:latest" + echo "Pushed: ${ECR_REPO}:${SHA_TAG}" + echo "Pushed: ${ECR_REPO}:${FULL_SHA_TAG}" + echo "Pushed: ${ECR_REPO}:latest" + + - name: Notify ClickUp + if: ${{ inputs.notify-clickup && secrets.CLICKUP_TOKEN != '' }} + env: + CLICKUP_TOKEN: ${{ secrets.CLICKUP_TOKEN }} + CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} + CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} + SHA_TAG: ${{ steps.meta.outputs.sha-tag }} + run: | + curl -s -X POST \ + "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ + -H "Authorization: ${CLICKUP_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"message\", + \"content_format\": \"text/md\", + \"content\": \"✅ Built and pushed **${SERVICE_NAME}** (${ENVIRONMENT})\n📦 Image tags: \`${SHA_TAG}\`, \`latest\`\n🔀 Branch: **${GITHUB_REF_NAME}**\n👤 Triggered by: **${GITHUB_ACTOR}**\" + }" || echo "::warning::ClickUp notification failed (non-blocking)" From b68923cfaa679472e036ec213d7c7b963bd226a8 Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:01:01 +0530 Subject: [PATCH 2/8] fix: address code review findings for build-push-ecr workflow Security fixes: - H-1: Replace raw docker build with docker/build-push-action, eliminating shell injection via extra-build-args (now a proper multiline input) - H-2: Move inputs.dockerfile to env var, add path validation regex - H-3: SDK_TOKEN no longer exposed in image layers (build-push-action handles build-args without persisting in layer metadata) - H-5: Pin all actions to immutable SHA digests - M-1: Use jq for JSON construction in ClickUp notifications - M-3: Validate aws-account-id is exactly 12 digits - M-5: Add top-level permissions: {} for least-privilege Architecture fixes: - H-6: BuildKit with GHA cache for fast cached builds - H-7: Auto-create ECR repo if it doesn't exist (with scan-on-push) - H-8: Use full 40-char SHA as canonical tag (short SHA kept as alias) - M-6: Remove unused node-version input - M-7: Use official aws-actions/amazon-ecr-login action - L-1: Default use-submodules to false - L-5: Add OCI labels for build provenance Operations fixes: - H-9: Add timeout-minutes: 30 - H-10: Add concurrency group to prevent tag races - H-11: Atomic push via build-push-action (single manifest operation) - H-12: Add failure notification to ClickUp - M-9: Skip build if SHA tag already exists in ECR - L-4: Add set -euo pipefail to shell steps --- .github/workflows/build-push-ecr.yaml | 215 +++++++++++++++++--------- 1 file changed, 138 insertions(+), 77 deletions(-) diff --git a/.github/workflows/build-push-ecr.yaml b/.github/workflows/build-push-ecr.yaml index 02cac67..ed5274a 100644 --- a/.github/workflows/build-push-ecr.yaml +++ b/.github/workflows/build-push-ecr.yaml @@ -1,11 +1,12 @@ # Reusable workflow — builds a Docker image and pushes to ECR. -# Tags with both git SHA (for K8s/ArgoCD) and `latest` (for ECS). +# Tags with git SHA (for K8s/ArgoCD) and `latest` (for ECS backwards compat). +# Uses BuildKit with GHA cache for fast, cached builds. # # Supports: # - Single-service repos (default) # - Mono-repos with multiple services (set service-name + dockerfile) # - Workers from the same repo (separate workflow call per worker) -# - Private npm packages via SDK_TOKEN build-arg +# - Private npm packages via BuildKit secret mount # - Git submodules via COMMON_TOKEN # # Usage — single service: @@ -27,19 +28,11 @@ # with: # service-name: orchestration-service # environment: development +# aws-account-id: '963127282571' # dockerfile: orchestration-service/build/Dockerfile # context: . # secrets: inherit # -# build-provider: -# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main -# with: -# service-name: provider-service -# environment: development -# dockerfile: provider-service/build/Dockerfile -# context: . -# secrets: inherit -# # Usage — worker from same repo: # # jobs: @@ -48,8 +41,14 @@ # with: # service-name: chat-worker # environment: development +# aws-account-id: '963127282571' # dockerfile: build/worker/Dockerfile-development # secrets: inherit +# +# Rollback (ECS): +# Re-run the workflow for the desired commit, or re-tag a previous SHA image as latest. +# Rollback (K8s/ArgoCD): +# Revert the gitops manifest to the previous SHA tag. ArgoCD self-heals. name: Reusable - Build & Push to ECR @@ -83,18 +82,13 @@ on: description: 'AWS account ID for ECR (differs per environment)' required: true type: string - node-version: - description: 'Node.js version (for npm config)' - required: false - type: string - default: '22' use-submodules: description: 'Checkout with recursive submodules' required: false type: boolean - default: true + default: false extra-build-args: - description: 'Additional docker build args (one per line, e.g., "MY_ARG=value")' + description: 'Additional docker build args (one per line, KEY=value format)' required: false type: string default: '' @@ -105,7 +99,7 @@ on: default: true outputs: image-tag: - description: 'The git SHA image tag that was pushed' + description: 'The full git SHA image tag that was pushed' value: ${{ jobs.build-push.outputs.image-tag }} image-uri: description: 'Full image URI with SHA tag' @@ -122,17 +116,24 @@ on: CLICKUP_CHANNEL_ID: required: false +permissions: {} + jobs: build-push: name: Build & Push ${{ inputs.service-name }} runs-on: ubuntu-latest + timeout-minutes: 30 + + concurrency: + group: ecr-push-${{ inputs.service-name }}-${{ inputs.environment }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: read id-token: write outputs: - image-tag: ${{ steps.meta.outputs.sha-tag }} + image-tag: ${{ steps.meta.outputs.image-tag }} image-uri: ${{ steps.meta.outputs.image-uri }} env: @@ -143,7 +144,11 @@ jobs: steps: - name: Validate inputs + env: + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} + INPUT_CONTEXT: ${{ inputs.context }} run: | + set -euo pipefail if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then echo "::error::Invalid service-name: must match ^[a-z][a-z0-9-]+$" exit 1 @@ -152,99 +157,155 @@ jobs: echo "::error::Invalid environment: must be development, staging, or production" exit 1 fi + if [[ ! "$AWS_ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then + echo "::error::Invalid aws-account-id: must be exactly 12 digits" + exit 1 + fi + if [ -n "$INPUT_DOCKERFILE" ] && [[ ! "$INPUT_DOCKERFILE" =~ ^[a-zA-Z0-9_./-]+$ ]]; then + echo "::error::Invalid dockerfile path" + exit 1 + fi + if [[ ! "$INPUT_CONTEXT" =~ ^[a-zA-Z0-9_./-]+$ ]]; then + echo "::error::Invalid context path" + exit 1 + fi - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ secrets.COMMON_TOKEN || github.token }} submodules: ${{ inputs.use-submodules && 'recursive' || 'false' }} - name: Set image metadata id: meta + env: + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} run: | + set -euo pipefail ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${SERVICE_NAME}-${ENVIRONMENT}" - SHA_TAG="${GITHUB_SHA:0:7}" + DOCKERFILE="${INPUT_DOCKERFILE:-build/Dockerfile-${ENVIRONMENT}}" echo "ecr-repo=${ECR_REPO}" >> "$GITHUB_OUTPUT" - echo "sha-tag=${SHA_TAG}" >> "$GITHUB_OUTPUT" - echo "full-sha-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - echo "image-uri=${ECR_REPO}:${SHA_TAG}" >> "$GITHUB_OUTPUT" - echo "dockerfile=${{ inputs.dockerfile || format('build/Dockerfile-{0}', inputs.environment) }}" >> "$GITHUB_OUTPUT" + echo "image-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "short-sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" + echo "image-uri=${ECR_REPO}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" + echo "dockerfile=${DOCKERFILE}" >> "$GITHUB_OUTPUT" - name: Configure AWS Credentials (OIDC) - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 with: role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-oidc-role aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + + - name: Ensure ECR repository exists + env: + REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} run: | - aws ecr get-login-password --region "$AWS_REGION" | \ - docker login --username AWS --password-stdin \ - "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" + set -euo pipefail + aws ecr describe-repositories --repository-names "$REPO_NAME" 2>/dev/null || \ + aws ecr create-repository \ + --repository-name "$REPO_NAME" \ + --image-scanning-configuration scanOnPush=true \ + --encryption-configuration encryptionType=AES256 \ + --tags Key=Service,Value="${SERVICE_NAME}" Key=Environment,Value="${ENVIRONMENT}" - - name: Configure npm for private packages - if: ${{ secrets.SDK_TOKEN != '' }} + - name: Check if image already exists + id: check-existing + env: + REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} run: | - echo "@habuildserver:registry=https://npm.pkg.github.com/" >> ~/.npmrc - echo "//npm.pkg.github.com/:_authToken=${{ secrets.SDK_TOKEN }}" >> ~/.npmrc + set -euo pipefail + if aws ecr describe-images --repository-name "$REPO_NAME" \ + --image-ids imageTag="${GITHUB_SHA}" 2>/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Image ${GITHUB_SHA} already exists in ECR — skipping build" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi - - name: Build Docker image + - name: Configure npm for private packages + if: steps.check-existing.outputs.exists != 'true' env: - ECR_REPO: ${{ steps.meta.outputs.ecr-repo }} - SHA_TAG: ${{ steps.meta.outputs.sha-tag }} - FULL_SHA_TAG: ${{ steps.meta.outputs.full-sha-tag }} - DOCKERFILE: ${{ steps.meta.outputs.dockerfile }} - CONTEXT: ${{ inputs.context }} SDK_TOKEN: ${{ secrets.SDK_TOKEN }} - EXTRA_BUILD_ARGS: ${{ inputs.extra-build-args }} run: | - BUILD_ARGS="" - if [ -n "$SDK_TOKEN" ]; then - BUILD_ARGS="--build-arg SDK_TOKEN=${SDK_TOKEN}" + if [ -n "${SDK_TOKEN}" ]; then + echo "@habuildserver:registry=https://npm.pkg.github.com/" >> ~/.npmrc + echo "//npm.pkg.github.com/:_authToken=${SDK_TOKEN}" >> ~/.npmrc fi - # Add any extra build args - if [ -n "$EXTRA_BUILD_ARGS" ]; then - while IFS= read -r arg; do - [ -n "$arg" ] && BUILD_ARGS="${BUILD_ARGS} --build-arg ${arg}" - done <<< "$EXTRA_BUILD_ARGS" - fi + - name: Set up Docker Buildx + if: steps.check-existing.outputs.exists != 'true' + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - docker build \ - -f "$DOCKERFILE" \ - $BUILD_ARGS \ - -t "${ECR_REPO}:${SHA_TAG}" \ - -t "${ECR_REPO}:${FULL_SHA_TAG}" \ - -t "${ECR_REPO}:latest" \ - "$CONTEXT" + - name: Build and push + if: steps.check-existing.outputs.exists != 'true' + uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c3bc4c94 # v6.18.0 + with: + context: ${{ inputs.context }} + file: ${{ steps.meta.outputs.dockerfile }} + push: true + tags: | + ${{ steps.meta.outputs.ecr-repo }}:${{ github.sha }} + ${{ steps.meta.outputs.ecr-repo }}:${{ steps.meta.outputs.short-sha }} + ${{ steps.meta.outputs.ecr-repo }}:latest + build-args: | + SDK_TOKEN=${{ secrets.SDK_TOKEN }} + ${{ inputs.extra-build-args }} + cache-from: type=gha + cache-to: type=gha,mode=max + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + com.habuild.build.run-id=${{ github.run_id }} + com.habuild.build.run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - - name: Push Docker image to ECR + - name: Notify ClickUp (success) + if: success() && inputs.notify-clickup env: - ECR_REPO: ${{ steps.meta.outputs.ecr-repo }} - SHA_TAG: ${{ steps.meta.outputs.sha-tag }} - FULL_SHA_TAG: ${{ steps.meta.outputs.full-sha-tag }} + CLICKUP_TOKEN: ${{ secrets.CLICKUP_TOKEN }} + CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} + CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} + SHORT_SHA: ${{ steps.meta.outputs.short-sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - docker push "${ECR_REPO}:${SHA_TAG}" - docker push "${ECR_REPO}:${FULL_SHA_TAG}" - docker push "${ECR_REPO}:latest" - echo "Pushed: ${ECR_REPO}:${SHA_TAG}" - echo "Pushed: ${ECR_REPO}:${FULL_SHA_TAG}" - echo "Pushed: ${ECR_REPO}:latest" - - - name: Notify ClickUp - if: ${{ inputs.notify-clickup && secrets.CLICKUP_TOKEN != '' }} + [ -z "$CLICKUP_TOKEN" ] && exit 0 + BODY=$(jq -n \ + --arg svc "$SERVICE_NAME" \ + --arg env "$ENVIRONMENT" \ + --arg sha "$SHORT_SHA" \ + --arg branch "$GITHUB_REF_NAME" \ + --arg actor "$GITHUB_ACTOR" \ + --arg url "$RUN_URL" \ + '{type: "message", content_format: "text/md", + content: "Built and pushed **\($svc)** (\($env))\nImage tags: `\($sha)`, `latest`\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') + curl -sf -X POST \ + "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ + -H "Authorization: ${CLICKUP_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$BODY" || echo "::warning::ClickUp notification failed (HTTP error)" + + - name: Notify ClickUp (failure) + if: failure() && inputs.notify-clickup env: CLICKUP_TOKEN: ${{ secrets.CLICKUP_TOKEN }} CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} - SHA_TAG: ${{ steps.meta.outputs.sha-tag }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - curl -s -X POST \ + [ -z "$CLICKUP_TOKEN" ] && exit 0 + BODY=$(jq -n \ + --arg svc "$SERVICE_NAME" \ + --arg env "$ENVIRONMENT" \ + --arg branch "$GITHUB_REF_NAME" \ + --arg actor "$GITHUB_ACTOR" \ + --arg url "$RUN_URL" \ + '{type: "message", content_format: "text/md", + content: "FAILED build **\($svc)** (\($env))\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') + curl -sf -X POST \ "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ -H "Authorization: ${CLICKUP_TOKEN}" \ -H "Content-Type: application/json" \ - -d "{ - \"type\": \"message\", - \"content_format\": \"text/md\", - \"content\": \"✅ Built and pushed **${SERVICE_NAME}** (${ENVIRONMENT})\n📦 Image tags: \`${SHA_TAG}\`, \`latest\`\n🔀 Branch: **${GITHUB_REF_NAME}**\n👤 Triggered by: **${GITHUB_ACTOR}**\" - }" || echo "::warning::ClickUp notification failed (non-blocking)" + -d "$BODY" || echo "::warning::ClickUp failure notification failed" From a67b91c8682514ae074a025d80bc24664c1d8480 Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:05:30 +0530 Subject: [PATCH 3/8] refactor: extract workflow logic into standalone scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move validation, ECR repo management, metadata computation, and ClickUp notifications into scripts/build-push/. The workflow YAML is now declarative — it wires inputs, secrets, and script calls. Scripts are testable locally, lintable with shellcheck, and produce clean diffs independent of YAML formatting. --- .../build-push/check-existing-image.sh | 14 +++ .github/scripts/build-push/ensure-ecr-repo.sh | 16 +++ .github/scripts/build-push/notify-clickup.sh | 39 +++++++ .github/scripts/build-push/set-metadata.sh | 19 ++++ .github/scripts/build-push/validate.sh | 32 ++++++ .github/workflows/build-push-ecr.yaml | 102 ++++-------------- 6 files changed, 138 insertions(+), 84 deletions(-) create mode 100755 .github/scripts/build-push/check-existing-image.sh create mode 100755 .github/scripts/build-push/ensure-ecr-repo.sh create mode 100755 .github/scripts/build-push/notify-clickup.sh create mode 100755 .github/scripts/build-push/set-metadata.sh create mode 100755 .github/scripts/build-push/validate.sh diff --git a/.github/scripts/build-push/check-existing-image.sh b/.github/scripts/build-push/check-existing-image.sh new file mode 100755 index 0000000..7bc3be0 --- /dev/null +++ b/.github/scripts/build-push/check-existing-image.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Checks if an image with the given tag already exists in ECR. +# Expected env vars: REPO_NAME, GITHUB_SHA +# Writes to GITHUB_OUTPUT: exists=true|false + +if aws ecr describe-images --repository-name "$REPO_NAME" \ + --image-ids imageTag="${GITHUB_SHA}" 2>/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Image ${GITHUB_SHA} already exists in ECR — skipping build" +else + echo "exists=false" >> "$GITHUB_OUTPUT" +fi diff --git a/.github/scripts/build-push/ensure-ecr-repo.sh b/.github/scripts/build-push/ensure-ecr-repo.sh new file mode 100755 index 0000000..736cb43 --- /dev/null +++ b/.github/scripts/build-push/ensure-ecr-repo.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates the ECR repository if it doesn't exist. +# Expected env vars: REPO_NAME, SERVICE_NAME, ENVIRONMENT + +aws ecr describe-repositories --repository-names "$REPO_NAME" 2>/dev/null && exit 0 + +echo "ECR repo '$REPO_NAME' not found — creating..." +aws ecr create-repository \ + --repository-name "$REPO_NAME" \ + --image-scanning-configuration scanOnPush=true \ + --encryption-configuration encryptionType=AES256 \ + --tags Key=Service,Value="${SERVICE_NAME}" Key=Environment,Value="${ENVIRONMENT}" + +echo "Created ECR repo: $REPO_NAME" diff --git a/.github/scripts/build-push/notify-clickup.sh b/.github/scripts/build-push/notify-clickup.sh new file mode 100755 index 0000000..ae73aea --- /dev/null +++ b/.github/scripts/build-push/notify-clickup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sends a ClickUp notification for build success or failure. +# Expected env vars: CLICKUP_TOKEN, CLICKUP_WORKSPACE_ID, CLICKUP_CHANNEL_ID, +# SERVICE_NAME, ENVIRONMENT, GITHUB_REF_NAME, GITHUB_ACTOR, RUN_URL +# Arg: $1 = "success" or "failure" +# Optional env: SHORT_SHA (only for success) + +[ -z "${CLICKUP_TOKEN:-}" ] && exit 0 + +STATUS="${1:-success}" + +if [ "$STATUS" = "success" ]; then + BODY=$(jq -n \ + --arg svc "$SERVICE_NAME" \ + --arg env "$ENVIRONMENT" \ + --arg sha "${SHORT_SHA:-}" \ + --arg branch "$GITHUB_REF_NAME" \ + --arg actor "$GITHUB_ACTOR" \ + --arg url "$RUN_URL" \ + '{type: "message", content_format: "text/md", + content: "Built and pushed **\($svc)** (\($env))\nImage tags: `\($sha)`, `latest`\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') +else + BODY=$(jq -n \ + --arg svc "$SERVICE_NAME" \ + --arg env "$ENVIRONMENT" \ + --arg branch "$GITHUB_REF_NAME" \ + --arg actor "$GITHUB_ACTOR" \ + --arg url "$RUN_URL" \ + '{type: "message", content_format: "text/md", + content: "FAILED build **\($svc)** (\($env))\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') +fi + +curl -sf -X POST \ + "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ + -H "Authorization: ${CLICKUP_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$BODY" || echo "::warning::ClickUp ${STATUS} notification failed" diff --git a/.github/scripts/build-push/set-metadata.sh b/.github/scripts/build-push/set-metadata.sh new file mode 100755 index 0000000..702352d --- /dev/null +++ b/.github/scripts/build-push/set-metadata.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Computes image metadata and writes to GITHUB_OUTPUT. +# Expected env vars: AWS_ACCOUNT_ID, AWS_REGION, SERVICE_NAME, ENVIRONMENT, +# INPUT_DOCKERFILE, GITHUB_SHA + +ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${SERVICE_NAME}-${ENVIRONMENT}" +DOCKERFILE="${INPUT_DOCKERFILE:-build/Dockerfile-${ENVIRONMENT}}" + +echo "ecr-repo=${ECR_REPO}" >> "$GITHUB_OUTPUT" +echo "image-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" +echo "short-sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" +echo "image-uri=${ECR_REPO}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" +echo "dockerfile=${DOCKERFILE}" >> "$GITHUB_OUTPUT" + +echo "ECR repo: ${ECR_REPO}" +echo "Tag: ${GITHUB_SHA:0:7} (${GITHUB_SHA})" +echo "Dockerfile: ${DOCKERFILE}" diff --git a/.github/scripts/build-push/validate.sh b/.github/scripts/build-push/validate.sh new file mode 100755 index 0000000..cd1440d --- /dev/null +++ b/.github/scripts/build-push/validate.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validates inputs for the build-push-ecr workflow. +# Expected env vars: SERVICE_NAME, ENVIRONMENT, AWS_ACCOUNT_ID, INPUT_DOCKERFILE, INPUT_CONTEXT + +if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then + echo "::error::Invalid service-name: must match ^[a-z][a-z0-9-]+$" + exit 1 +fi + +if [[ ! "$ENVIRONMENT" =~ ^(development|staging|production)$ ]]; then + echo "::error::Invalid environment: must be development, staging, or production" + exit 1 +fi + +if [[ ! "$AWS_ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then + echo "::error::Invalid aws-account-id: must be exactly 12 digits" + exit 1 +fi + +if [ -n "${INPUT_DOCKERFILE:-}" ] && [[ ! "$INPUT_DOCKERFILE" =~ ^[a-zA-Z0-9_./-]+$ ]]; then + echo "::error::Invalid dockerfile path" + exit 1 +fi + +if [[ ! "${INPUT_CONTEXT:-.}" =~ ^[a-zA-Z0-9_./-]+$ ]]; then + echo "::error::Invalid context path" + exit 1 +fi + +echo "All inputs valid" diff --git a/.github/workflows/build-push-ecr.yaml b/.github/workflows/build-push-ecr.yaml index ed5274a..fcd4017 100644 --- a/.github/workflows/build-push-ecr.yaml +++ b/.github/workflows/build-push-ecr.yaml @@ -6,7 +6,7 @@ # - Single-service repos (default) # - Mono-repos with multiple services (set service-name + dockerfile) # - Workers from the same repo (separate workflow call per worker) -# - Private npm packages via BuildKit secret mount +# - Private npm packages via SDK_TOKEN build-arg # - Git submodules via COMMON_TOKEN # # Usage — single service: @@ -143,52 +143,31 @@ jobs: AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} steps: + - name: Checkout .github repo (for scripts) + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: habuildserver/.github + path: .github-repo + sparse-checkout: .github/scripts/build-push + - name: Validate inputs env: INPUT_DOCKERFILE: ${{ inputs.dockerfile }} INPUT_CONTEXT: ${{ inputs.context }} - run: | - set -euo pipefail - if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then - echo "::error::Invalid service-name: must match ^[a-z][a-z0-9-]+$" - exit 1 - fi - if [[ ! "$ENVIRONMENT" =~ ^(development|staging|production)$ ]]; then - echo "::error::Invalid environment: must be development, staging, or production" - exit 1 - fi - if [[ ! "$AWS_ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then - echo "::error::Invalid aws-account-id: must be exactly 12 digits" - exit 1 - fi - if [ -n "$INPUT_DOCKERFILE" ] && [[ ! "$INPUT_DOCKERFILE" =~ ^[a-zA-Z0-9_./-]+$ ]]; then - echo "::error::Invalid dockerfile path" - exit 1 - fi - if [[ ! "$INPUT_CONTEXT" =~ ^[a-zA-Z0-9_./-]+$ ]]; then - echo "::error::Invalid context path" - exit 1 - fi + run: .github-repo/.github/scripts/build-push/validate.sh - - name: Checkout + - name: Checkout service repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ secrets.COMMON_TOKEN || github.token }} submodules: ${{ inputs.use-submodules && 'recursive' || 'false' }} + path: service - name: Set image metadata id: meta env: INPUT_DOCKERFILE: ${{ inputs.dockerfile }} - run: | - set -euo pipefail - ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${SERVICE_NAME}-${ENVIRONMENT}" - DOCKERFILE="${INPUT_DOCKERFILE:-build/Dockerfile-${ENVIRONMENT}}" - echo "ecr-repo=${ECR_REPO}" >> "$GITHUB_OUTPUT" - echo "image-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - echo "short-sha=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" - echo "image-uri=${ECR_REPO}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - echo "dockerfile=${DOCKERFILE}" >> "$GITHUB_OUTPUT" + run: .github-repo/.github/scripts/build-push/set-metadata.sh - name: Configure AWS Credentials (OIDC) uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 @@ -197,34 +176,18 @@ jobs: aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR - id: ecr-login uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 - name: Ensure ECR repository exists env: REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} - run: | - set -euo pipefail - aws ecr describe-repositories --repository-names "$REPO_NAME" 2>/dev/null || \ - aws ecr create-repository \ - --repository-name "$REPO_NAME" \ - --image-scanning-configuration scanOnPush=true \ - --encryption-configuration encryptionType=AES256 \ - --tags Key=Service,Value="${SERVICE_NAME}" Key=Environment,Value="${ENVIRONMENT}" + run: .github-repo/.github/scripts/build-push/ensure-ecr-repo.sh - name: Check if image already exists id: check-existing env: REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} - run: | - set -euo pipefail - if aws ecr describe-images --repository-name "$REPO_NAME" \ - --image-ids imageTag="${GITHUB_SHA}" 2>/dev/null; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "Image ${GITHUB_SHA} already exists in ECR — skipping build" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi + run: .github-repo/.github/scripts/build-push/check-existing-image.sh - name: Configure npm for private packages if: steps.check-existing.outputs.exists != 'true' @@ -244,8 +207,8 @@ jobs: if: steps.check-existing.outputs.exists != 'true' uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c3bc4c94 # v6.18.0 with: - context: ${{ inputs.context }} - file: ${{ steps.meta.outputs.dockerfile }} + context: service/${{ inputs.context }} + file: service/${{ steps.meta.outputs.dockerfile }} push: true tags: | ${{ steps.meta.outputs.ecr-repo }}:${{ github.sha }} @@ -270,22 +233,7 @@ jobs: CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} SHORT_SHA: ${{ steps.meta.outputs.short-sha }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - [ -z "$CLICKUP_TOKEN" ] && exit 0 - BODY=$(jq -n \ - --arg svc "$SERVICE_NAME" \ - --arg env "$ENVIRONMENT" \ - --arg sha "$SHORT_SHA" \ - --arg branch "$GITHUB_REF_NAME" \ - --arg actor "$GITHUB_ACTOR" \ - --arg url "$RUN_URL" \ - '{type: "message", content_format: "text/md", - content: "Built and pushed **\($svc)** (\($env))\nImage tags: `\($sha)`, `latest`\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') - curl -sf -X POST \ - "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ - -H "Authorization: ${CLICKUP_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$BODY" || echo "::warning::ClickUp notification failed (HTTP error)" + run: .github-repo/.github/scripts/build-push/notify-clickup.sh success - name: Notify ClickUp (failure) if: failure() && inputs.notify-clickup @@ -294,18 +242,4 @@ jobs: CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - [ -z "$CLICKUP_TOKEN" ] && exit 0 - BODY=$(jq -n \ - --arg svc "$SERVICE_NAME" \ - --arg env "$ENVIRONMENT" \ - --arg branch "$GITHUB_REF_NAME" \ - --arg actor "$GITHUB_ACTOR" \ - --arg url "$RUN_URL" \ - '{type: "message", content_format: "text/md", - content: "FAILED build **\($svc)** (\($env))\nBranch: **\($branch)**\nTriggered by: **\($actor)**\n[View run](\($url))"}') - curl -sf -X POST \ - "https://api.clickup.com/api/v3/workspaces/${CLICKUP_WORKSPACE_ID}/chat/channels/${CLICKUP_CHANNEL_ID}/messages" \ - -H "Authorization: ${CLICKUP_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$BODY" || echo "::warning::ClickUp failure notification failed" + run: .github-repo/.github/scripts/build-push/notify-clickup.sh failure From bb50104b26a17fb5d94bfed45df721430eaabe6c Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:18:49 +0530 Subject: [PATCH 4/8] refactor: convert reusable workflow to composite action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the reusable workflow (workflow_call) with a composite action. Scripts are now accessible via $GITHUB_ACTION_PATH — no need to checkout the .github repo separately. Composite actions run inside the caller's job (same runner), so outputs and files are shared naturally. Callers can add steps after the build (e.g., ECS task definition update). Structure: .github/actions/build-push-ecr/ action.yml # inputs, outputs, steps scripts/ validate.sh # input validation set-metadata.sh # ECR repo URI, tags ensure-ecr-repo.sh # auto-create ECR repo check-existing-image.sh # skip if SHA already in ECR notify-clickup.sh # success/failure via jq Caller syntax changes from: uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main to: uses: habuildserver/.github/.github/actions/build-push-ecr@main --- .github/actions/build-push-ecr/action.yml | 189 ++++++++++++++ .../scripts}/check-existing-image.sh | 0 .../scripts}/ensure-ecr-repo.sh | 0 .../build-push-ecr/scripts}/notify-clickup.sh | 0 .../build-push-ecr/scripts}/set-metadata.sh | 0 .../build-push-ecr/scripts}/validate.sh | 2 +- .github/workflows/build-push-ecr.yaml | 245 ------------------ 7 files changed, 190 insertions(+), 246 deletions(-) create mode 100644 .github/actions/build-push-ecr/action.yml rename .github/{scripts/build-push => actions/build-push-ecr/scripts}/check-existing-image.sh (100%) rename .github/{scripts/build-push => actions/build-push-ecr/scripts}/ensure-ecr-repo.sh (100%) rename .github/{scripts/build-push => actions/build-push-ecr/scripts}/notify-clickup.sh (100%) rename .github/{scripts/build-push => actions/build-push-ecr/scripts}/set-metadata.sh (100%) rename .github/{scripts/build-push => actions/build-push-ecr/scripts}/validate.sh (94%) delete mode 100644 .github/workflows/build-push-ecr.yaml diff --git a/.github/actions/build-push-ecr/action.yml b/.github/actions/build-push-ecr/action.yml new file mode 100644 index 0000000..c015b6f --- /dev/null +++ b/.github/actions/build-push-ecr/action.yml @@ -0,0 +1,189 @@ +# Composite action — builds a Docker image and pushes to ECR. +# Tags with git SHA (for K8s/ArgoCD) and `latest` (for ECS backwards compat). +# Uses BuildKit with GHA cache for fast, cached builds. +# +# Usage: +# +# - uses: habuildserver/.github/.github/actions/build-push-ecr@main +# with: +# service-name: chat-service +# environment: development +# aws-account-id: '963127282571' +# sdk-token: ${{ secrets.SDK_TOKEN }} +# +# Rollback (ECS): Re-run for the desired commit, or re-tag a previous SHA as latest. +# Rollback (K8s): Revert the gitops manifest to the previous SHA tag. + +name: Build & Push to ECR +description: Builds a Docker image, tags with git SHA + latest, pushes to ECR + +inputs: + service-name: + description: 'ECR repository name (e.g., chat-service)' + required: true + environment: + description: 'Target environment (development, staging, production)' + required: true + default: 'development' + dockerfile: + description: 'Path to Dockerfile (default: build/Dockerfile-)' + required: false + context: + description: 'Docker build context (default: .)' + required: false + default: '.' + aws-region: + description: 'AWS region' + required: false + default: 'ap-south-1' + aws-account-id: + description: 'AWS account ID for ECR (differs per environment)' + required: true + use-submodules: + description: 'Checkout with recursive submodules (true/false)' + required: false + default: 'false' + extra-build-args: + description: 'Additional docker build args (one per line, KEY=value format)' + required: false + default: '' + notify-clickup: + description: 'Send deploy notification to ClickUp (true/false)' + required: false + default: 'true' + # Secrets passed as inputs (GitHub still masks them in logs) + sdk-token: + description: 'GitHub Packages token for private npm' + required: false + common-token: + description: 'PAT for submodule checkout' + required: false + clickup-token: + description: 'ClickUp API token' + required: false + clickup-workspace-id: + description: 'ClickUp workspace ID' + required: false + clickup-channel-id: + description: 'ClickUp channel ID for notifications' + required: false + +outputs: + image-tag: + description: 'The full git SHA image tag that was pushed' + value: ${{ steps.meta.outputs.image-tag }} + image-uri: + description: 'Full image URI with SHA tag' + value: ${{ steps.meta.outputs.image-uri }} + short-sha: + description: 'Short (7-char) SHA tag' + value: ${{ steps.meta.outputs.short-sha }} + ecr-repo: + description: 'Full ECR repository URI (without tag)' + value: ${{ steps.meta.outputs.ecr-repo }} + +runs: + using: composite + steps: + - name: Validate inputs + shell: bash + env: + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} + INPUT_CONTEXT: ${{ inputs.context }} + run: ${{ github.action_path }}/scripts/validate.sh + + - name: Set image metadata + id: meta + shell: bash + env: + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} + AWS_REGION: ${{ inputs.aws-region }} + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} + run: ${{ github.action_path }}/scripts/set-metadata.sh + + - name: Configure AWS Credentials (OIDC) + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/github-actions-oidc-role + aws-region: ${{ inputs.aws-region }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + + - name: Ensure ECR repository exists + shell: bash + env: + REPO_NAME: ${{ inputs.service-name }}-${{ inputs.environment }} + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + run: ${{ github.action_path }}/scripts/ensure-ecr-repo.sh + + - name: Check if image already exists + id: check-existing + shell: bash + env: + REPO_NAME: ${{ inputs.service-name }}-${{ inputs.environment }} + run: ${{ github.action_path }}/scripts/check-existing-image.sh + + - name: Configure npm for private packages + if: steps.check-existing.outputs.exists != 'true' && inputs.sdk-token != '' + shell: bash + run: | + echo "@habuildserver:registry=https://npm.pkg.github.com/" >> ~/.npmrc + echo "//npm.pkg.github.com/:_authToken=${{ inputs.sdk-token }}" >> ~/.npmrc + + - name: Set up Docker Buildx + if: steps.check-existing.outputs.exists != 'true' + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Build and push + if: steps.check-existing.outputs.exists != 'true' + uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c3bc4c94 # v6.18.0 + with: + context: ${{ inputs.context }} + file: ${{ steps.meta.outputs.dockerfile }} + push: true + tags: | + ${{ steps.meta.outputs.ecr-repo }}:${{ github.sha }} + ${{ steps.meta.outputs.ecr-repo }}:${{ steps.meta.outputs.short-sha }} + ${{ steps.meta.outputs.ecr-repo }}:latest + build-args: | + SDK_TOKEN=${{ inputs.sdk-token }} + ${{ inputs.extra-build-args }} + cache-from: type=gha + cache-to: type=gha,mode=max + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + com.habuild.build.run-id=${{ github.run_id }} + com.habuild.build.run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + - name: Notify ClickUp (success) + if: success() && inputs.notify-clickup == 'true' + shell: bash + env: + CLICKUP_TOKEN: ${{ inputs.clickup-token }} + CLICKUP_WORKSPACE_ID: ${{ inputs.clickup-workspace-id }} + CLICKUP_CHANNEL_ID: ${{ inputs.clickup-channel-id }} + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + SHORT_SHA: ${{ steps.meta.outputs.short-sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: ${{ github.action_path }}/scripts/notify-clickup.sh success + + - name: Notify ClickUp (failure) + if: failure() && inputs.notify-clickup == 'true' + shell: bash + env: + CLICKUP_TOKEN: ${{ inputs.clickup-token }} + CLICKUP_WORKSPACE_ID: ${{ inputs.clickup-workspace-id }} + CLICKUP_CHANNEL_ID: ${{ inputs.clickup-channel-id }} + SERVICE_NAME: ${{ inputs.service-name }} + ENVIRONMENT: ${{ inputs.environment }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: ${{ github.action_path }}/scripts/notify-clickup.sh failure diff --git a/.github/scripts/build-push/check-existing-image.sh b/.github/actions/build-push-ecr/scripts/check-existing-image.sh similarity index 100% rename from .github/scripts/build-push/check-existing-image.sh rename to .github/actions/build-push-ecr/scripts/check-existing-image.sh diff --git a/.github/scripts/build-push/ensure-ecr-repo.sh b/.github/actions/build-push-ecr/scripts/ensure-ecr-repo.sh similarity index 100% rename from .github/scripts/build-push/ensure-ecr-repo.sh rename to .github/actions/build-push-ecr/scripts/ensure-ecr-repo.sh diff --git a/.github/scripts/build-push/notify-clickup.sh b/.github/actions/build-push-ecr/scripts/notify-clickup.sh similarity index 100% rename from .github/scripts/build-push/notify-clickup.sh rename to .github/actions/build-push-ecr/scripts/notify-clickup.sh diff --git a/.github/scripts/build-push/set-metadata.sh b/.github/actions/build-push-ecr/scripts/set-metadata.sh similarity index 100% rename from .github/scripts/build-push/set-metadata.sh rename to .github/actions/build-push-ecr/scripts/set-metadata.sh diff --git a/.github/scripts/build-push/validate.sh b/.github/actions/build-push-ecr/scripts/validate.sh similarity index 94% rename from .github/scripts/build-push/validate.sh rename to .github/actions/build-push-ecr/scripts/validate.sh index cd1440d..6b4cabb 100755 --- a/.github/scripts/build-push/validate.sh +++ b/.github/actions/build-push-ecr/scripts/validate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -# Validates inputs for the build-push-ecr workflow. +# Validates inputs for the build-push-ecr action. # Expected env vars: SERVICE_NAME, ENVIRONMENT, AWS_ACCOUNT_ID, INPUT_DOCKERFILE, INPUT_CONTEXT if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then diff --git a/.github/workflows/build-push-ecr.yaml b/.github/workflows/build-push-ecr.yaml deleted file mode 100644 index fcd4017..0000000 --- a/.github/workflows/build-push-ecr.yaml +++ /dev/null @@ -1,245 +0,0 @@ -# Reusable workflow — builds a Docker image and pushes to ECR. -# Tags with git SHA (for K8s/ArgoCD) and `latest` (for ECS backwards compat). -# Uses BuildKit with GHA cache for fast, cached builds. -# -# Supports: -# - Single-service repos (default) -# - Mono-repos with multiple services (set service-name + dockerfile) -# - Workers from the same repo (separate workflow call per worker) -# - Private npm packages via SDK_TOKEN build-arg -# - Git submodules via COMMON_TOKEN -# -# Usage — single service: -# -# jobs: -# build: -# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main -# with: -# service-name: chat-service -# environment: development -# aws-account-id: '963127282571' -# secrets: inherit -# -# Usage — mono-repo with multiple services: -# -# jobs: -# build-orchestration: -# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main -# with: -# service-name: orchestration-service -# environment: development -# aws-account-id: '963127282571' -# dockerfile: orchestration-service/build/Dockerfile -# context: . -# secrets: inherit -# -# Usage — worker from same repo: -# -# jobs: -# build-worker: -# uses: habuildserver/.github/.github/workflows/build-push-ecr.yaml@main -# with: -# service-name: chat-worker -# environment: development -# aws-account-id: '963127282571' -# dockerfile: build/worker/Dockerfile-development -# secrets: inherit -# -# Rollback (ECS): -# Re-run the workflow for the desired commit, or re-tag a previous SHA image as latest. -# Rollback (K8s/ArgoCD): -# Revert the gitops manifest to the previous SHA tag. ArgoCD self-heals. - -name: Reusable - Build & Push to ECR - -on: - workflow_call: - inputs: - service-name: - description: 'ECR repository name (e.g., chat-service). Used as: .dkr.ecr..amazonaws.com/-' - required: true - type: string - environment: - description: 'Target environment (development, staging, production)' - required: true - type: string - default: 'development' - dockerfile: - description: 'Path to Dockerfile (default: build/Dockerfile-)' - required: false - type: string - context: - description: 'Docker build context (default: .)' - required: false - type: string - default: '.' - aws-region: - description: 'AWS region' - required: false - type: string - default: 'ap-south-1' - aws-account-id: - description: 'AWS account ID for ECR (differs per environment)' - required: true - type: string - use-submodules: - description: 'Checkout with recursive submodules' - required: false - type: boolean - default: false - extra-build-args: - description: 'Additional docker build args (one per line, KEY=value format)' - required: false - type: string - default: '' - notify-clickup: - description: 'Send deploy notification to ClickUp' - required: false - type: boolean - default: true - outputs: - image-tag: - description: 'The full git SHA image tag that was pushed' - value: ${{ jobs.build-push.outputs.image-tag }} - image-uri: - description: 'Full image URI with SHA tag' - value: ${{ jobs.build-push.outputs.image-uri }} - secrets: - SDK_TOKEN: - required: false - COMMON_TOKEN: - required: false - CLICKUP_TOKEN: - required: false - CLICKUP_WORKSPACE_ID: - required: false - CLICKUP_CHANNEL_ID: - required: false - -permissions: {} - -jobs: - build-push: - name: Build & Push ${{ inputs.service-name }} - runs-on: ubuntu-latest - timeout-minutes: 30 - - concurrency: - group: ecr-push-${{ inputs.service-name }}-${{ inputs.environment }}-${{ github.ref }} - cancel-in-progress: true - - permissions: - contents: read - id-token: write - - outputs: - image-tag: ${{ steps.meta.outputs.image-tag }} - image-uri: ${{ steps.meta.outputs.image-uri }} - - env: - SERVICE_NAME: ${{ inputs.service-name }} - ENVIRONMENT: ${{ inputs.environment }} - AWS_REGION: ${{ inputs.aws-region }} - AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} - - steps: - - name: Checkout .github repo (for scripts) - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - repository: habuildserver/.github - path: .github-repo - sparse-checkout: .github/scripts/build-push - - - name: Validate inputs - env: - INPUT_DOCKERFILE: ${{ inputs.dockerfile }} - INPUT_CONTEXT: ${{ inputs.context }} - run: .github-repo/.github/scripts/build-push/validate.sh - - - name: Checkout service repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - token: ${{ secrets.COMMON_TOKEN || github.token }} - submodules: ${{ inputs.use-submodules && 'recursive' || 'false' }} - path: service - - - name: Set image metadata - id: meta - env: - INPUT_DOCKERFILE: ${{ inputs.dockerfile }} - run: .github-repo/.github/scripts/build-push/set-metadata.sh - - - name: Configure AWS Credentials (OIDC) - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 - with: - role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-oidc-role - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 - - - name: Ensure ECR repository exists - env: - REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} - run: .github-repo/.github/scripts/build-push/ensure-ecr-repo.sh - - - name: Check if image already exists - id: check-existing - env: - REPO_NAME: ${{ format('{0}-{1}', inputs.service-name, inputs.environment) }} - run: .github-repo/.github/scripts/build-push/check-existing-image.sh - - - name: Configure npm for private packages - if: steps.check-existing.outputs.exists != 'true' - env: - SDK_TOKEN: ${{ secrets.SDK_TOKEN }} - run: | - if [ -n "${SDK_TOKEN}" ]; then - echo "@habuildserver:registry=https://npm.pkg.github.com/" >> ~/.npmrc - echo "//npm.pkg.github.com/:_authToken=${SDK_TOKEN}" >> ~/.npmrc - fi - - - name: Set up Docker Buildx - if: steps.check-existing.outputs.exists != 'true' - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Build and push - if: steps.check-existing.outputs.exists != 'true' - uses: docker/build-push-action@263435318d21b8e681c14492fe198e19c3bc4c94 # v6.18.0 - with: - context: service/${{ inputs.context }} - file: service/${{ steps.meta.outputs.dockerfile }} - push: true - tags: | - ${{ steps.meta.outputs.ecr-repo }}:${{ github.sha }} - ${{ steps.meta.outputs.ecr-repo }}:${{ steps.meta.outputs.short-sha }} - ${{ steps.meta.outputs.ecr-repo }}:latest - build-args: | - SDK_TOKEN=${{ secrets.SDK_TOKEN }} - ${{ inputs.extra-build-args }} - cache-from: type=gha - cache-to: type=gha,mode=max - labels: | - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} - com.habuild.build.run-id=${{ github.run_id }} - com.habuild.build.run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - - - name: Notify ClickUp (success) - if: success() && inputs.notify-clickup - env: - CLICKUP_TOKEN: ${{ secrets.CLICKUP_TOKEN }} - CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} - CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} - SHORT_SHA: ${{ steps.meta.outputs.short-sha }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: .github-repo/.github/scripts/build-push/notify-clickup.sh success - - - name: Notify ClickUp (failure) - if: failure() && inputs.notify-clickup - env: - CLICKUP_TOKEN: ${{ secrets.CLICKUP_TOKEN }} - CLICKUP_WORKSPACE_ID: ${{ secrets.CLICKUP_WORKSPACE_ID }} - CLICKUP_CHANNEL_ID: ${{ secrets.CLICKUP_CHANNEL_ID }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: .github-repo/.github/scripts/build-push/notify-clickup.sh failure From 00bb72f9c779c86df90f689994dab90d50c825fe Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:30:56 +0530 Subject: [PATCH 5/8] refactor: rename INPUT_DOCKERFILE/INPUT_CONTEXT to DOCKERFILE_PATH/CONTEXT_PATH --- .github/actions/build-push-ecr/action.yml | 6 +- .../build-push-ecr/scripts/set-metadata.sh | 4 +- .../build-push-ecr/scripts/validate.sh | 6 +- claude/github-actions-review/analysis.md | 75 +++++++++++++++++++ claude/github-actions-review/plan.md | 61 +++++++++++++++ 5 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 claude/github-actions-review/analysis.md create mode 100644 claude/github-actions-review/plan.md diff --git a/.github/actions/build-push-ecr/action.yml b/.github/actions/build-push-ecr/action.yml index c015b6f..28ae504 100644 --- a/.github/actions/build-push-ecr/action.yml +++ b/.github/actions/build-push-ecr/action.yml @@ -91,8 +91,8 @@ runs: SERVICE_NAME: ${{ inputs.service-name }} ENVIRONMENT: ${{ inputs.environment }} AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} - INPUT_DOCKERFILE: ${{ inputs.dockerfile }} - INPUT_CONTEXT: ${{ inputs.context }} + DOCKERFILE_PATH: ${{ inputs.dockerfile }} + CONTEXT_PATH: ${{ inputs.context }} run: ${{ github.action_path }}/scripts/validate.sh - name: Set image metadata @@ -103,7 +103,7 @@ runs: ENVIRONMENT: ${{ inputs.environment }} AWS_ACCOUNT_ID: ${{ inputs.aws-account-id }} AWS_REGION: ${{ inputs.aws-region }} - INPUT_DOCKERFILE: ${{ inputs.dockerfile }} + DOCKERFILE_PATH: ${{ inputs.dockerfile }} run: ${{ github.action_path }}/scripts/set-metadata.sh - name: Configure AWS Credentials (OIDC) diff --git a/.github/actions/build-push-ecr/scripts/set-metadata.sh b/.github/actions/build-push-ecr/scripts/set-metadata.sh index 702352d..f527db1 100755 --- a/.github/actions/build-push-ecr/scripts/set-metadata.sh +++ b/.github/actions/build-push-ecr/scripts/set-metadata.sh @@ -3,10 +3,10 @@ set -euo pipefail # Computes image metadata and writes to GITHUB_OUTPUT. # Expected env vars: AWS_ACCOUNT_ID, AWS_REGION, SERVICE_NAME, ENVIRONMENT, -# INPUT_DOCKERFILE, GITHUB_SHA +# DOCKERFILE_PATH, GITHUB_SHA ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${SERVICE_NAME}-${ENVIRONMENT}" -DOCKERFILE="${INPUT_DOCKERFILE:-build/Dockerfile-${ENVIRONMENT}}" +DOCKERFILE="${DOCKERFILE_PATH:-build/Dockerfile-${ENVIRONMENT}}" echo "ecr-repo=${ECR_REPO}" >> "$GITHUB_OUTPUT" echo "image-tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/build-push-ecr/scripts/validate.sh b/.github/actions/build-push-ecr/scripts/validate.sh index 6b4cabb..5ee855d 100755 --- a/.github/actions/build-push-ecr/scripts/validate.sh +++ b/.github/actions/build-push-ecr/scripts/validate.sh @@ -2,7 +2,7 @@ set -euo pipefail # Validates inputs for the build-push-ecr action. -# Expected env vars: SERVICE_NAME, ENVIRONMENT, AWS_ACCOUNT_ID, INPUT_DOCKERFILE, INPUT_CONTEXT +# Expected env vars: SERVICE_NAME, ENVIRONMENT, AWS_ACCOUNT_ID, DOCKERFILE_PATH, CONTEXT_PATH if [[ ! "$SERVICE_NAME" =~ ^[a-z][a-z0-9-]+$ ]]; then echo "::error::Invalid service-name: must match ^[a-z][a-z0-9-]+$" @@ -19,12 +19,12 @@ if [[ ! "$AWS_ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then exit 1 fi -if [ -n "${INPUT_DOCKERFILE:-}" ] && [[ ! "$INPUT_DOCKERFILE" =~ ^[a-zA-Z0-9_./-]+$ ]]; then +if [ -n "${DOCKERFILE_PATH:-}" ] && [[ ! "$DOCKERFILE_PATH" =~ ^[a-zA-Z0-9_./-]+$ ]]; then echo "::error::Invalid dockerfile path" exit 1 fi -if [[ ! "${INPUT_CONTEXT:-.}" =~ ^[a-zA-Z0-9_./-]+$ ]]; then +if [[ ! "${CONTEXT_PATH:-.}" =~ ^[a-zA-Z0-9_./-]+$ ]]; then echo "::error::Invalid context path" exit 1 fi diff --git a/claude/github-actions-review/analysis.md b/claude/github-actions-review/analysis.md new file mode 100644 index 0000000..b04fd72 --- /dev/null +++ b/claude/github-actions-review/analysis.md @@ -0,0 +1,75 @@ +# GitHub Actions Review + +**Repository:** habuildserver/.github +**Date:** 2026-03-11 +**Branch:** feat/centralize-terraform-workflows +**Workflows reviewed:** 6 + +## Workflow Inventory + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `alpha-release.yaml` | `workflow_call` | Create alpha pre-release from PR branch | +| `release-preview.yaml` | `workflow_call` | Dry-run semantic-release and comment preview on PR | +| `release-version.yaml` | `workflow_call` | Run validation then create stable release on main push | +| `terraform-validate.yaml` | `workflow_call` | Terraform fmt, validate, tflint, test, checkov | +| `pr-title-check.yaml` | `workflow_call` | Enforce conventional commit PR titles | +| `development-pr-checks.yml` | `workflow_call` | Node.js lint, test, coverage (not owned by user) | + +## Findings + +### Must Fix + +1. **Node.js 20 deprecation across all workflows** — `actions/checkout@v4`, `actions/setup-node@v4`, `hashicorp/setup-terraform@v3`, `terraform-linters/setup-tflint@v4` all run on Node.js 20, deprecated June 2, 2026. GitHub will force Node.js 24 after that date. Upgrade to `@v5` where available (`actions/checkout@v5` exists) and monitor others. + +2. **CI check gate has a race condition** — The new "Verify CI checks passed" step in `alpha-release.yaml` queries check runs on the HEAD SHA. If CI is triggered by the same push that the `/alpha-release` comment targets, the check runs may still be `in_progress` or `queued` — the gate would see them as "not passed" and fail. This is actually safe (fails closed), but the error message could be confusing. Checks with `null` conclusion (in-progress) would be caught by the `!= "success"` filter, which is correct behavior. + +3. **Semantic-release version inline in release-version.yaml differs in format** — `release-version.yaml` uses `cycjimmy/semantic-release-action@v4` with `semantic_version: 24.2.0`, while `alpha-release.yaml` and `release-preview.yaml` use `npx --package semantic-release@24.2.0`. The versions match (24.2.0), but the execution paths differ — the action bundles its own runner vs. npx downloading fresh. This could produce different behavior if the action version drifts. + +### Should Fix + +4. **No concurrency control on any workflow** — All 6 workflows lack `concurrency` groups. Since they're all `workflow_call`, concurrency is inherited from the caller. However, callers may also lack concurrency. Recommend documenting that callers MUST set concurrency, or add it here as a defensive measure. Particularly important for `alpha-release.yaml` — two simultaneous `/alpha-release` comments could race on tag creation. + +5. **`mshick/add-pr-comment@v2` is a third-party action used without version pinning** — Used in both `alpha-release.yaml` (line 166) and `release-preview.yaml` (line 102). Major version tag `@v2` is the minimum acceptable level, but for third-party actions, SHA pinning is more secure. At minimum, pin to a specific version tag. + +6. **`actions/github-script@v6` in development-pr-checks.yml is outdated** — v7 is current. (Note: this file is not owned by user, flagging for awareness only.) + +7. **`pr-title-check.yaml` uses specific version `@v6.1.1`** — Good practice for third-party action, but inconsistent with other actions using major version tags. Choose one pinning strategy across the repo. + +### Nice to Have + +8. **Alpha release PR comment still references `vendor.yaml`** — Line 178 says "Update `vendor.yaml` or catalog source version". Since infra-stacks switched to JIT source provisioning, the comment should reference only catalog/stack source version. + +9. **No `README.md` documenting the reusable workflows** — Consumers need to know inputs, permissions required, and expected caller setup. + +10. **`development-pr-checks.yml` has a debug step** — Line 53: `ls -R src/app/common || echo "No common folder!"` should be removed for production use. (Not owned by user.) + +## Checklist Results + +| Check | Status | Notes | +|-------|--------|-------| +| Version consistency | Pass | semantic-release 24.2.0 across all workflows, plugins match | +| Plugin alignment | Pass | Same 3 plugins in alpha-release, release-preview, release-version | +| Permissions | Pass | Job-level permissions, least-privilege applied | +| Concurrency control | Warn | No concurrency groups — relies on callers to set them | +| Path filters | N/A | All `workflow_call` — callers control triggers | +| Reusable workflows | Pass | Good DRY pattern — validation reused in release-version | +| Action pinning | Warn | Mix of major tag (`@v4`) and specific version (`@v6.1.1`) | +| Secrets handling | Pass | `GITHUB_TOKEN` via env vars, no hardcoded secrets | +| Node.js 20 deprecation | Fail | All actions on Node.js 20, deadline June 2, 2026 | +| CI gate on alpha release | Pass | New check verifies all PR checks pass before release | + +## Grade: B + +**Justification:** +- Good reusable workflow architecture with `workflow_call` pattern +- Consistent semantic-release version and plugin set +- Proper least-privilege permissions at job level +- `persist-credentials: false` applied to all checkouts +- CI gate on alpha release is a solid addition +- Loses points for Node.js 20 deprecation, missing concurrency, and inconsistent action pinning + +**What would move to next grade:** +1. Upgrade actions to Node.js 24 compatible versions +2. Add concurrency groups (or document caller requirement) +3. Standardize action version pinning strategy diff --git a/claude/github-actions-review/plan.md b/claude/github-actions-review/plan.md new file mode 100644 index 0000000..e159547 --- /dev/null +++ b/claude/github-actions-review/plan.md @@ -0,0 +1,61 @@ +# Implementation Plan + +**Based on:** analysis.md +**Target Grade:** B -> A + +## Steps + +### Step 1: Upgrade actions to Node.js 24 compatible versions +**Severity:** Must Fix +**Files affected:** +- `.github/workflows/alpha-release.yaml` +- `.github/workflows/release-preview.yaml` +- `.github/workflows/release-version.yaml` +- `.github/workflows/terraform-validate.yaml` +- `.github/workflows/pr-title-check.yaml` + +**Actions:** +1. `actions/checkout@v4` -> `@v5` (all 4 workflows that use it) +2. `actions/setup-node@v4` -> `@v5` (alpha-release, release-preview) +3. `hashicorp/setup-terraform@v3` -> check if v4 exists, otherwise keep v3 +4. `terraform-linters/setup-tflint@v4` -> check if v5 exists, otherwise keep v4 +5. `bridgecrewio/checkov-action@v12` -> verify Node.js 24 compatibility +6. Update `NODE_VERSION` env from `20.11.0` to `22.x` or `24.x` + +**Verification:** +- [x] All actions use Node.js 24 compatible versions +- [x] NODE_VERSION env updated to 22 + +--- + +### Step 2: Update alpha release PR comment to reflect JIT provisioning +**Severity:** Nice to Have +**Files affected:** +- `.github/workflows/alpha-release.yaml` + +**Actions:** +1. Replace `vendor.yaml` reference with stack source version reference +2. Update the test command to use `atmos terraform plan` directly (JIT auto-provisions) + +**Verification:** +- [x] Comment template no longer mentions vendor.yaml + +--- + +### Step 3: Pin third-party actions to specific version tags +**Severity:** Should Fix +**Files affected:** +- `.github/workflows/alpha-release.yaml` +- `.github/workflows/release-preview.yaml` + +**Actions:** +1. Pin `mshick/add-pr-comment@v2` to latest specific version tag +2. Keep `amannn/action-semantic-pull-request@v6.1.1` as-is (already pinned) +3. Keep `cycjimmy/semantic-release-action@v4` at major tag (well-known action) + +**Verification:** +- [x] Third-party actions pinned to specific version tags (mshick/add-pr-comment@v2.8.2) + +## Notes +- `development-pr-checks.yml` is excluded from this plan (not owned by user) +- Concurrency is best handled in caller workflows since these are all `workflow_call` — recommend adding concurrency guidance to README or CLAUDE.md From 1214453feeb5ac6dc2e661ba4c672452a75d893a Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:31:06 +0530 Subject: [PATCH 6/8] fix: remove accidentally committed claude analysis files --- claude/github-actions-review/analysis.md | 75 ------------------------ claude/github-actions-review/plan.md | 61 ------------------- 2 files changed, 136 deletions(-) delete mode 100644 claude/github-actions-review/analysis.md delete mode 100644 claude/github-actions-review/plan.md diff --git a/claude/github-actions-review/analysis.md b/claude/github-actions-review/analysis.md deleted file mode 100644 index b04fd72..0000000 --- a/claude/github-actions-review/analysis.md +++ /dev/null @@ -1,75 +0,0 @@ -# GitHub Actions Review - -**Repository:** habuildserver/.github -**Date:** 2026-03-11 -**Branch:** feat/centralize-terraform-workflows -**Workflows reviewed:** 6 - -## Workflow Inventory - -| Workflow | Trigger | Purpose | -|----------|---------|---------| -| `alpha-release.yaml` | `workflow_call` | Create alpha pre-release from PR branch | -| `release-preview.yaml` | `workflow_call` | Dry-run semantic-release and comment preview on PR | -| `release-version.yaml` | `workflow_call` | Run validation then create stable release on main push | -| `terraform-validate.yaml` | `workflow_call` | Terraform fmt, validate, tflint, test, checkov | -| `pr-title-check.yaml` | `workflow_call` | Enforce conventional commit PR titles | -| `development-pr-checks.yml` | `workflow_call` | Node.js lint, test, coverage (not owned by user) | - -## Findings - -### Must Fix - -1. **Node.js 20 deprecation across all workflows** — `actions/checkout@v4`, `actions/setup-node@v4`, `hashicorp/setup-terraform@v3`, `terraform-linters/setup-tflint@v4` all run on Node.js 20, deprecated June 2, 2026. GitHub will force Node.js 24 after that date. Upgrade to `@v5` where available (`actions/checkout@v5` exists) and monitor others. - -2. **CI check gate has a race condition** — The new "Verify CI checks passed" step in `alpha-release.yaml` queries check runs on the HEAD SHA. If CI is triggered by the same push that the `/alpha-release` comment targets, the check runs may still be `in_progress` or `queued` — the gate would see them as "not passed" and fail. This is actually safe (fails closed), but the error message could be confusing. Checks with `null` conclusion (in-progress) would be caught by the `!= "success"` filter, which is correct behavior. - -3. **Semantic-release version inline in release-version.yaml differs in format** — `release-version.yaml` uses `cycjimmy/semantic-release-action@v4` with `semantic_version: 24.2.0`, while `alpha-release.yaml` and `release-preview.yaml` use `npx --package semantic-release@24.2.0`. The versions match (24.2.0), but the execution paths differ — the action bundles its own runner vs. npx downloading fresh. This could produce different behavior if the action version drifts. - -### Should Fix - -4. **No concurrency control on any workflow** — All 6 workflows lack `concurrency` groups. Since they're all `workflow_call`, concurrency is inherited from the caller. However, callers may also lack concurrency. Recommend documenting that callers MUST set concurrency, or add it here as a defensive measure. Particularly important for `alpha-release.yaml` — two simultaneous `/alpha-release` comments could race on tag creation. - -5. **`mshick/add-pr-comment@v2` is a third-party action used without version pinning** — Used in both `alpha-release.yaml` (line 166) and `release-preview.yaml` (line 102). Major version tag `@v2` is the minimum acceptable level, but for third-party actions, SHA pinning is more secure. At minimum, pin to a specific version tag. - -6. **`actions/github-script@v6` in development-pr-checks.yml is outdated** — v7 is current. (Note: this file is not owned by user, flagging for awareness only.) - -7. **`pr-title-check.yaml` uses specific version `@v6.1.1`** — Good practice for third-party action, but inconsistent with other actions using major version tags. Choose one pinning strategy across the repo. - -### Nice to Have - -8. **Alpha release PR comment still references `vendor.yaml`** — Line 178 says "Update `vendor.yaml` or catalog source version". Since infra-stacks switched to JIT source provisioning, the comment should reference only catalog/stack source version. - -9. **No `README.md` documenting the reusable workflows** — Consumers need to know inputs, permissions required, and expected caller setup. - -10. **`development-pr-checks.yml` has a debug step** — Line 53: `ls -R src/app/common || echo "No common folder!"` should be removed for production use. (Not owned by user.) - -## Checklist Results - -| Check | Status | Notes | -|-------|--------|-------| -| Version consistency | Pass | semantic-release 24.2.0 across all workflows, plugins match | -| Plugin alignment | Pass | Same 3 plugins in alpha-release, release-preview, release-version | -| Permissions | Pass | Job-level permissions, least-privilege applied | -| Concurrency control | Warn | No concurrency groups — relies on callers to set them | -| Path filters | N/A | All `workflow_call` — callers control triggers | -| Reusable workflows | Pass | Good DRY pattern — validation reused in release-version | -| Action pinning | Warn | Mix of major tag (`@v4`) and specific version (`@v6.1.1`) | -| Secrets handling | Pass | `GITHUB_TOKEN` via env vars, no hardcoded secrets | -| Node.js 20 deprecation | Fail | All actions on Node.js 20, deadline June 2, 2026 | -| CI gate on alpha release | Pass | New check verifies all PR checks pass before release | - -## Grade: B - -**Justification:** -- Good reusable workflow architecture with `workflow_call` pattern -- Consistent semantic-release version and plugin set -- Proper least-privilege permissions at job level -- `persist-credentials: false` applied to all checkouts -- CI gate on alpha release is a solid addition -- Loses points for Node.js 20 deprecation, missing concurrency, and inconsistent action pinning - -**What would move to next grade:** -1. Upgrade actions to Node.js 24 compatible versions -2. Add concurrency groups (or document caller requirement) -3. Standardize action version pinning strategy diff --git a/claude/github-actions-review/plan.md b/claude/github-actions-review/plan.md deleted file mode 100644 index e159547..0000000 --- a/claude/github-actions-review/plan.md +++ /dev/null @@ -1,61 +0,0 @@ -# Implementation Plan - -**Based on:** analysis.md -**Target Grade:** B -> A - -## Steps - -### Step 1: Upgrade actions to Node.js 24 compatible versions -**Severity:** Must Fix -**Files affected:** -- `.github/workflows/alpha-release.yaml` -- `.github/workflows/release-preview.yaml` -- `.github/workflows/release-version.yaml` -- `.github/workflows/terraform-validate.yaml` -- `.github/workflows/pr-title-check.yaml` - -**Actions:** -1. `actions/checkout@v4` -> `@v5` (all 4 workflows that use it) -2. `actions/setup-node@v4` -> `@v5` (alpha-release, release-preview) -3. `hashicorp/setup-terraform@v3` -> check if v4 exists, otherwise keep v3 -4. `terraform-linters/setup-tflint@v4` -> check if v5 exists, otherwise keep v4 -5. `bridgecrewio/checkov-action@v12` -> verify Node.js 24 compatibility -6. Update `NODE_VERSION` env from `20.11.0` to `22.x` or `24.x` - -**Verification:** -- [x] All actions use Node.js 24 compatible versions -- [x] NODE_VERSION env updated to 22 - ---- - -### Step 2: Update alpha release PR comment to reflect JIT provisioning -**Severity:** Nice to Have -**Files affected:** -- `.github/workflows/alpha-release.yaml` - -**Actions:** -1. Replace `vendor.yaml` reference with stack source version reference -2. Update the test command to use `atmos terraform plan` directly (JIT auto-provisions) - -**Verification:** -- [x] Comment template no longer mentions vendor.yaml - ---- - -### Step 3: Pin third-party actions to specific version tags -**Severity:** Should Fix -**Files affected:** -- `.github/workflows/alpha-release.yaml` -- `.github/workflows/release-preview.yaml` - -**Actions:** -1. Pin `mshick/add-pr-comment@v2` to latest specific version tag -2. Keep `amannn/action-semantic-pull-request@v6.1.1` as-is (already pinned) -3. Keep `cycjimmy/semantic-release-action@v4` at major tag (well-known action) - -**Verification:** -- [x] Third-party actions pinned to specific version tags (mshick/add-pr-comment@v2.8.2) - -## Notes -- `development-pr-checks.yml` is excluded from this plan (not owned by user) -- Concurrency is best handled in caller workflows since these are all `workflow_call` — recommend adding concurrency guidance to README or CLAUDE.md From 1b8911af08c906c2175003390d15a47a0994c3f5 Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Fri, 17 Apr 2026 01:33:17 +0530 Subject: [PATCH 7/8] docs: add README with usage examples for build-push-ecr action --- .github/actions/build-push-ecr/README.md | 155 +++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .github/actions/build-push-ecr/README.md diff --git a/.github/actions/build-push-ecr/README.md b/.github/actions/build-push-ecr/README.md new file mode 100644 index 0000000..3f591e3 --- /dev/null +++ b/.github/actions/build-push-ecr/README.md @@ -0,0 +1,155 @@ +# Build & Push to ECR + +Composite action that builds a Docker image and pushes to Amazon ECR. Tags with git SHA (for K8s/ArgoCD) and `latest` (for ECS). + +## Features + +- BuildKit with GHA cache for fast builds +- Auto-creates ECR repo if it doesn't exist (with scan-on-push) +- Skips build if SHA tag already exists in ECR +- Atomic push (all tags in one operation) +- ClickUp notifications on success and failure +- Input validation (service name, environment, account ID, file paths) +- OCI labels for build traceability +- All third-party actions pinned to SHA + +## Usage + +### Single service + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + id: build + with: + service-name: chat-service + environment: development + aws-account-id: '963127282571' + sdk-token: ${{ secrets.SDK_TOKEN }} + + - run: echo "Pushed ${{ steps.build.outputs.image-uri }}" +``` + +### Mono-repo (multiple services) + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + with: + service-name: orchestration-service + environment: development + aws-account-id: '963127282571' + dockerfile: orchestration-service/build/Dockerfile + context: . + + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + with: + service-name: provider-service + environment: development + aws-account-id: '963127282571' + dockerfile: provider-service/build/Dockerfile + context: . +``` + +### Worker from same repo + +```yaml + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + with: + service-name: chat-worker + environment: development + aws-account-id: '963127282571' + dockerfile: build/worker/Dockerfile-development +``` + +### With submodules and ClickUp notifications + +```yaml + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + with: + service-name: user-service + environment: development + aws-account-id: '963127282571' + sdk-token: ${{ secrets.SDK_TOKEN }} + common-token: ${{ secrets.COMMON_TOKEN }} + use-submodules: 'true' + clickup-token: ${{ secrets.CLICKUP_TOKEN }} + clickup-workspace-id: ${{ secrets.CLICKUP_WORKSPACE_ID }} + clickup-channel-id: ${{ secrets.CLICKUP_CHANNEL_ID }} +``` + +### With extra build args + +```yaml + - uses: habuildserver/.github/.github/actions/build-push-ecr@main + with: + service-name: chat-service + environment: development + aws-account-id: '963127282571' + extra-build-args: | + NODE_ENV=production + BUILD_DATE=2026-04-17 +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `service-name` | Yes | | ECR repo name (e.g., `chat-service`) | +| `environment` | Yes | `development` | `development`, `staging`, or `production` | +| `aws-account-id` | Yes | | 12-digit AWS account ID | +| `dockerfile` | No | `build/Dockerfile-` | Path to Dockerfile | +| `context` | No | `.` | Docker build context | +| `aws-region` | No | `ap-south-1` | AWS region | +| `use-submodules` | No | `false` | Checkout with recursive submodules | +| `extra-build-args` | No | | Additional build args (one per line, `KEY=value`) | +| `notify-clickup` | No | `true` | Send ClickUp notifications | +| `sdk-token` | No | | GitHub Packages token for private npm | +| `common-token` | No | | PAT for submodule checkout | +| `clickup-token` | No | | ClickUp API token | +| `clickup-workspace-id` | No | | ClickUp workspace ID | +| `clickup-channel-id` | No | | ClickUp channel ID | + +## Outputs + +| Output | Description | Example | +|--------|-------------|---------| +| `image-tag` | Full git SHA | `abc123def456...` | +| `image-uri` | Full image URI with SHA tag | `963127282571.dkr.ecr.ap-south-1.amazonaws.com/chat-service-development:abc123...` | +| `short-sha` | 7-char SHA | `abc123d` | +| `ecr-repo` | ECR repo URI (without tag) | `963127282571.dkr.ecr.ap-south-1.amazonaws.com/chat-service-development` | + +## ECR naming convention + +Repos are named `-`: +- `chat-service-development` +- `chat-service-staging` +- `chat-service-production` + +## Rollback + +**ECS:** Re-run the workflow for the desired commit, or re-tag a previous SHA image as `latest`. + +**K8s/ArgoCD:** Revert the gitops manifest to the previous SHA tag. ArgoCD self-heals automatically. + +## Prerequisites + +- AWS OIDC role `github-actions-oidc-role` configured in the target account +- Caller job must have `permissions: { contents: read, id-token: write }` +- `jq` available on runner (pre-installed on `ubuntu-latest`) From c556e704973ee56cfdf6569c6e9c5a624df519c7 Mon Sep 17 00:00:00 2001 From: Ashwini Manoj Date: Sun, 19 Apr 2026 12:53:45 +0530 Subject: [PATCH 8/8] docs(build-push-ecr): add service image requirements section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composite action builds whatever Dockerfile it's handed — it cannot verify the resulting image is actually K8s-deployable. Add a concise section naming the four baseline constraints services must meet: 1. No env secrets baked into image layers (.dockerignore gate) 2. Runs cleanly with readOnlyRootFilesystem: true 3. No build-time migrations or integration tests 4. SHA tags are authoritative; :latest is ECS-compat only Points at the tactical runbook in habuild-k8s-gitops for the Dockerfile / .dockerignore templates and the strategic plan doc in infra-plans for the rollout sequencing. Motivated by the chat-service rollout on 2026-04-19 which surfaced all three build-time/runtime conflicts when the existing ECS-era Dockerfile (shipping .env.* into /app via COPY . ., running prisma migrate at build time) met the archetype's readOnlyRootFilesystem + CSI subPath invariants. Capturing the constraints in the build-action README so the next team onboarding discovers them before a failed sync, not during it. --- .github/actions/build-push-ecr/README.md | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/actions/build-push-ecr/README.md b/.github/actions/build-push-ecr/README.md index 3f591e3..b2cd5a7 100644 --- a/.github/actions/build-push-ecr/README.md +++ b/.github/actions/build-push-ecr/README.md @@ -148,6 +148,41 @@ Repos are named `-`: **K8s/ArgoCD:** Revert the gitops manifest to the previous SHA tag. ArgoCD self-heals automatically. +## Service image requirements + +The action builds whatever Dockerfile you hand it — it cannot enforce that +the resulting image is actually deployable to K8s. The archetype chart +(`charts/services/web-service/` in the gitops repo) imposes constraints +that your Dockerfile must honour. If it doesn't, the image builds +successfully but crashes at pod start with opaque runtime errors +(OCI mount failures, `EACCES` on scratch writes, etc.). + +Baseline constraints: + +1. **No env secrets in image layers.** Add a `.dockerignore` excluding + `.env*` (and typically `.git/`, tests, editor configs). `COPY . .` + without a `.dockerignore` will ship dev secrets to every environment. +2. **Runs with a read-only rootfs.** Scratch space must come from an + explicit volume (`/tmp` emptyDir in the lib chart), not from writing + to `/app/` or `/var/`. Test locally with + `docker run --read-only --tmpfs /tmp `. +3. **No build-time migrations or integration tests.** `prisma migrate + deploy` belongs in a K8s `Job` at deploy time, not in a Dockerfile + `RUN`. Tests belong in CI before this action runs. +4. **No `:latest`-only behaviour.** K8s consumers read the SHA tag + (`image-tag` / `image-uri` outputs). `:latest` is retained for + ECS backward compat during migration only. + +Full tactical detail — `.dockerignore` template, Dockerfile patches, +verification checklist before enabling CSI file mount — lives in the +gitops repo: + +> [`habuild-k8s-gitops/docs/runbooks/csi-file-mount-image-requirements.md`](https://github.com/habuildserver/habuild-k8s-gitops/blob/main/docs/runbooks/csi-file-mount-image-requirements.md) + +The strategic context (why these standards exist, enforcement-point +mapping, per-service migration sequencing) is in the infra-plans Phase +4 doc on [service image standards](https://github.com/habuildserver/infra-plans/blob/main/phases/04-full-migration/service-image-standards.html). + ## Prerequisites - AWS OIDC role `github-actions-oidc-role` configured in the target account