Repository navigation
119 lines (109 loc) · 6.09 KB
/
Copy pathbackup.yml
File metadata and controls
119 lines (109 loc) · 6.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
name: Supabase weekly backup
# Supabase 무료 플랜은 자동 백업이 없다. 공식 문서도 "CLI로 직접 덤프해 외부에 보관하라"고
# 권고한다. 주 1회 pg_dump를 떠서 **비공개** 저장소에 커밋한다.
#
# 왜 아티팩트가 아니라 별도 저장소인가:
# 이 저장소(mind_map)는 PUBLIC이다. 공개 저장소의 Actions 아티팩트는 누구나 내려받을 수 있어,
# 사용자 데이터를 거기 올리면 백업이 아니라 유출이다.
#
# 필요한 저장소 Secrets (둘 다 사용자가 등록):
# BACKUP_DB_URL Supabase 대시보드 → Connect → Session pooler 연결 문자열(비밀번호 포함).
# GitHub 러너는 IPv4만 되고 Supabase 직접 연결은 IPv6라 **pooler**여야 한다.
# BACKUP_REPO_TOKEN myh7754/mind_map-backup 에 Contents: Read/Write 권한만 가진 fine-grained PAT.
#
# ponytail: 보관 정리는 하지 않는다. 덤프가 작아(수 MB) 당분간 쌓여도 문제없다.
# 저장소가 무거워지면 그때 오래된 파일을 지우는 단계를 넣는다.
#
# 덤프는 **평문**이다 (2026-10-01 사용자 결정). 즉 백업 저장소 접근권 = 전원 데이터 접근권이고,
# git 이력에 남아 과거 커밋의 데이터는 파일을 지워도 사라지지 않는다. 사용자가 늘어
# 그 위험이 커지면 age 대칭키 암호화를 넣는다(암호는 Secret + 비밀번호 관리자 양쪽에 보관).
on:
schedule:
# 매주 일요일 18:00 UTC (= 월요일 KST 03:00)
- cron: '0 18 * * 0'
workflow_dispatch:
jobs:
dump:
runs-on: ubuntu-latest
# 복원 리허설용 빈 Postgres. 복원해 보지 않은 백업은 백업이 아니다.
services:
verify:
image: postgres:17
env:
POSTGRES_PASSWORD: verify
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
ports:
- 5432:5432
steps:
- name: Postgres 17 클라이언트 설치
# 서버가 PG17이라 러너 기본 pg_dump(구버전)로는 덤프가 거부된다.
run: |
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -fsSL -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update -qq
sudo apt-get install -y -qq postgresql-client-17
pg_dump --version
- name: 백업 저장소 체크아웃
uses: actions/checkout@v4
with:
repository: myh7754/mind_map-backup
token: ${{ secrets.BACKUP_REPO_TOKEN }}
path: backup
- name: 덤프
env:
DB_URL: ${{ secrets.BACKUP_DB_URL }}
run: |
mkdir -p backup/dumps
STAMP=$(date -u +%Y-%m-%d)
OUT="backup/dumps/mindmap-$STAMP.sql"
# 전체 경로로 부른다 — PATH에는 러너 기본(구버전) pg_dump가 먼저 잡혀
# "server version mismatch"로 거부당한다.
/usr/lib/postgresql/17/bin/pg_dump "$DB_URL" \
--no-owner --no-privileges --schema=public --schema=auth -f "$OUT"
gzip -f "$OUT"
ls -l "$OUT.gz"
# 빈 덤프를 백업이라 부르지 않는다. 10KB 미만이면 뭔가 잘못된 것이다.
SIZE=$(stat -c%s "$OUT.gz")
[ "$SIZE" -ge 10240 ] || { echo "덤프가 너무 작다($SIZE bytes) — 실패 처리"; exit 1; }
# 내용 확인: maps 테이블의 COPY 구문이 있어야 한다.
# zgrep -q 를 쓰지 말 것 — 첫 일치에서 파이프를 닫아 gzip이 SIGPIPE로 죽고,
# 일치했는데도 실패 코드가 돌아온다(2026-10-01에 이걸로 한 번 속았다).
COPIES=$(zcat "$OUT.gz" | grep -c "COPY public\.maps" || true)
echo "maps COPY 구문 $COPIES 개"
[ "$COPIES" -ge 1 ] || { echo "maps 데이터가 덤프에 없다 — 실패 처리"; exit 1; }
- name: 복원 리허설
# 덤프가 "열리기만" 하는지가 아니라 **되살아나는지**를 본다.
# 원본 행 수와 복원 후 행 수가 같아야 통과.
env:
DB_URL: ${{ secrets.BACKUP_DB_URL }}
PGBIN: /usr/lib/postgresql/17/bin
run: |
LOCAL="postgresql://postgres:verify@localhost:5432/postgres"
SRC=$("$PGBIN/psql" "$DB_URL" -tAc "select count(*) from public.maps")
echo "원본 maps 행 수: $SRC"
# auth 스키마는 Supabase 전용 롤·확장을 참조할 수 있어 일부 구문이 실패한다.
# 목적은 완전 복제가 아니라 "데이터가 살아나는가"이므로 멈추지 않고 진행한다.
zcat backup/dumps/mindmap-*.sql.gz | "$PGBIN/psql" "$LOCAL" -v ON_ERROR_STOP=0 -q > /tmp/restore.log 2>&1 || true
tail -5 /tmp/restore.log
DST=$("$PGBIN/psql" "$LOCAL" -tAc "select count(*) from public.maps")
echo "복원된 maps 행 수: $DST"
[ "$SRC" = "$DST" ] || { echo "복원 결과가 원본과 다르다 ($SRC vs $DST) — 실패 처리"; exit 1; }
# 내용까지 한 겹 더: 노드가 들어 있는지 (빈 껍데기 복원 방지)
NODES=$("$PGBIN/psql" "$LOCAL" -tAc \
"select coalesce(sum(jsonb_array_length(coalesce(jsonb_path_query_array(data->'nodes','\$.keyvalue()'),'[]'::jsonb))),0) from public.maps")
echo "복원된 노드 총합: $NODES"
[ "$NODES" -gt 0 ] || { echo "복원된 맵에 노드가 없다 — 실패 처리"; exit 1; }
- name: 커밋
working-directory: backup
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add dumps
git diff --cached --quiet && { echo "변경 없음"; exit 0; }
git commit -m "backup: $(date -u +%Y-%m-%d)"
git push