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
feat: Add CI/CD scripts for Google Cloud deployment
  • Loading branch information
taiphanvan2k3 committed Nov 14, 2025
commit 8eed01f6a2c0e7c04edebf0fcdc0c589824adf19
156 changes: 156 additions & 0 deletions .github/workflows/deploy-gcloud.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Deploy to Google Cloud Artifact Registry

on:
push:
branches:
- main
- develop
paths:
- 'src/**'
- 'appsettings*.json'
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/deploy-gcloud.yml'
pull_request:
branches:
- main
paths:
- 'src/**'
- 'appsettings*.json'
- 'Dockerfile'
- 'docker-compose.yml'
workflow_dispatch:
inputs:
version_tag:
description: 'Custom version tag (optional, will use SHA if empty)'
required: false
type: string

env:
PROJECT_ID: lucky-union-472503-c7
REGION: asia-southeast1
REPOSITORY: backendnetcore
IMAGE_NAME: webapi
SERVICE_NAME: legal-assistant-api

jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate version tag
id: version
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.version_tag }}" ]; then
VERSION="${{ inputs.version_tag }}"
elif [ "${{ github.ref_type }}" == "tag" ]; then
VERSION="${{ github.ref_name }}"
else
# Use date + short SHA for immutable versioning
VERSION="$(date +%Y%m%d)-${GITHUB_SHA::7}"
fi
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "Generated version: ${VERSION}"

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for Artifact Registry
run: |
gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

- name: Build Docker image
run: |
docker build -t ${{ env.IMAGE_NAME }}:latest -f src/Web.Api/Dockerfile .

- name: Tag Docker image
run: |
REMOTE_IMAGE="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}"
REMOTE_IMAGE_LATEST="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:latest"

docker tag ${{ env.IMAGE_NAME }}:latest ${REMOTE_IMAGE}
docker tag ${{ env.IMAGE_NAME }}:latest ${REMOTE_IMAGE_LATEST}

echo "REMOTE_IMAGE=${REMOTE_IMAGE}" >> $GITHUB_ENV
echo "REMOTE_IMAGE_LATEST=${REMOTE_IMAGE_LATEST}" >> $GITHUB_ENV

- name: Push Docker image to Artifact Registry
run: |
docker push ${{ env.REMOTE_IMAGE }}
docker push ${{ env.REMOTE_IMAGE_LATEST }}

- name: Deploy to Cloud Run (Production)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
gcloud run deploy ${{ env.SERVICE_NAME }} \
--image ${{ env.REMOTE_IMAGE }} \
--platform managed \
--region ${{ env.REGION }} \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--max-instances 10 \
--min-instances 0 \
--port 8080 \
--set-env-vars "ASPNETCORE_ENVIRONMENT=Production" \
--quiet

- name: Deploy to Cloud Run (Staging)
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
run: |
gcloud run deploy ${{ env.SERVICE_NAME }}-staging \
--image ${{ env.REMOTE_IMAGE }} \
--platform managed \
--region ${{ env.REGION }} \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--max-instances 5 \
--min-instances 0 \
--port 8080 \
--set-env-vars "ASPNETCORE_ENVIRONMENT=Staging" \
--quiet

- name: Output deployment info
run: |
echo "=== Deployment Complete ==="
echo "Version: ${{ steps.version.outputs.VERSION }}"
echo "Image: ${{ env.REMOTE_IMAGE }}"
echo ""
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} --region ${{ env.REGION }} --format 'value(status.url)')
echo "Production URL: ${SERVICE_URL}"
elif [ "${{ github.ref }}" == "refs/heads/develop" ]; then
SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }}-staging --region ${{ env.REGION }} --format 'value(status.url)')
echo "Staging URL: ${SERVICE_URL}"
fi

- name: Create deployment summary
run: |
echo "## Deployment Summary 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: \`${{ steps.version.outputs.VERSION }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: \`${{ env.REMOTE_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Branch**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY
elif [ "${{ github.ref }}" == "refs/heads/develop" ]; then
echo "- **Environment**: Staging" >> $GITHUB_STEP_SUMMARY
else
echo "- **Environment**: Image built only (no deployment)" >> $GITHUB_STEP_SUMMARY
fi
5 changes: 5 additions & 0 deletions scripts/ci-cd-deploy-gcloud.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
gcloud projects add-iam-policy-binding lucky-union-472503-c7 --member="serviceAccount:ci-cd-fucntions-hosting@lucky-union-472503-c7.iam.gserviceaccount.com" --role="roles/run.admin"

# Tạo key cho service account
gcloud iam service-accounts keys create github-key.json --iam-account=ci-cd-fucntions-hosting@lucky-union-472503-c7.iam.gserviceaccount.com
Comment on lines +1 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Service account key will be exposed if not properly excluded from version control.

Creating github-key.json directly in the repository root is a severe security risk. If this file is not strictly excluded from version control, it will expose GCP credentials in the repository history.

  1. Verify that github-key.json is listed in .gitignore and add it if missing.
  2. Consider whether this script should run as a one-time setup (outside CI/CD) rather than in every workflow execution.
  3. If this must run in CI/CD, use a temporary directory and ensure the key is only stored as a GitHub secret (e.g., base64-encoded in secrets.GCP_SA_KEY), never written to disk.
+#!/bin/bash
+set -euo pipefail
+
 gcloud projects add-iam-policy-binding lucky-union-472503-c7 --member="serviceAccount:ci-cd-fucntions-hosting@lucky-union-472503-c7.iam.gserviceaccount.com" --role="roles/run.admin"
 
-# Tạo key cho service account
+# Create key for service account
-gcloud iam service-accounts keys create github-key.json --iam-account=ci-cd-fucntions-hosting@lucky-union-472503-c7.iam.gserviceaccount.com
+gcloud iam service-accounts keys create /tmp/github-key.json --iam-account=ci-cd-fucntions-hosting@lucky-union-472503-c7.iam.gserviceaccount.com
+echo "Key created at /tmp/github-key.json"

🏁 Script executed:

#!/bin/bash
# Check for .gitignore and verify github-key.json is excluded
if [ -f ".gitignore" ]; then
  echo "=== .gitignore contents ==="
  cat .gitignore
else
  echo ".gitignore does not exist"
fi

Length of output: 6924


🏁 Script executed:

#!/bin/bash
# Search for service account references with both spellings
echo "=== Searching for 'ci-cd-fucntions-hosting' (typo version) ==="
rg -n "ci-cd-fucntions-hosting" --type-list | head -20

echo ""
echo "=== Searching for 'ci-cd-functions-hosting' (correct spelling) ==="
rg -n "ci-cd-functions-hosting" --type-list | head -20

Length of output: 910


🏁 Script executed:

#!/bin/bash
# Search for github-key.json references throughout codebase
echo "=== Searching for github-key.json references ==="
rg -n "github-key\.json" -C2

echo ""
echo "=== Searching for /tmp/github-key references ==="
rg -n "tmp.*github-key" -C2

Length of output: 368


🏁 Script executed:

#!/bin/bash
# Examine the full script and look for CI/CD workflow files
echo "=== Full content of scripts/ci-cd-deploy-gcloud.sh ==="
cat scripts/ci-cd-deploy-gcloud.sh

echo ""
echo "=== Searching for workflow files that might call this script ==="
fd -e yaml -e yml . .github/workflows 2>/dev/null || echo "No .github/workflows directory found"

Length of output: 548


🏁 Script executed:

#!/bin/bash
# Check for any error handling patterns in other scripts
echo "=== Checking for set -e or set -o pipefail in shell scripts ==="
rg -n "set -[eo]" --type bash -C1 | head -30

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check workflow files for calls to the deploy script
echo "=== deploy-gcloud.yml content ==="
cat .github/workflows/deploy-gcloud.yml

echo ""
echo "=== build.yml content ==="
cat .github/workflows/build.yml

Length of output: 7577


🏁 Script executed:

#!/bin/bash
# Search for references to the ci-cd-deploy-gcloud.sh script
echo "=== Searching for ci-cd-deploy-gcloud.sh references ==="
rg -n "ci-cd-deploy-gcloud\.sh" -C3

echo ""
echo "=== Searching for github-key usage in workflows or scripts ==="
rg -n "github-key" -C2

Length of output: 389


🏁 Script executed:

#!/bin/bash
# Check if scripts contain error handling directly
echo "=== Checking scripts directory for error handling ==="
fd -e sh scripts/ -exec grep -l "set -e\|set -o pipefail\|trap.*ERR" {} \;

echo ""
echo "=== Full listing of scripts directory ==="
find scripts/ -type f -exec file {} \;

Length of output: 486


🏁 Script executed:

#!/bin/bash
# Check README or documentation for setup instructions
echo "=== Searching for README or setup documentation ==="
fd -i "readme|setup|install" . -type f | head -10

echo ""
echo "=== Searching for mentions of one-time setup or manual steps ==="
rg -i "setup|manual|one-time|initialize" README* 2>/dev/null || echo "No README found with these terms"

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Final verification: search for any documentation, comments, or issues mentioning this script
echo "=== Searching for documentation or comments about manual setup ==="
rg -i "deploy-gcloud|manual setup|one-time setup|initialize gcp" -C2 --max-count=10

echo ""
echo "=== Check git history for when script was added ==="
git log --follow --oneline scripts/ci-cd-deploy-gcloud.sh 2>/dev/null | head -5 || echo "Git history unavailable"

echo ""
echo "=== Check if README mentions this script ==="
cat README.md 2>/dev/null | grep -i "deploy-gcloud\|manual\|setup" | head -10 || echo "No matches in README"

Length of output: 280


Add github-key.json to .gitignore to prevent credential exposure.

The critical security issue is that github-key.json is not listed in .gitignore. If this script creates a credentials file in the repository root, it could be accidentally committed, exposing GCP service account credentials in version control history.

Add the following line to .gitignore:

github-key.json

Note: The service account name ci-cd-fucntions-hosting is the actual account in use (not a typo). However, the script also lacks error handling—add set -euo pipefail after the shebang to ensure the script fails gracefully if any command encounters an error, rather than silently continuing.

🤖 Prompt for AI Agents
In scripts/ci-cd-deploy-gcloud.sh around lines 1 to 5, the script writes a
credentials file (github-key.json) into the repo and lacks strict error
handling; add github-key.json to the repository .gitignore (repo root) to
prevent accidental commits of credentials, and modify this script to enable
strict failure mode by inserting set -euo pipefail immediately after the shebang
so the script exits on errors/unset variables and fails safely; do not change
the service account name.