diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml index 8c7df04..d995bee 100644 --- a/.github/workflows/deploy-api.yml +++ b/.github/workflows/deploy-api.yml @@ -16,6 +16,7 @@ on: env: REGISTRY_ALIAS: m8q5m4u3 REPOSITORY: mega/campsite-api + HARBOR_REGISTRY: registry.xuanwu.openatom.cn RUBY_VERSION: 3.3.4 NODE_VERSION: 18.16.1 BUNDLER_VERSION: 2.3.14 @@ -68,7 +69,14 @@ jobs: with: registry-type: public - - name: Build & push amd64 image (AWS) + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Build & push amd64 image (AWS + Harbor) env: AWS_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} @@ -76,6 +84,7 @@ jobs: set -euo pipefail AWS_IMAGE_BASE="$AWS_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_IMAGE_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" docker buildx build \ --platform linux/amd64 \ @@ -88,6 +97,9 @@ jobs: -t "$AWS_IMAGE_BASE:${IMAGE_TAG}" \ -t "$AWS_IMAGE_BASE:${IMAGE_TAG}-amd64" \ -t "$AWS_IMAGE_BASE:latest-amd64" \ + -t "$HARBOR_IMAGE_BASE:${IMAGE_TAG}" \ + -t "$HARBOR_IMAGE_BASE:${IMAGE_TAG}-amd64" \ + -t "$HARBOR_IMAGE_BASE:latest-amd64" \ --push ./api # Merge locally-built arm64 image (pushed separately) with CI-built amd64 @@ -111,6 +123,13 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - name: Create & push manifest lists env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} @@ -119,23 +138,40 @@ jobs: set -euo pipefail IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" - - docker manifest create "$IMAGE_BASE:latest" \ - "$IMAGE_BASE:latest-amd64" \ - "$IMAGE_BASE:latest-arm64" - docker manifest push "$IMAGE_BASE:latest" - echo "Pushed manifest: $IMAGE_BASE:latest" - - if docker manifest inspect "$IMAGE_BASE:${IMAGE_TAG}-arm64" >/dev/null 2>&1; then - docker buildx imagetools create \ - -t "$IMAGE_BASE:${IMAGE_TAG}" \ - "$IMAGE_BASE:${IMAGE_TAG}-amd64" \ - "$IMAGE_BASE:${IMAGE_TAG}-arm64" - echo "Pushed multi-arch manifest: $IMAGE_BASE:${IMAGE_TAG}" - else - echo "Using amd64-only tag: $IMAGE_BASE:${IMAGE_TAG}" - echo "Push arm64 locally to upgrade to multi-arch: ./script/demo/build-images-arm64-local.sh ${IMAGE_TAG} --push" - fi + HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" + + push_latest() { + local base="$1" + local refs=("$base:latest-amd64") + if docker manifest inspect "$base:latest-arm64" >/dev/null 2>&1; then + refs+=("$base:latest-arm64") + else + echo "WARN: $base:latest-arm64 not found; publishing amd64-only latest" + fi + docker manifest create "$base:latest" "${refs[@]}" + docker manifest push "$base:latest" + echo "Pushed manifest: $base:latest" + } + + push_tag_if_arm64() { + local base="$1" + if docker manifest inspect "$base:${IMAGE_TAG}-arm64" >/dev/null 2>&1; then + docker buildx imagetools create \ + -t "$base:${IMAGE_TAG}" \ + "$base:${IMAGE_TAG}-amd64" \ + "$base:${IMAGE_TAG}-arm64" + echo "Pushed multi-arch manifest: $base:${IMAGE_TAG}" + else + echo "Using amd64-only tag: $base:${IMAGE_TAG}" + echo "Push arm64 locally to upgrade to multi-arch: ./script/demo/build-images-arm64-local.sh ${IMAGE_TAG} --push" + fi + } + + push_latest "$IMAGE_BASE" + push_tag_if_arm64 "$IMAGE_BASE" + + push_latest "$HARBOR_BASE" + push_tag_if_arm64 "$HARBOR_BASE" deploy-aws: if: ${{ github.repository == 'web3infra-foundation/campsite' }} diff --git a/api/app/models/user.rb b/api/app/models/user.rb index f684f35..2db9840 100644 --- a/api/app/models/user.rb +++ b/api/app/models/user.rb @@ -160,6 +160,14 @@ def self.from_omniauth(access_token) found.confirm if found.skip_confirmation_notification! && found.save end + if access_token.provider.to_s == "github" + github_login = access_token.info.nickname.presence + if github_login.present? && found.github_login != github_login + found.github_login = github_login + found.save! if found.persisted? + end + end + ImportRemoteUserAvatarJob.perform_async(found.id) if found.avatar_path && found.persisted? found end diff --git a/api/app/models/user/null_user.rb b/api/app/models/user/null_user.rb index c03fb37..3c22b26 100644 --- a/api/app/models/user/null_user.rb +++ b/api/app/models/user/null_user.rb @@ -35,6 +35,10 @@ def username "" end + def github_login + nil + end + def onboarded_at "" end diff --git a/api/app/serializers/current_user_serializer.rb b/api/app/serializers/current_user_serializer.rb index a6d27c7..f626858 100644 --- a/api/app/serializers/current_user_serializer.rb +++ b/api/app/serializers/current_user_serializer.rb @@ -7,6 +7,7 @@ class CurrentUserSerializer < ApiSerializer api_field :cover_photo_url, nullable: true api_field :email api_field :username + api_field :github_login, nullable: true api_field :display_name api_field :onboarded_at, nullable: true diff --git a/api/app/serializers/sync_user_serializer.rb b/api/app/serializers/sync_user_serializer.rb index 478f0d5..83890c8 100644 --- a/api/app/serializers/sync_user_serializer.rb +++ b/api/app/serializers/sync_user_serializer.rb @@ -5,6 +5,7 @@ class SyncUserSerializer < ApiSerializer api_association :avatar_urls, blueprint: AvatarUrlsSerializer api_field :display_name api_field :username + api_field :github_login, nullable: true api_field :email api_field :integration?, name: :integration, type: :boolean api_field :notifications_paused?, name: :notifications_paused, type: :boolean diff --git a/api/app/serializers/user_serializer.rb b/api/app/serializers/user_serializer.rb index 46f0738..ad97cc8 100644 --- a/api/app/serializers/user_serializer.rb +++ b/api/app/serializers/user_serializer.rb @@ -7,6 +7,7 @@ class UserSerializer < ApiSerializer api_field :cover_photo_url, nullable: true api_field :email api_field :username + api_field :github_login, nullable: true api_field :display_name api_field :system?, name: :system, type: :boolean api_field :integration?, name: :integration, type: :boolean diff --git a/api/db/migrate/20260723023000_add_github_login_to_users.rb b/api/db/migrate/20260723023000_add_github_login_to_users.rb new file mode 100644 index 0000000..d31e1bf --- /dev/null +++ b/api/db/migrate/20260723023000_add_github_login_to_users.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddGithubLoginToUsers < ActiveRecord::Migration[7.2] + def change + add_column :users, :github_login, :string + add_index :users, :github_login, unique: true + end +end diff --git a/api/db/schema.rb b/api/db/schema.rb index 9a2f7a3..8b2f451 100644 --- a/api/db/schema.rb +++ b/api/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_01_29_075537) do +ActiveRecord::Schema[7.2].define(version: 2026_07_23_023000) do create_table "attachments", id: { type: :bigint, unsigned: true }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "public_id", limit: 12, null: false t.text "file_path", null: false @@ -1472,8 +1472,10 @@ t.datetime "notification_pause_expires_at" t.string "preferred_timezone" t.datetime "notifications_paused_at" + t.string "github_login" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true, length: 320 + t.index ["github_login"], name: "index_users_on_github_login", unique: true t.index ["login_token"], name: "index_users_on_login_token", unique: true t.index ["omniauth_provider", "omniauth_uid"], name: "index_users_on_omniauth_provider_and_omniauth_uid", unique: true t.index ["public_id"], name: "index_users_on_public_id", unique: true diff --git a/api/script/demo/build-images-arm64-local.sh b/api/script/demo/build-images-arm64-local.sh index 5a3ef66..b24cfe6 100755 --- a/api/script/demo/build-images-arm64-local.sh +++ b/api/script/demo/build-images-arm64-local.sh @@ -5,16 +5,25 @@ # # Usage: # ./script/demo/build-images-arm64-local.sh [IMAGE_TAG] [--push] -# --push Also push to remote registry; if omitted, image is only built and loaded locally. +# --push Also push to remote registries; if omitted, image is only built and loaded locally. +# +# Registries (both tagged when --push): +# - public.ecr.aws/m8q5m4u3/mega/campsite-api +# - registry.xuanwu.openatom.cn/mega/campsite-api # # Requirements: # - Docker with buildx enabled ("docker buildx create --use") -# - If using --push you must already be logged into the destination registry. +# - If using --push you must already be logged into the destination registries. set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +API_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +cd "$API_DIR" + REGISTRY_ALIAS="m8q5m4u3" # Public ECR alias -REGISTRY="public.ecr.aws/${REGISTRY_ALIAS}" +ECR_REGISTRY="public.ecr.aws/${REGISTRY_ALIAS}" +HARBOR_REGISTRY="${HARBOR_REGISTRY:-registry.xuanwu.openatom.cn}" REPOSITORY="mega/campsite-api" # ---------------------------------------- @@ -60,12 +69,16 @@ NODE_VERSION="${NODE_VERSION:-18.16.1}" BUNDLER_VERSION="${BUNDLER_VERSION:-2.3.14}" IMAGE_TAG="${IMAGE_TAG_BASE}-${ARCH_SUFFIX}" -IMAGE_NAME="${REGISTRY}/${REPOSITORY}:${IMAGE_TAG}" -LATEST_ARCH_TAG="${REGISTRY}/${REPOSITORY}:latest-${ARCH_SUFFIX}" -CACHE_IMAGE="${REGISTRY}/${REPOSITORY}:buildcache-${ARCH_SUFFIX}" +ECR_IMAGE="${ECR_REGISTRY}/${REPOSITORY}:${IMAGE_TAG}" +ECR_LATEST_ARCH="${ECR_REGISTRY}/${REPOSITORY}:latest-${ARCH_SUFFIX}" +HARBOR_IMAGE="${HARBOR_REGISTRY}/${REPOSITORY}:${IMAGE_TAG}" +HARBOR_LATEST_ARCH="${HARBOR_REGISTRY}/${REPOSITORY}:latest-${ARCH_SUFFIX}" +CACHE_IMAGE="${ECR_REGISTRY}/${REPOSITORY}:buildcache-${ARCH_SUFFIX}" CACHE_DIR="${BUILDX_CACHE_DIR:-${HOME}/.cache/campsite-api-buildx/${ARCH_SUFFIX}}" -echo "Building ${IMAGE_NAME} (${PLATFORM}) …" +echo "Building (${PLATFORM}) …" +echo " ECR: $ECR_IMAGE" +echo " Harbor: $HARBOR_IMAGE" # Ensure buildx is available if ! docker buildx version >/dev/null 2>&1; then @@ -78,13 +91,15 @@ BUILD_ARGS=( --platform "$PLATFORM" --provenance=false --sbom=false - -t "$IMAGE_NAME" - -t "$LATEST_ARCH_TAG" + -t "$ECR_IMAGE" + -t "$ECR_LATEST_ARCH" + -t "$HARBOR_IMAGE" + -t "$HARBOR_LATEST_ARCH" --build-arg "RUBY_VERSION=$RUBY_VERSION" --build-arg "NODE_VERSION=$NODE_VERSION" --build-arg "BUNDLER_VERSION=$BUNDLER_VERSION" --cache-from "type=registry,ref=${CACHE_IMAGE}" - --cache-from "type=registry,ref=${IMAGE_NAME}" + --cache-from "type=registry,ref=${ECR_IMAGE}" ) if $DO_PUSH; then @@ -100,12 +115,16 @@ if ! docker buildx build "${BUILD_ARGS[@]}" .; then exit 1 fi -echo "Image built successfully: $IMAGE_NAME" +echo "Image built successfully: $ECR_IMAGE / $HARBOR_IMAGE" if $DO_PUSH; then - echo "Image pushed successfully: $IMAGE_NAME" + echo "Image pushed successfully:" + echo " $ECR_IMAGE" + echo " $ECR_LATEST_ARCH" + echo " $HARBOR_IMAGE" + echo " $HARBOR_LATEST_ARCH" echo "Build cache saved to: ${CACHE_IMAGE}" else - echo "Image loaded locally: $IMAGE_NAME" + echo "Image loaded locally: $ECR_IMAGE" echo "Build cache saved to: ${CACHE_DIR}" fi diff --git a/api/vendor/cache/prism-1.4.0.gem b/api/vendor/cache/prism-1.4.0.gem deleted file mode 100644 index 005bf8e..0000000 Binary files a/api/vendor/cache/prism-1.4.0.gem and /dev/null differ diff --git a/api/vendor/cache/prism-1.9.0.gem b/api/vendor/cache/prism-1.9.0.gem new file mode 100644 index 0000000..45fb871 Binary files /dev/null and b/api/vendor/cache/prism-1.9.0.gem differ diff --git a/api/vendor/cache/rbs-3.9.1.gem b/api/vendor/cache/rbs-3.9.1.gem deleted file mode 100644 index 8ece503..0000000 Binary files a/api/vendor/cache/rbs-3.9.1.gem and /dev/null differ diff --git a/api/vendor/cache/rbs-4.0.0.gem b/api/vendor/cache/rbs-4.0.0.gem new file mode 100644 index 0000000..2235888 Binary files /dev/null and b/api/vendor/cache/rbs-4.0.0.gem differ diff --git a/api/vendor/cache/ruby-lsp-0.23.12.gem b/api/vendor/cache/ruby-lsp-0.23.12.gem deleted file mode 100644 index 9d1eb72..0000000 Binary files a/api/vendor/cache/ruby-lsp-0.23.12.gem and /dev/null differ diff --git a/api/vendor/cache/ruby-lsp-0.26.8.gem b/api/vendor/cache/ruby-lsp-0.26.8.gem new file mode 100644 index 0000000..8e26c22 Binary files /dev/null and b/api/vendor/cache/ruby-lsp-0.26.8.gem differ diff --git a/api/vendor/cache/ruby-lsp-rails-0.4.0.gem b/api/vendor/cache/ruby-lsp-rails-0.4.0.gem deleted file mode 100644 index ccccd43..0000000 Binary files a/api/vendor/cache/ruby-lsp-rails-0.4.0.gem and /dev/null differ diff --git a/api/vendor/cache/ruby-lsp-rails-0.4.8.gem b/api/vendor/cache/ruby-lsp-rails-0.4.8.gem new file mode 100644 index 0000000..4cd69c3 Binary files /dev/null and b/api/vendor/cache/ruby-lsp-rails-0.4.8.gem differ diff --git a/api/vendor/cache/sorbet-runtime-0.5.11956.gem b/api/vendor/cache/sorbet-runtime-0.5.11956.gem deleted file mode 100644 index 184acc3..0000000 Binary files a/api/vendor/cache/sorbet-runtime-0.5.11956.gem and /dev/null differ diff --git a/api/vendor/cache/tsort-0.2.0.gem b/api/vendor/cache/tsort-0.2.0.gem new file mode 100644 index 0000000..b80e65b Binary files /dev/null and b/api/vendor/cache/tsort-0.2.0.gem differ diff --git a/ci/jenkins/campsite-api/Jenkinsfile b/ci/jenkins/campsite-api/Jenkinsfile new file mode 100644 index 0000000..564e50d --- /dev/null +++ b/ci/jenkins/campsite-api/Jenkinsfile @@ -0,0 +1,72 @@ +pipeline { + agent any + + environment { + HARBOR_REGISTRY = 'registry.xuanwu.openatom.cn' + HARBOR_REPO = 'mega/campsite-api' + DOCKERFILE = 'api/Dockerfile' + BUILD_CONTEXT = 'api' + GIT_URL = 'https://github.com/benjamin-747/campsite' + GIT_BRANCH = 'main' + GIT_CREDENTIALS = 'd4cedbd2-3e68-411b-8367-0960974d7b6d' + HARBOR_CREDENTIALS = '3cefd32b-0fae-4749-b125-0a3f0c537f88' + RUBY_VERSION = '3.3.4' + NODE_VERSION = '18.16.1' + BUNDLER_VERSION = '2.3.14' + } + + stages { + stage('Checkout') { + steps { + git branch: "${GIT_BRANCH}", + credentialsId: "${GIT_CREDENTIALS}", + url: "${GIT_URL}" + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim() + env.IMAGE_SHA = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}" + env.IMAGE_LATEST = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest" + env.IMAGE_SHA_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}-amd64" + env.IMAGE_LATEST_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest-amd64" + currentBuild.description = "${HARBOR_REPO}:${env.SHORT_SHA}" + } + echo "Building image: ${env.IMAGE_SHA}" + } + } + + stage('Build Docker Image') { + steps { + sh ''' + /opt/docker-cli/docker build --network=host \ + --build-arg RUBY_VERSION=${RUBY_VERSION} \ + --build-arg NODE_VERSION=${NODE_VERSION} \ + --build-arg BUNDLER_VERSION=${BUNDLER_VERSION} \ + -f ${DOCKERFILE} \ + -t ${IMAGE_SHA} \ + ${BUILD_CONTEXT} + ''' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST_AMD64}' + } + } + + stage('Push to Harbor') { + steps { + withCredentials([ + usernamePassword( + credentialsId: "${HARBOR_CREDENTIALS}", + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh '/opt/docker-cli/docker login ${HARBOR_REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASS}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST_AMD64}' + sh '/opt/docker-cli/docker logout ${HARBOR_REGISTRY}' + } + } + } + } +}