diff --git a/.gitea/workflows/build_RC.yml b/.gitea/workflows/build_RC.yml new file mode 100644 index 00000000..1a0b4b7d --- /dev/null +++ b/.gitea/workflows/build_RC.yml @@ -0,0 +1,100 @@ +name: Build Baron SSO RC + +on: + workflow_dispatch: + inputs: + version_tag: + description: "The version tag to release to staging (e.g., v1.2601.1)" + required: true + type: string + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y jq curl + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.HARBOR_ENDPOINT }} + username: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + password: ${{ secrets.HARBOR_ROBOT_KEY }} + + - name: Calculate next RC tag + id: rc_calculator + env: + INPUT_TAG: ${{ github.event.inputs.version_tag }} + REGISTRY_URL: ${{ vars.HARBOR_ENDPOINT }} + HARBOR_USER: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_KEY }} + run: | + # Generate YYMM dynamically for the new tag + CURRENT_YYMM=$(date +'%y%m') + + # Reconstruct the base tag with the current YYMM + MAJOR_VERSION=$(echo "${INPUT_TAG}" | cut -d'.' -f1) + MINOR_VERSION=$(echo "${INPUT_TAG}" | cut -d'.' -f3) + BASE_TAG="${MAJOR_VERSION}.${CURRENT_YYMM}.${MINOR_VERSION}" + + echo "Input tag: ${INPUT_TAG}" + echo "Generated dynamic base tag: ${BASE_TAG}" + + # Using the backend repository as the source for RC version calculation + API_URL="${REGISTRY_URL}/api/v2.0/projects/baron_sso/repositories/backend/artifacts?sort=-creation_time&page_size=100" + + AUTH_HEADER=$(echo -n "${HARBOR_USER}:${HARBOR_PASSWORD}" | base64) + API_RESPONSE=$(curl -s -k -H "Authorization: Basic ${AUTH_HEADER}" "${API_URL}") + + # Define a search pattern to find RCs across different months for the same major/minor version + # e.g., matches v1.2508.1-RC, v1.2509.1-RC, etc. + SEARCH_PATTERN="^${MAJOR_VERSION}\.[0-9]{4}\.${MINOR_VERSION}-RC" + echo "Using search pattern: ${SEARCH_PATTERN}" + + # Disable pipefail for grep, as it will exit with 1 if no match is found + set +o pipefail + # Find the highest RC number regardless of the YYMM part + LATEST_RC_NUM=$(echo "${API_RESPONSE}" | jq -r '.[] | .tags[]? | .name' | grep -E "${SEARCH_PATTERN}" | sed 's/.*-RC//' | sort -rn | head -n 1) + set -o pipefail + + if [ -z "$LATEST_RC_NUM" ]; then + NEXT_RC_NUM=1 + else + NEXT_RC_NUM=$((LATEST_RC_NUM + 1)) + fi + + # Create the new tag using the dynamically generated BASE_TAG and the incremented RC number + NEW_RC_TAG="${BASE_TAG}-RC${NEXT_RC_NUM}" + echo "new_rc_tag=$NEW_RC_TAG" >> $GITHUB_OUTPUT + echo "Found latest RC number: ${LATEST_RC_NUM:-0}" + echo "Calculated new RC tag: $NEW_RC_TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push backend RC image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile.backend + push: true + tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend:${{ steps.rc_calculator.outputs.new_rc_tag }} + provenance: false + sbom: false + + - name: Temporarily update frontend nginx port + run: sed -i 's/listen 5000;/listen 80;/g' frontend/nginx.conf + + - name: Build and push frontend RC image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile.frontend + push: true + tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/frontend:${{ steps.rc_calculator.outputs.new_rc_tag }} + provenance: false + sbom: false diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml new file mode 100644 index 00000000..ee86b006 --- /dev/null +++ b/.gitea/workflows/code_check.yml @@ -0,0 +1,124 @@ +name: Code Check + +on: + workflow_dispatch: + inputs: + run_lint: + description: "Run linters for Go and Flutter" + required: true + type: boolean + default: true + run_backend_tests: + description: "Run backend Go tests" + required: true + type: boolean + default: true + run_frontend_tests: + description: "Run frontend Flutter tests" + required: true + type: boolean + default: true + +jobs: + lint: + if: ${{ inputs.run_lint == true }} + runs-on: ubuntu-latest + steps: + # 리포지토리에서 소스 코드를 체크아웃합니다. + - name: Checkout code + uses: actions/checkout@v4 + + # Go 언어 환경을 설정합니다. + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + # Go 백엔드 코드의 정적 분석을 수행합니다. + - name: Lint Go backend + uses: golangci/golangci-lint-action@v6 + with: + version: v1.59 + working-directory: backend + + # Flutter SDK 환경을 설정합니다. + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + # Flutter/Dart 프론트엔드 코드의 정적 분석을 수행합니다. + - name: Analyze Flutter frontend + run: | + cd frontend + flutter pub get + flutter analyze + + backend-tests: + needs: lint + if: ${{ inputs.run_backend_tests == true }} + runs-on: ubuntu-latest + services: + # 통합 테스트에 사용될 Redis 서비스 컨테이너입니다. + # 운영 환경과 일치하도록 포트를 6399로 설정합니다. + redis: + image: redis:7-alpine + command: redis-server --port 6399 + options: > + --health-cmd "redis-cli -p 6399 ping" --health-interval 10s --health-timeout 5s --health-retries 5 + ports: + - 6399:6399 + # 통합 테스트에 사용될 ClickHouse 서비스 컨테이너입니다. + clickhouse: + image: clickhouse/clickhouse-server:24.6 + ports: + - 9000:9000 + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8123/ping"] + interval: 10s + timeout: 5s + retries: 5 + + env: + REDIS_ADDR: localhost:6399 + CLICKHOUSE_HOST: localhost + CLICKHOUSE_PORT_NATIVE: 9000 + + steps: + # 리포지토리에서 소스 코드를 체크아웃합니다. + - name: Checkout code + uses: actions/checkout@v4 + + # Go 언어 환경을 설정합니다. + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + # 백엔드 디렉토리의 모든 Go 테스트를 실행합니다. + - name: Run backend tests + run: | + cd backend + go test -v ./... + + frontend-tests: + needs: lint + if: ${{ inputs.run_frontend_tests == true }} + runs-on: ubuntu-latest + steps: + # 리포지토리에서 소스 코드를 체크아웃합니다. + - name: Checkout code + uses: actions/checkout@v4 + + # Flutter SDK 환경을 설정합니다. + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + # 프론트엔드 디렉토리의 모든 위젯 테스트를 실행합니다. + - name: Run frontend tests + run: | + cd frontend + flutter pub get + flutter test diff --git a/.gitea/workflows/production_release.yml b/.gitea/workflows/production_release.yml new file mode 100644 index 00000000..47bcb6ec --- /dev/null +++ b/.gitea/workflows/production_release.yml @@ -0,0 +1,133 @@ +name: Release Baron SSO to Production + +on: + workflow_dispatch: + inputs: + rc_version_tag: + description: "The version tag to release to production (e.g., v1.2601.1-RC1)" + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to Harbor Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.HARBOR_ENDPOINT }} + username: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + password: ${{ secrets.HARBOR_ROBOT_KEY }} + + - name: Parse RC and re-tag to final + id: retag + env: + HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }} + HARBOR_USER: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_ROBOT_KEY }} + run: | + BASE_TAG=$(echo "${{ github.event.inputs.rc_version_tag }}" | xargs) + + if [[ ! "$BASE_TAG" =~ ^v[0-9]+\.[0-9]{4}\.[0-9]+-RC[0-9]+$ ]]; then + echo "::error::rc_version_tag must look like vX.YYMM.Z-RC## (got: $BASE_TAG)" + exit 1 + fi + RE_TAG="${BASE_TAG%-RC*}" + echo "Final tag will be: ${RE_TAG}" + + if ! command -v skopeo >/dev/null 2>&1; then + sudo apt-get update -y && sudo apt-get install -y skopeo + fi + + # Re-tag backend image + echo "Re-tagging backend image..." + skopeo copy --preserve-digests \ + --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ + --src-tls-verify=false --dest-tls-verify=false \ + "docker://${HARBOR_HOSTNAME}/baron_sso/backend:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/backend:${RE_TAG}" + + # Re-tag frontend image + echo "Re-tagging frontend image..." + skopeo copy --preserve-digests \ + --src-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" --dest-creds "${HARBOR_USER}:${HARBOR_PASSWORD}" \ + --src-tls-verify=false --dest-tls-verify=false \ + "docker://${HARBOR_HOSTNAME}/baron_sso/frontend:${BASE_TAG}" "docker://${HARBOR_HOSTNAME}/baron_sso/frontend:${RE_TAG}" + + echo "final_image_tag=${RE_TAG}" >> "$GITHUB_OUTPUT" + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }} + + - name: Deploy to Production + env: + IMAGE_TAG: ${{ steps.retag.outputs.final_image_tag }} + BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend + FRONTEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/frontend + DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} + PROD_HOST: ${{ vars.PROD_HOST }} + PROD_USER: ${{ vars.PROD_USER }} + HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }} + HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }} + HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }} + run: | + set -euo pipefail + ssh-keyscan -H "${PROD_HOST}" >> ~/.ssh/known_hosts + + ssh "${PROD_USER}@${PROD_HOST}" "mkdir -p '${DEPLOY_PATH}'" + + # Create the main .env file for Baron SSO on the remote server + # Note: All values are pulled from Gitea secrets and variables + printf '%s\n' \ + "APP_ENV=production" \ + "TZ=Asia/Seoul" \ + "DB_PORT=${{ vars.PROD_DB_PORT }}" \ + "CLICKHOUSE_PORT_HTTP=${{ vars.PROD_CLICKHOUSE_PORT_HTTP }}" \ + "CLICKHOUSE_PORT_NATIVE=${{ vars.PROD_CLICKHOUSE_PORT_NATIVE }}" \ + "CLICKHOUSE_USER=${{ vars.PROD_CLICKHOUSE_USER }}" \ + "CLICKHOUSE_PASSWORD=${{ secrets.PROD_CLICKHOUSE_PASSWORD }}" \ + "BACKEND_PORT=${{ vars.PROD_BACKEND_PORT }}" \ + "FRONTEND_PORT=${{ vars.PROD_FRONTEND_PORT }}" \ + "DB_USER=${{ vars.PROD_DB_USER }}" \ + "DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }}" \ + "DB_NAME=${{ vars.PROD_DB_NAME }}" \ + "COOKIE_SECRET=${{ secrets.PROD_COOKIE_SECRET }}" \ + "JWT_SECRET=${{ secrets.PROD_JWT_SECRET }}" \ + "REDIS_ADDR=${{ vars.PROD_REDIS_ADDR }}" \ + "DESCOPE_PROJECT_ID=${{ vars.DESCOPE_PROJECT_ID }}" \ + "DESCOPE_MANAGEMENT_KEY=${{ secrets.DESCOPE_MANAGEMENT_KEY }}" \ + "NAVER_CLOUD_ACCESS_KEY=${{ secrets.NAVER_CLOUD_ACCESS_KEY }}" \ + "NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}" \ + "NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}" \ + "NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}" \ + "AWS_REGION=${{ vars.AWS_REGION }}" \ + "AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" \ + "AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \ + "AWS_SES_SENDER=${{ vars.AWS_SES_SENDER }}" \ + "FRONTEND_URL=${{ vars.PROD_FRONTEND_URL }}" \ + "BACKEND_URL=${{ vars.PROD_BACKEND_URL }}" \ + > .env + + # Copy compose template and .env file to the remote server + scp docker/docker-compose.template.yaml .env "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/" + scp docker/compose.infra.prd.yaml "${PROD_USER}@${PROD_HOST}:${DEPLOY_PATH}/compose.infra.yml" + + # Deploy Baron SSO + echo "${HARBOR_ROBOT_KEY}" | ssh "${PROD_USER}@${PROD_HOST}" \ + "export DEPLOY_PATH='${DEPLOY_PATH}'; \ + export BACKEND_IMAGE_NAME='${BACKEND_IMAGE_NAME}'; \ + export FRONTEND_IMAGE_NAME='${FRONTEND_IMAGE_NAME}'; \ + export IMAGE_TAG='${IMAGE_TAG}'; \ + export HARBOR_ENDPOINT='${HARBOR_ENDPOINT}'; \ + export HARBOR_ROBOT_ACCOUNT='${HARBOR_ROBOT_ACCOUNT}'; \ + set -e; \ + cd \"\${DEPLOY_PATH}\"; \ + read HARBOR_ROBOT_KEY_FROM_STDIN; \ + echo \"\${HARBOR_ROBOT_KEY_FROM_STDIN}\" | docker login \"\${HARBOR_ENDPOINT}\" -u \"\${HARBOR_ROBOT_ACCOUNT}\" --password-stdin; \ + envsubst < docker-compose.template.yaml > docker-compose.yml; \ + docker compose -f compose.infra.yml -f docker-compose.yml pull; \ + docker compose -f compose.infra.yml -f docker-compose.yml up -d --remove-orphans"