diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 1459ff2..ec34326 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,118 +1,69 @@ -name: Deploy im-server (k3s test cluster) +name: Deploy im-server production on: + push: + branches: + - pro workflow_dispatch: - inputs: - image_tag: - description: '镜像tag,默认为 test(与集群 kustomization 里的 newTag 一致)' - required: false + +concurrency: + group: production-im-server + cancel-in-progress: false + +permissions: + contents: read + +env: + REGISTRY: git.olns.shop + IMAGE: git.olns.shop/pro/im_server + SERVICE: im-server + DEPLOY_HOST: 145.223.69.9 + DEPLOY_USER: olns-deploy jobs: deploy: runs-on: deploy-host + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - name: 确定镜像tag - id: vars + - name: Log in to Gitea Container Registry + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | - TAG="${{ github.event.inputs.image_tag }}" - [ -z "$TAG" ] && TAG=test - echo "tag=$TAG" >> "$GITHUB_OUTPUT" + set -euo pipefail + printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin - - name: 登录镜像仓库 - run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.118im.com -u admin --password-stdin - - - name: 构建并推送镜像(从本仓库源码编译,不是官方预编译包) + - name: Build and push image + env: + GITHUB_SHA: ${{ github.sha }} run: | + set -euo pipefail + SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")" docker build -f docker/tinode/Dockerfile.build \ - -t git.118im.com/im_team/im-server:${{ steps.vars.outputs.tag }} . - docker push git.118im.com/im_team/im-server:${{ steps.vars.outputs.tag }} + -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" . + docker push "$IMAGE:pro" + docker push "$IMAGE:$SHORT_SHA" - - name: 部署到k3s测试集群 + - name: Deploy Compose service + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | - mkdir -p ~/.kube - echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config - chmod 600 ~/.kube/config - kubectl set resources statefulset/im-server -n im-server-system -c im-server \ - --requests=cpu=2,memory=512Mi --limits=cpu=2,memory=1Gi - kubectl set env statefulset/im-server -n im-server-system -c im-server \ - CLUSTER_PROXY_TO_MASTER_BUFFER=4096 \ - CLUSTER_PROXY_TO_MASTER_WORKERS=16 \ - CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS=300 \ - CLUSTER_ROUTE_RETRIES=3 \ - CLUSTER_ROUTE_RETRY_BACKOFF_MS=25 \ - MYSQL_MAX_OPEN_CONNS=24 \ - MYSQL_MAX_IDLE_CONNS=8 \ - MYSQL_REGISTRY_MAX_OPEN_CONNS=4 \ - MYSQL_REGISTRY_MAX_IDLE_CONNS=2 \ - MYSQL_CONN_MAX_LIFETIME_SECONDS=1800 - kubectl patch statefulset/im-server -n im-server-system --type=strategic -p \ - '{"spec":{"template":{"spec":{"containers":[{"name":"im-server","readinessProbe":{"httpGet":{"path":"/healthz","port":6060},"timeoutSeconds":3,"periodSeconds":10,"failureThreshold":3},"livenessProbe":{"httpGet":{"path":"/livez","port":6060},"timeoutSeconds":3,"periodSeconds":20,"failureThreshold":3}}]}}}}' - if kubectl auth can-i get services -n im-server-system >/dev/null 2>&1 && \ - kubectl auth can-i patch services -n im-server-system >/dev/null 2>&1; then - kubectl patch service/im-server -n im-server-system --type=merge -p \ - '{"spec":{"sessionAffinity":"None"}}' - else - echo "::warning::ci-deployer cannot get/patch Service; skipped sessionAffinity update" - fi - if kubectl auth can-i get ingresses.networking.k8s.io -n im-server-system >/dev/null 2>&1 && \ - kubectl auth can-i patch ingresses.networking.k8s.io -n im-server-system >/dev/null 2>&1 && \ - kubectl get ingress/im-server -n im-server-system >/dev/null 2>&1; then - kubectl annotate ingress/im-server -n im-server-system --overwrite \ - nginx.ingress.kubernetes.io/load-balance=least_conn - else - echo "::warning::ci-deployer cannot manage Ingress/im-server or it does not exist; skipped least_conn update" - fi - kubectl rollout restart statefulset/im-server -n im-server-system - if ! kubectl rollout status statefulset/im-server -n im-server-system --timeout=300s; then - echo "::error::im-server rollout did not become ready within 300 seconds" - kubectl get statefulset/im-server -n im-server-system -o wide || true - kubectl get pods -n im-server-system -l app=im-server -o wide || true - kubectl describe statefulset/im-server -n im-server-system || true - for pod in $(kubectl get pods -n im-server-system -l app=im-server -o name 2>/dev/null); do - echo "===== ${pod} describe =====" - kubectl describe -n im-server-system "${pod}" || true - echo "===== ${pod} current log =====" - kubectl logs -n im-server-system "${pod}" -c im-server --tail=200 || true - echo "===== ${pod} previous log =====" - kubectl logs -n im-server-system "${pod}" -c im-server --previous --tail=200 || true - done - if kubectl auth can-i list events -n im-server-system >/dev/null 2>&1; then - echo "===== recent events =====" - kubectl get events -n im-server-system --sort-by=.lastTimestamp | tail -100 || true - fi - exit 1 - fi - - echo "Waiting for ArgoCD reconciliation before validating effective settings" - sleep 60 - request_cpu="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].resources.requests.cpu}')" - request_memory="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].resources.requests.memory}')" - limit_cpu="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].resources.limits.cpu}')" - limit_memory="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].resources.limits.memory}')" - liveness="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].livenessProbe.httpGet.path}')" - mysql_max_open="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].env[?(@.name=="MYSQL_MAX_OPEN_CONNS")].value}')" - mysql_max_idle="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].env[?(@.name=="MYSQL_MAX_IDLE_CONNS")].value}')" - mysql_registry_max_open="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].env[?(@.name=="MYSQL_REGISTRY_MAX_OPEN_CONNS")].value}')" - mysql_registry_max_idle="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].env[?(@.name=="MYSQL_REGISTRY_MAX_IDLE_CONNS")].value}')" - mysql_conn_max_lifetime="$(kubectl get statefulset/im-server -n im-server-system -o \ - jsonpath='{.spec.template.spec.containers[?(@.name=="im-server")].env[?(@.name=="MYSQL_CONN_MAX_LIFETIME_SECONDS")].value}')" - echo "effective requests=${request_cpu}/${request_memory} limits=${limit_cpu}/${limit_memory} liveness=${liveness} mysql=${mysql_max_open}/${mysql_max_idle} registry=${mysql_registry_max_open}/${mysql_registry_max_idle} lifetime=${mysql_conn_max_lifetime}" - if [ "${request_cpu}" != "2" ] || [ "${request_memory}" != "512Mi" ] || \ - [ "${limit_cpu}" != "2" ] || [ "${limit_memory}" != "1Gi" ] || \ - [ "${liveness}" != "/livez" ] || [ "${mysql_max_open}" != "24" ] || \ - [ "${mysql_max_idle}" != "8" ] || [ "${mysql_registry_max_open}" != "4" ] || \ - [ "${mysql_registry_max_idle}" != "2" ] || [ "${mysql_conn_max_lifetime}" != "1800" ]; then - echo "::error::ArgoCD reverted StatefulSet resources/probes/MySQL pools. Apply deploy/argocd/im-server-test/statefulset-performance-patch.yaml to the im-server-test source." - exit 1 - fi + set -euo pipefail + install -d -m 700 "$HOME/.ssh" + KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")" + cleanup() { + shred -u "$KEY_FILE" 2>/dev/null || true + } + trap cleanup EXIT + printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_FILE" + chmod 600 "$KEY_FILE" + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts" + chmod 600 "$HOME/.ssh/known_hosts" + SSH_ARGS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=yes) + printf '%s' "$REGISTRY_TOKEN" | ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \ + "docker login git.olns.shop -u admin --password-stdin" + ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \ + "cd /opt/olns && docker compose pull im-server && docker compose up -d --no-deps --wait im-server" diff --git a/tests/test_actions_workflow.py b/tests/test_actions_workflow.py new file mode 100644 index 0000000..749e535 --- /dev/null +++ b/tests/test_actions_workflow.py @@ -0,0 +1,39 @@ +from pathlib import Path +import unittest + + +WORKFLOW = Path(__file__).resolve().parents[1] / ".gitea/workflows/deploy.yml" + + +class DeployWorkflowContractTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.workflow = WORKFLOW.read_text(encoding="utf-8") + + def test_targets_pro_branch_and_single_runner(self): + self.assertIn("push:", self.workflow) + self.assertIn("- pro", self.workflow) + self.assertIn("workflow_dispatch:", self.workflow) + self.assertIn("runs-on: deploy-host", self.workflow) + self.assertIn("concurrency:", self.workflow) + + def test_builds_and_pushes_pro_image(self): + self.assertIn("git.olns.shop/pro/im_server", self.workflow) + self.assertIn("docker push", self.workflow) + self.assertIn(":pro", self.workflow) + + def test_deploys_compose_service_with_pinned_host_key(self): + self.assertIn("DEPLOY_SSH_KEY", self.workflow) + self.assertIn("DEPLOY_KNOWN_HOSTS", self.workflow) + self.assertIn("StrictHostKeyChecking=yes", self.workflow) + self.assertIn("docker compose pull im-server", self.workflow) + self.assertIn("docker compose up -d --no-deps --wait im-server", self.workflow) + + def test_removes_old_kubernetes_and_registry_targets(self): + self.assertNotIn("kubectl", self.workflow) + self.assertNotIn("KUBECONFIG", self.workflow) + self.assertNotIn("git.118im.com", self.workflow) + +if __name__ == "__main__": + unittest.main() +