docs: README 업데이트
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
This commit is contained in:
423
README.md
423
README.md
@@ -1,42 +1,74 @@
|
|||||||
# Web Portal - Kubernetes 배포 가이드
|
# Web Portal - Kubernetes + GitOps 배포 가이드
|
||||||
|
|
||||||
|
## 📋 프로젝트 개요
|
||||||
|
|
||||||
|
조직 내부 웹페이지를 통합 관리하는 포털 시스템입니다.
|
||||||
|
로그인 후 본인에게 할당된 웹페이지 목록을 확인하고 접속할 수 있으며,
|
||||||
|
관리자는 페이지 및 사용자 권한을 관리할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 전체 아키텍처
|
||||||
|
|
||||||
|
```
|
||||||
|
개발자 (git push)
|
||||||
|
↓
|
||||||
|
Gitea (192.168.10.101:30000)
|
||||||
|
└── Container Registry (이미지 저장)
|
||||||
|
↓
|
||||||
|
ArgoCD 자동 감지 & 배포 (192.168.10.101:30080)
|
||||||
|
↓
|
||||||
|
Kubernetes - web-portal 네임스페이스
|
||||||
|
├── Nginx Frontend (NodePort: 30090)
|
||||||
|
├── FastAPI Backend (ClusterIP: 8000)
|
||||||
|
└── PostgreSQL DB (ClusterIP: 5432)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 기술 스택
|
||||||
|
|
||||||
|
| 구분 | 기술 |
|
||||||
|
|------|------|
|
||||||
|
| Frontend | Nginx + HTML/CSS/JS (SPA) |
|
||||||
|
| Backend | Python FastAPI |
|
||||||
|
| Database | PostgreSQL |
|
||||||
|
| Container | Docker Desktop |
|
||||||
|
| Orchestration | Kubernetes (Docker Desktop 내장) |
|
||||||
|
| GitOps | Gitea + ArgoCD |
|
||||||
|
| Image Registry | Gitea Container Registry |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📁 프로젝트 구조
|
## 📁 프로젝트 구조
|
||||||
|
|
||||||
```
|
```
|
||||||
k8s-portal/
|
nginx-portal/
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── build-and-push.yaml # Gitea Actions CI (선택사항)
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── main.py # FastAPI 백엔드 (전체 API)
|
│ ├── main.py # FastAPI 전체 API 로직
|
||||||
│ ├── requirements.txt
|
│ ├── requirements.txt
|
||||||
│ └── Dockerfile
|
│ └── Dockerfile
|
||||||
├── frontend/
|
├── frontend/
|
||||||
│ ├── index.html # 싱글 페이지 앱 (SPA)
|
│ ├── index.html # 싱글 페이지 앱 (SPA)
|
||||||
│ ├── nginx.conf # Nginx 설정 (API 프록시 포함)
|
│ ├── nginx.conf # Nginx 설정 + /api/* 프록시
|
||||||
│ └── Dockerfile
|
│ └── Dockerfile
|
||||||
├── k8s/
|
├── k8s/
|
||||||
│ ├── 01-namespace.yaml # web-portal 네임스페이스
|
│ ├── 01-namespace.yaml # web-portal 네임스페이스
|
||||||
│ ├── 02-postgres.yaml # PostgreSQL + PVC + Service
|
│ ├── 02-postgres.yaml # PostgreSQL + PVC + Service
|
||||||
│ ├── 03-secrets.yaml # DB 패스워드, JWT 시크릿
|
│ ├── 03-secrets.yaml # DB/JWT 시크릿
|
||||||
│ ├── 04-backend.yaml # FastAPI Deployment + Service
|
│ ├── 04-backend.yaml # FastAPI Deployment + Service
|
||||||
│ └── 05-frontend.yaml # Nginx Deployment + NodePort(30080)
|
│ └── 05-frontend.yaml # Nginx Deployment + NodePort(30090)
|
||||||
├── build-and-deploy.sh # 원클릭 빌드 & 배포
|
├── 06-argocd-app.yaml # ArgoCD Application 정의 (k8s 폴더 밖에 위치)
|
||||||
├── cleanup.sh # 전체 삭제
|
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 설치 및 실행
|
> ⚠️ `06-argocd-app.yaml` 은 반드시 `k8s/` 폴더 **밖**에 위치해야 합니다.
|
||||||
|
> ArgoCD가 `k8s/` 폴더를 감시하므로 해당 파일이 안에 있으면 순환 참조 문제가 발생합니다.
|
||||||
|
|
||||||
### 전제 조건
|
---
|
||||||
- Docker Desktop 설치 및 실행
|
|
||||||
- Docker Desktop > Settings > Kubernetes > Enable Kubernetes ✅
|
|
||||||
|
|
||||||
### 배포 (원클릭)
|
|
||||||
```bash
|
|
||||||
chmod +x build-and-deploy.sh
|
|
||||||
./build-and-deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 접속
|
|
||||||
브라우저에서: **http://localhost:30080**
|
|
||||||
|
|
||||||
## 🔑 기본 계정
|
## 🔑 기본 계정
|
||||||
|
|
||||||
@@ -45,17 +77,119 @@ chmod +x build-and-deploy.sh
|
|||||||
| 관리자 | `admin` | `admin1234` |
|
| 관리자 | `admin` | `admin1234` |
|
||||||
| 일반사용자 | `user1` | `user1234` |
|
| 일반사용자 | `user1` | `user1234` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ 기능 설명
|
## ✨ 기능 설명
|
||||||
|
|
||||||
### 일반 사용자
|
### 일반 사용자
|
||||||
- 로그인 후 **본인에게 할당된 웹페이지 목록** 확인
|
- 로그인 후 **본인에게 할당된 웹페이지 목록** 카드 형태로 확인
|
||||||
- 카드 클릭 시 **새 탭에서 해당 URL로 이동**
|
- 카드 클릭 시 **새 탭에서 해당 URL로 이동**
|
||||||
|
|
||||||
### 관리자
|
### 관리자
|
||||||
일반 사용자 기능 + 추가:
|
일반 사용자 기능 + 추가:
|
||||||
- **페이지 관리**: 웹페이지 추가/수정/삭제
|
- **페이지 관리**: 웹페이지 추가 / 수정 / 삭제
|
||||||
- **사용자 관리**: 계정 생성/삭제
|
- **사용자 관리**: 계정 생성 / 삭제
|
||||||
- **권한 설정**: 사용자별 접근 가능 페이지 체크박스로 지정
|
- **권한 설정**: 사용자별 접근 가능 페이지를 체크박스로 지정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 최초 배포 순서
|
||||||
|
|
||||||
|
### 1단계. Docker Desktop insecure-registry 설정
|
||||||
|
Gitea Registry가 HTTP이므로 Docker Desktop에서 허용 설정 필요.
|
||||||
|
|
||||||
|
Docker Desktop → Settings → Docker Engine:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"builder": {
|
||||||
|
"gc": {
|
||||||
|
"defaultKeepStorage": "20GB",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"experimental": false,
|
||||||
|
"insecure-registries": ["192.168.10.101:30000"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Apply & Restart** 클릭
|
||||||
|
|
||||||
|
### 2단계. Gitea Registry 로그인 및 이미지 빌드 & Push
|
||||||
|
```bash
|
||||||
|
# Registry 로그인
|
||||||
|
docker login 192.168.10.101:30000 -u <Gitea계정>
|
||||||
|
|
||||||
|
# 백엔드 이미지 빌드 & Push
|
||||||
|
docker build -t 192.168.10.101:30000/<계정>/portal-backend:latest ./backend/
|
||||||
|
docker push 192.168.10.101:30000/<계정>/portal-backend:latest
|
||||||
|
|
||||||
|
# 프론트엔드 이미지 빌드 & Push
|
||||||
|
docker build -t 192.168.10.101:30000/<계정>/portal-frontend:latest ./frontend/
|
||||||
|
docker push 192.168.10.101:30000/<계정>/portal-frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3단계. K8s Registry Secret 생성
|
||||||
|
```bash
|
||||||
|
kubectl create namespace web-portal
|
||||||
|
|
||||||
|
kubectl create secret docker-registry gitea-registry-secret \
|
||||||
|
--namespace=web-portal \
|
||||||
|
--docker-server=gitea-http.gitea.svc.cluster.local:3000 \
|
||||||
|
--docker-username=<Gitea계정> \
|
||||||
|
--docker-password=<Gitea패스워드>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4단계. ArgoCD에 Gitea 저장소 인증 등록
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic gitea-repo-secret \
|
||||||
|
--namespace=argocd \
|
||||||
|
--from-literal=type=git \
|
||||||
|
--from-literal=url=http://192.168.10.101:30000/<계정>/nginx-portal.git \
|
||||||
|
--from-literal=username=<Gitea계정> \
|
||||||
|
--from-literal=password=<Gitea패스워드>
|
||||||
|
|
||||||
|
kubectl label secret gitea-repo-secret \
|
||||||
|
-n argocd \
|
||||||
|
argocd.argoproj.io/secret-type=repository
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5단계. ArgoCD Application 등록
|
||||||
|
```bash
|
||||||
|
kubectl apply -f 06-argocd-app.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6단계. 접속 확인
|
||||||
|
```
|
||||||
|
http://192.168.10.101:30090
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 이후 배포 방법 (코드 수정 시)
|
||||||
|
|
||||||
|
### yaml만 변경한 경우 (설정 변경)
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "fix: 변경내용"
|
||||||
|
git push origin main
|
||||||
|
# → ArgoCD가 자동으로 감지해서 재배포
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이미지도 변경한 경우 (코드 변경)
|
||||||
|
```bash
|
||||||
|
# 이미지 재빌드 & Push
|
||||||
|
docker build -t 192.168.10.101:30000/<계정>/portal-backend:latest ./backend/
|
||||||
|
docker push 192.168.10.101:30000/<계정>/portal-backend:latest
|
||||||
|
|
||||||
|
# Pod 재시작
|
||||||
|
kubectl rollout restart deployment/backend -n web-portal
|
||||||
|
|
||||||
|
# yaml도 변경했다면
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: 변경내용"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔧 운영 명령어
|
## 🔧 운영 명령어
|
||||||
|
|
||||||
@@ -69,34 +203,233 @@ kubectl logs -n web-portal deployment/backend -f
|
|||||||
# 프론트엔드 로그 확인
|
# 프론트엔드 로그 확인
|
||||||
kubectl logs -n web-portal deployment/frontend -f
|
kubectl logs -n web-portal deployment/frontend -f
|
||||||
|
|
||||||
# 재시작
|
# Pod 재시작
|
||||||
kubectl rollout restart deployment/backend -n web-portal
|
kubectl rollout restart deployment/backend -n web-portal
|
||||||
kubectl rollout restart deployment/frontend -n web-portal
|
kubectl rollout restart deployment/frontend -n web-portal
|
||||||
|
|
||||||
# 전체 삭제
|
# 전체 삭제
|
||||||
./cleanup.sh
|
kubectl delete namespace web-portal
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔐 운영 시 보안 설정
|
## 🔐 운영 시 보안 설정
|
||||||
|
|
||||||
`k8s/03-secrets.yaml`에서 반드시 변경:
|
`k8s/03-secrets.yaml` 에서 반드시 변경:
|
||||||
```yaml
|
```yaml
|
||||||
stringData:
|
stringData:
|
||||||
db-password: "강력한패스워드로변경" # PostgreSQL 패스워드
|
db-password: "강력한패스워드로변경"
|
||||||
jwt-secret: "64자이상의랜덤문자열로변경" # JWT 서명 키
|
jwt-secret: "64자이상의랜덤문자열로변경"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ 아키텍처
|
---
|
||||||
|
|
||||||
|
## ❗ 트러블슈팅
|
||||||
|
|
||||||
|
### 1. NodePort 포트 충돌
|
||||||
|
**증상**
|
||||||
```
|
```
|
||||||
브라우저
|
Service "frontend-service" is invalid: spec.ports[0].nodePort:
|
||||||
│
|
Invalid value: 30080: provided port is already allocated
|
||||||
▼ :30080 (NodePort)
|
```
|
||||||
[Nginx Frontend] ─── 정적 HTML/JS 제공
|
**원인** 해당 NodePort가 다른 서비스에서 이미 사용 중.
|
||||||
│ /api/* 프록시
|
|
||||||
▼
|
**해결**
|
||||||
[FastAPI Backend] ─── JWT 인증, REST API
|
`k8s/05-frontend.yaml` 에서 nodePort를 다른 번호로 변경 (30000~32767 범위):
|
||||||
│
|
```yaml
|
||||||
▼
|
nodePort: 30090
|
||||||
[PostgreSQL] ─── 사용자, 페이지, 권한 데이터
|
```
|
||||||
|
변경 후 기존 서비스 삭제 및 재적용:
|
||||||
|
```bash
|
||||||
|
kubectl delete service frontend-service -n web-portal
|
||||||
|
kubectl apply -f k8s/05-frontend.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Backend Liveness Probe 실패로 인한 CrashLoopBackOff
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
Liveness probe failed: HTTP probe failed with statuscode: 404
|
||||||
|
Back-off restarting failed container
|
||||||
|
```
|
||||||
|
**원인** Backend가 DB 연결을 기다리는 동안 liveness probe가 먼저 실패해서 K8s가 강제 재시작.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
`k8s/04-backend.yaml` 의 probe 대기 시간 증가:
|
||||||
|
```yaml
|
||||||
|
readinessProbe:
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 15
|
||||||
|
failureThreshold: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Nginx가 API 요청을 백엔드로 프록시 못함
|
||||||
|
**증상** 로그인 시 `Unexpected token '<', "<html>..." is not valid JSON` 에러.
|
||||||
|
|
||||||
|
**원인** Nginx가 `/api/` 요청을 백엔드로 전달하지 못하고 HTML을 반환.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
`frontend/nginx.conf` 에서 백엔드 주소를 FQDN으로 변경:
|
||||||
|
```nginx
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend-service.web-portal.svc.cluster.local:8000/api/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ArgoCD - authentication required: Unauthorized
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
failed to list refs: authentication required: Unauthorized
|
||||||
|
```
|
||||||
|
**원인** ArgoCD가 Gitea 저장소에 접근할 인증 정보가 없음.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
kubectl로 직접 인증 Secret 생성:
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic gitea-repo-secret \
|
||||||
|
--namespace=argocd \
|
||||||
|
--from-literal=type=git \
|
||||||
|
--from-literal=url=http://192.168.10.101:30000/<계정>/nginx-portal.git \
|
||||||
|
--from-literal=username=<계정> \
|
||||||
|
--from-literal=password=<패스워드>
|
||||||
|
|
||||||
|
kubectl label secret gitea-repo-secret \
|
||||||
|
-n argocd \
|
||||||
|
argocd.argoproj.io/secret-type=repository
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. RepeatedResourceWarning - 리소스 중복
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
Resource apps/Deployment/web-portal/backend appeared 2 times among application resources
|
||||||
|
```
|
||||||
|
**원인** `k8s/` 폴더 안에 동일한 리소스를 정의하는 yaml 파일이 중복 존재 (`portal.yaml` 등).
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
중복 파일 삭제 후 push:
|
||||||
|
```bash
|
||||||
|
rm k8s/portal.yaml
|
||||||
|
git add .
|
||||||
|
git commit -m "fix: 중복 yaml 파일 제거"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. ImagePullBackOff - Gitea Registry HTTP 접근 오류
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
Failed to pull image: server gave HTTP response to HTTPS client
|
||||||
|
```
|
||||||
|
**원인** Gitea Registry가 HTTP인데 Docker가 HTTPS로 접근 시도.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
Docker Desktop → Settings → Docker Engine에 insecure-registry 추가:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"insecure-registries": ["192.168.10.101:30000"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Apply & Restart 후 이미지 재빌드 & Push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Gitea Container Registry 로그인 실패 (context deadline exceeded)
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
Error response from daemon: Get "http://192.168.10.101:30000/v2/":
|
||||||
|
context deadline exceeded
|
||||||
|
```
|
||||||
|
**원인** Gitea의 ROOT_URL이 `localhost` 로 설정되어 있어 token 요청이 외부로 나가지 못함.
|
||||||
|
또한 Packages(Container Registry) 기능이 비활성화된 상태.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
Helm upgrade로 Gitea 설정 영구 변경:
|
||||||
|
```bash
|
||||||
|
helm repo add gitea https://dl.gitea.com/charts/
|
||||||
|
helm repo update
|
||||||
|
|
||||||
|
helm upgrade gitea gitea/gitea -n gitea \
|
||||||
|
--set gitea.config.server.DOMAIN=192.168.10.101 \
|
||||||
|
--set gitea.config.server.ROOT_URL=http://192.168.10.101:30000 \
|
||||||
|
--set gitea.config.server.HTTP_PORT=3000 \
|
||||||
|
--set gitea.config.packages.ENABLED=true \
|
||||||
|
--set service.http.type=NodePort \
|
||||||
|
--set service.http.nodePort=30000 \
|
||||||
|
--reuse-values
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. K8s 내부에서 Gitea Registry 이미지 Pull 실패
|
||||||
|
**증상** Pod가 `ImagePullBackOff` 상태. 외부에서는 push가 되지만 K8s Pod는 이미지를 못 가져옴.
|
||||||
|
|
||||||
|
**원인** K8s Pod는 외부 IP(`192.168.10.101:30000`)로 접근이 불안정하므로
|
||||||
|
내부 서비스명으로 접근해야 함.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
yaml의 image 주소를 K8s 내부 서비스명으로 변경:
|
||||||
|
```bash
|
||||||
|
sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/04-backend.yaml
|
||||||
|
sed -i "s|192.168.10.101:30000|gitea-http.gitea.svc.cluster.local:3000|g" k8s/05-frontend.yaml
|
||||||
|
```
|
||||||
|
Registry Secret도 내부 주소로 재생성:
|
||||||
|
```bash
|
||||||
|
kubectl delete secret gitea-registry-secret -n web-portal
|
||||||
|
|
||||||
|
kubectl create secret docker-registry gitea-registry-secret \
|
||||||
|
--namespace=web-portal \
|
||||||
|
--docker-server=gitea-http.gitea.svc.cluster.local:3000 \
|
||||||
|
--docker-username=<계정> \
|
||||||
|
--docker-password=<패스워드>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 로그인 실패 - 비밀번호 해시 오염
|
||||||
|
**증상** 올바른 계정으로 로그인해도 `아이디 또는 비밀번호가 올바르지 않습니다` 출력.
|
||||||
|
|
||||||
|
**원인** 터미널 색상 코드(`\x1B[0m` 등)가 비밀번호 해시에 섞여 DB에 저장됨.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
Backend Pod에서 직접 Python으로 해시 재생성 후 DB 업데이트:
|
||||||
|
```bash
|
||||||
|
kubectl exec -n web-portal deployment/backend -- python3 -c "
|
||||||
|
import bcrypt, psycopg2
|
||||||
|
conn = psycopg2.connect(host='postgres-service', database='portaldb', user='portaluser', password='portalpass')
|
||||||
|
cur = conn.cursor()
|
||||||
|
h1 = bcrypt.hashpw('admin1234'.encode(), bcrypt.gensalt()).decode()
|
||||||
|
h2 = bcrypt.hashpw('user1234'.encode(), bcrypt.gensalt()).decode()
|
||||||
|
cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h1, 'admin'))
|
||||||
|
cur.execute('UPDATE users SET password_hash=%s WHERE username=%s', (h2, 'user1'))
|
||||||
|
conn.commit()
|
||||||
|
print('완료')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. git push 거절 (non-fast-forward)
|
||||||
|
**증상**
|
||||||
|
```
|
||||||
|
error: failed to push some refs
|
||||||
|
hint: Updates were rejected because the tip of your current branch is behind
|
||||||
|
```
|
||||||
|
**원인** Gitea UI에서 직접 파일을 수정해서 로컬과 원격 브랜치가 diverge된 상태.
|
||||||
|
|
||||||
|
**해결**
|
||||||
|
```bash
|
||||||
|
git pull origin main --rebase
|
||||||
|
git push origin main
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user