pipeline {
    agent {
        node {
            label '构建机1'
            customWorkspace '/home/nanxunai/goujian/workspace/imagefind-release'
        }
    }

    options {
        timestamps()
        disableConcurrentBuilds()
        skipDefaultCheckout(true)
        timeout(time: 240, unit: 'MINUTES')
        buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '15'))
    }

    environment {
        PATH = '/home/nanxunai/.local/bin:/usr/local/bin:/usr/bin:/bin'
        FNPACK_BIN = '/home/nanxunai/.local/bin/fnpack'
        REGISTRY_URL = 'reg.nxsir.cn'
        IMAGE_REPO = 'reg.nxsir.cn/imagefind/app'
        HARBOR_CREDENTIALS = 'imagefind_key'
        OPENLIST_CREDENTIALS = 'openlist_key'
        NODE_CREDENTIALS = 'bbb939ea-4f01-4b47-aecb-c5ee2a551ef4'
        OPENLIST_BASE_URL = 'https://openlist.nxsir.cn'
        OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/imagefind'
        HTTP_PROXY_URL = 'http://192.168.5.200:7890'
        NO_PROXY_HOSTS = '127.0.0.1,localhost,reg.nxsir.cn,gitea.nxsir.cn,openlist.nxsir.cn,pypi.tuna.tsinghua.edu.cn'
        DOCKER_CONFIG = "${WORKSPACE}/.ci/docker"
        NPM_CONFIG_CACHE = "${WORKSPACE}/.ci/npm-cache"
        PIP_CACHE_DIR = "${WORKSPACE}/.ci/pip-cache"
        PIP_INDEX_URL = 'https://pypi.tuna.tsinghua.edu.cn/simple'
        PIP_DEFAULT_TIMEOUT = '60'
        IMAGEFIND_WHEEL_CACHE = "${WORKSPACE}/.ci/wheelhouse-python312"
        IMAGEFIND_DEPENDENCY_TMPDIR = "${WORKSPACE}/.ci/dependency-tmp"
        IMAGEFIND_VERIFY_TMPDIR = "${WORKSPACE}/.ci/fnos-verify"
    }

    stages {
        stage('Checkout') {
            steps {
                deleteDir()
                checkout scm
            }
        }

        stage('Metadata And Preflight') {
            steps {
                script {
                    env.APP_VERSION = sh(script: "sed -n 's/^version=//p' fnos/manifest | head -n 1", returnStdout: true).trim()
                    env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim()
                    env.FNOS_VERSION = sh(script: '''
                        set -euo pipefail
                        base="$APP_VERSION"; major=${base%%.*}; remainder=${base#*.}
                        minor=${remainder%%.*}; patch=${remainder#*.}
                        test "$base" = "$major.$minor.$patch" && test "$BUILD_NUMBER" -lt 100000
                        printf '%s.%s.%s\n' "$major" "$minor" "$((patch * 100000 + BUILD_NUMBER))"
                    ''', returnStdout: true).trim()
                    env.IMMUTABLE_TAG = "${env.APP_VERSION}-b${env.BUILD_NUMBER}-${env.SHORT_SHA}"
                    env.IMAGE_REF = "${env.IMAGE_REPO}:${env.IMMUTABLE_TAG}"
                    env.FPK_BASENAME = "imagefind-${env.FNOS_VERSION}-x86_64.fpk"
                    env.FPK_PATH = "${env.WORKSPACE}/artifacts/${env.FPK_BASENAME}"
                    env.MANIFEST_PATH = "${env.WORKSPACE}/artifacts/imagefind-${env.IMMUTABLE_TAG}-build-manifest.json"
                    currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.IMMUTABLE_TAG}"
                    currentBuild.description = "fnOS ${env.FNOS_VERSION}"
                }
                withCredentials([usernamePassword(credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD')]) {
                    sh '''
                        set -euo pipefail
                        if ! python3 -c 'import ensurepip' >/dev/null 2>&1; then
                          printf '%s\n' "$JENKINS_NODE_PASSWORD" | sudo -S -p '' apt-get update
                          printf '%s\n' "$JENKINS_NODE_PASSWORD" | sudo -S -p '' \
                            apt-get install -y --no-install-recommends python3.13-venv
                          printf '%s\n' "$JENKINS_NODE_PASSWORD" | sudo -S -p '' apt-get clean
                        fi
                        python3 -c 'import ensurepip'
                    '''
                }
                sh '''
                    set -euo pipefail
                    test "$(uname -m)" = x86_64
                    for command_name in git sudo docker curl python3 npm node sha256sum tar dpkg-deb; do command -v "$command_name" >/dev/null; done
                    test -x "$FNPACK_BIN"
                    mkdir -p "$DOCKER_CONFIG" "$NPM_CONFIG_CACHE" "$PIP_CACHE_DIR" "$IMAGEFIND_WHEEL_CACHE" \
                      "$IMAGEFIND_DEPENDENCY_TMPDIR" "$IMAGEFIND_VERIFY_TMPDIR" artifacts
                    chmod 700 "$DOCKER_CONFIG"
                    available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }')
                    test "$available_kb" -ge 3145728 || {
                      printf 'at least 3 GiB free disk space is required; available: %s KiB\n' "$available_kb" >&2
                      exit 1
                    }
                '''
            }
        }

        stage('Backend And Frontend Tests') {
            steps {
                sh '''
                    set -euo pipefail
                    python3 -m venv .ci/venv
                    .ci/venv/bin/python -m pip install --disable-pip-version-check --upgrade pip
                    .ci/venv/bin/python -m pip install --disable-pip-version-check '.[dev]'
                    .ci/venv/bin/ruff check backend tests
                    # Unit tests that do not require the fnOS shared PostgreSQL service.
                    # PostgreSQL integration is covered by the real-container smoke stage below.
                    PYTHONPATH=. .ci/venv/bin/pytest -q \
                      tests/test_openlist_client.py \
                      tests/test_workflow_contracts.py \
                      tests/test_accelerator_runtime.py \
                      tests/test_inference.py
                    npm ci --prefix frontend
                    npm run lint --prefix frontend
                    npm run build --prefix frontend
                '''
            }
        }

        stage('Prepare And Build fnOS x64') {
            steps {
                sh '''
                    set -euo pipefail
                    PYTHON=.ci/venv/bin/python ./scripts/prepare-fnos-dependencies.sh
                    PACKAGE_VERSION="$FNOS_VERSION" PYTHON=.ci/venv/bin/python FNPACK="$FNPACK_BIN" \
                      ./scripts/build-fnos.sh "$FPK_PATH"
                    ./scripts/verify-fnos-package.sh "$FPK_PATH" "$FNOS_VERSION"
                '''
            }
        }

        stage('Build And Smoke Docker amd64') {
            steps {
                withCredentials([usernamePassword(credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD')]) {
                    sh '''
                        set -euo pipefail
                        DOCKER_BUILDKIT=1 ./scripts/ci-docker.sh build --network host --progress=plain --provenance=false \
                          --build-arg PIP_INDEX_URL="$PIP_INDEX_URL" \
                          --build-arg HTTP_PROXY="$HTTP_PROXY_URL" --build-arg HTTPS_PROXY="$HTTP_PROXY_URL" \
                          --build-arg NO_PROXY="$NO_PROXY_HOSTS" -t "$IMAGE_REF" .
                        smoke_name="imagefind-smoke-$BUILD_NUMBER"
                        mkdir -p .ci/docker-data
                        ./scripts/ci-docker.sh rm -f "$smoke_name" >/dev/null 2>&1 || true
                        ./scripts/ci-docker.sh run -d --name "$smoke_name" \
                          -e IMAGEFIND_ADMIN_PASSWORD='ImageFind-Smoke-2026!' \
                          -v "$WORKSPACE/.ci/docker-data:/data" "$IMAGE_REF"
                        ready=0
                        for attempt in $(seq 1 90); do
                          if ./scripts/ci-docker.sh exec "$smoke_name" curl -fsS http://127.0.0.1:8765/ >/dev/null; then ready=1; break; fi
                          if ! ./scripts/ci-docker.sh inspect --format '{{.State.Running}}' "$smoke_name" 2>/dev/null | grep -qx true; then
                            echo "Smoke container exited before becoming ready (attempt $attempt)." >&2
                            ./scripts/ci-docker.sh logs "$smoke_name" || true
                            ./scripts/ci-docker.sh inspect "$smoke_name" || true
                            exit 1
                          fi
                          sleep 2
                        done
                        test "$ready" = 1 || { ./scripts/ci-docker.sh logs "$smoke_name" || true; exit 1; }
                        ./scripts/ci-docker.sh exec "$smoke_name" curl -fsS -H 'Content-Type: application/json' \
                          --data '{"password":"ImageFind-Smoke-2026!"}' \
                          http://127.0.0.1:8765/api/v1/auth/login | grep -q 'csrf'
                        ./scripts/ci-docker.sh restart "$smoke_name" >/dev/null
                        ready=0
                        for attempt in $(seq 1 60); do
                          if ./scripts/ci-docker.sh exec "$smoke_name" curl -fsS http://127.0.0.1:8765/ >/dev/null; then ready=1; break; fi
                          if ! ./scripts/ci-docker.sh inspect --format '{{.State.Running}}' "$smoke_name" 2>/dev/null | grep -qx true; then
                            echo "Smoke container exited after restart (attempt $attempt)." >&2
                            ./scripts/ci-docker.sh logs "$smoke_name" || true
                            ./scripts/ci-docker.sh inspect "$smoke_name" || true
                            exit 1
                          fi
                          sleep 2
                        done
                        test "$ready" = 1 || { ./scripts/ci-docker.sh logs "$smoke_name" || true; exit 1; }
                        ./scripts/ci-docker.sh exec "$smoke_name" curl -fsS -H 'Content-Type: application/json' \
                          --data '{"password":"ImageFind-Smoke-2026!"}' \
                          http://127.0.0.1:8765/api/v1/auth/login | grep -q 'csrf'
                        ./scripts/ci-docker.sh exec "$smoke_name" test -s /data/postgresql/PG_VERSION
                        ./scripts/ci-docker.sh exec "$smoke_name" test -s /data/postgres-client.conf
                        ./scripts/ci-docker.sh exec "$smoke_name" test -e /data/.imagefind-admin-initialized
                        ./scripts/ci-docker.sh rm -f "$smoke_name"
                    '''
                }
            }
        }

        stage('Push Immutable And Describe') {
            steps {
                withCredentials([
                    usernamePassword(credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD'),
                    usernamePassword(credentialsId: "${HARBOR_CREDENTIALS}", usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')
                ]) {
                    sh '''
                        set -euo pipefail
                        set +x
                        auth=$(printf '%s:%s' "$HARBOR_USERNAME" "$HARBOR_PASSWORD" | base64 -w0)
                        printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$REGISTRY_URL" "$auth" >"$DOCKER_CONFIG/config.json"
                        chmod 600 "$DOCKER_CONFIG/config.json"
                        unset auth
                        set -x
                        ./scripts/ci-docker.sh push "$IMAGE_REF"
                        digest=$(./scripts/ci-docker.sh image inspect --format '{{index .RepoDigests 0}}' "$IMAGE_REF" | sed -n 's/.*@//p'); test -n "$digest"; export digest
                        fpk_sha=$(awk '{print $1}' "$FPK_PATH.sha256"); export fpk_sha
                        python3 - <<'PY'
import json, os, pathlib, subprocess
path = pathlib.Path(os.environ['MANIFEST_PATH'])
payload = {
  'project':'imagefind','buildNumber':os.environ['BUILD_NUMBER'],
  'commit':subprocess.check_output(['git','rev-parse','HEAD'],text=True).strip(),
  'productVersion':os.environ['APP_VERSION'],'fnosVersion':os.environ['FNOS_VERSION'],
  'fpk':pathlib.Path(os.environ['FPK_PATH']).name,'fpkSha256':os.environ['fpk_sha'],
  'image':os.environ['IMAGE_REF'],'digest':os.environ['digest'],'platform':'linux/amd64',
  'database':'PostgreSQL 15 + pgvector 0.8.6 (embedded Docker runtime)'
}
path.write_text(json.dumps(payload,ensure_ascii=False,indent=2)+chr(10),encoding='utf-8')
PY
                        sha256sum "$MANIFEST_PATH" >"$MANIFEST_PATH.sha256"
                    '''
                }
            }
        }

        stage('Upload Artifacts') {
            steps {
                withCredentials([usernamePassword(credentialsId: "${OPENLIST_CREDENTIALS}", usernameVariable: 'OPENLIST_USERNAME', passwordVariable: 'OPENLIST_PASSWORD')]) {
                    sh '''
                        set -euo pipefail
                        for artifact in "$FPK_PATH" "$FPK_PATH.sha256" "$MANIFEST_PATH" "$MANIFEST_PATH.sha256"; do
                          ./scripts/upload-openlist-artifact.sh "$artifact" "$OPENLIST_REMOTE_DIR"
                        done
                    '''
                }
            }
        }

        stage('Publish Stable Image Tags') {
            steps {
                withCredentials([usernamePassword(credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD')]) {
                    sh '''
                        set -euo pipefail
                        for tag in "$APP_VERSION" latest; do
                          ./scripts/ci-docker.sh tag "$IMAGE_REF" "$IMAGE_REPO:$tag"
                          ./scripts/ci-docker.sh push "$IMAGE_REPO:$tag"
                        done
                    '''
                }
            }
        }
    }

    post {
        success { archiveArtifacts artifacts: 'artifacts/*.fpk,artifacts/*.sha256,artifacts/*.json', fingerprint: true }
        always {
            withCredentials([usernamePassword(credentialsId: "${NODE_CREDENTIALS}", usernameVariable: 'JENKINS_NODE_USERNAME', passwordVariable: 'JENKINS_NODE_PASSWORD')]) {
                sh '''
                    set +e
                    ./scripts/ci-docker.sh rm -f "imagefind-smoke-$BUILD_NUMBER" >/dev/null 2>&1 || true
                    for tag in "$IMMUTABLE_TAG" "$APP_VERSION" latest; do ./scripts/ci-docker.sh rmi "$IMAGE_REPO:$tag" >/dev/null 2>&1 || true; done
                    : >"$DOCKER_CONFIG/config.json" 2>/dev/null || true
                '''
            }
            cleanWs(deleteDirs: true, notFailBuild: true)
        }
    }
}
