Obsidian 플러그인 릴리스에서 styles.css가 사라지는 이유: GitHub Actions 워크플로우의 빠진 한 줄
커스텀 summary card 디자인을 새로 도입하면서 styles.css가 생겼는데, release.yml의 gh release upload 커맨드에 파일명을 빠뜨려서 GitHub Release asset에서 누락됐습니다. 사용자에게는 "스타일이 안 보인다"는 버그로 나타났고, 2줄 fix로 해결했습니다. Obsidian 플러그인 release workflow 체크리스트도 함께 정리.
1. 증상 — "커스텀 UI가 안 보여요"
issue #56에 이렇게 적혀 있었습니다.
v0.8.0 설치 후 설정 탭의 summary card 테두리와 badge 색이 안 보입니다. 테마 문제인가요?
증상 자체는 간단했지만 첫 반응은 "이 사용자의 Obsidian 테마가 이상한가?"였습니다. Summary card의 시각적 스타일은 styles.css에 정의돼 있고, 로컬 개발 vault에서는 잘 보였기 때문입니다.
그런데 사용자에게 플러그인 폴더를 직접 확인해 달라고 했더니 결과가 이상했습니다.
.obsidian/plugins/auto-note-importer/
├── main.js
├── manifest.json
└── (styles.css 없음)
styles.css 파일이 없었습니다. 사용자는 Obsidian의 플러그인 설치 기능을 통해 GitHub Release에서 다운로드 받은 상태였고, 릴리스 페이지의 asset 목록을 확인해 보니 거기에도 없었습니다.
로컬에서는:
dist/
├── main.js
├── manifest.json
└── styles.css # ← 여기 있음
빌드 결과물에는 분명히 포함돼 있었습니다. 그런데 릴리스 자산에는 없었습니다. 그 사이에 누군가가 styles.css를 빠뜨리고 있었습니다.
2. 원인 — release.yml의 한 줄
범인은 .github/workflows/release.yml이었습니다. GitHub Actions에서 release 빌드를 만들고 자산을 업로드하는 워크플로우입니다.
# Before: styles.css가 없음
- name: Create or update release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
existing=$(gh release list --json tagName,isDraft -q '.[] | select(.isDraft) | .tagName' | head -1)
if [ -n "$existing" ]; then
# Upload assets to existing draft and update tag/title
gh release upload "$existing" main.js manifest.json --clobber
gh release edit "$existing" --tag "$tag" --title "$tag" --draft=false
else
# No draft exists — create and publish directly
gh release create "$tag" \
--title="$tag" \
main.js manifest.json
fi
gh release upload와 gh release create 두 곳 모두에 main.js manifest.json 두 파일만 넣혀 있고 styles.css가 빠져 있었습니다.
2.1 왜 발견이 늦었나
이런 류의 버그가 오래 살아남는 이유는 세 가지입니다.
첫째, 개발 vault와 릴리스 asset의 경로가 다르다. 개발 환경에서는 npm run build가 생성한 로컬 dist/의 파일을 vault에 직접 복사하거나 symlink해서 씁니다. styles.css가 당연히 존재합니다. 반면 사용자 환경에서는 GitHub Release의 zip을 Obsidian이 다운로드해서 풀어 씁니다. 개발자가 자기 설치를 통해 버그를 발견할 기회가 없습니다.
둘째, 플러그인이 대체로 작동한다. styles.css가 없어도 플러그인의 기능은 전부 동작합니다. 파일 싱크, command palette, settings 페이지 열기 — 다 됩니다. 시각적 디테일만 사라집니다. 테두리가 없고, badge 색이 디폴트이고, card 배경이 없는 정도입니다. 사용자가 "원래 이렇게 생긴 건가?"라고 생각하기 쉽습니다.
셋째, 이전 버전에는 styles.css가 없었다. v0.7.x까지 플러그인은 Obsidian의 기본 스타일만 쓰고 커스텀 CSS가 없었습니다. v0.8.0에서 summary card 디자인을 도입하면서 styles.css가 처음 생겼습니다. 새로 생긴 파일을 release 워크플로우에 추가하는 걸 놓친 것입니다. 기존 파일 목록(main.js, manifest.json)은 plugin 템플릿에서 온 것이었고, 새 파일을 체크해야 한다는 생각 자체가 빠졌습니다.
3. 수정 — 2줄
# After
if [ -n "$existing" ]; then
gh release upload "$existing" main.js manifest.json styles.css --clobber
# ^^^^^^^^^^
gh release edit "$existing" --tag "$tag" --title "$tag" --draft=false
else
gh release create "$tag" \
--title="$tag" \
main.js manifest.json styles.css
# ^^^^^^^^^^
fi
2곳, 각 styles.css 한 단어 추가. 검증은 다음 릴리스(v0.8.1)에서 실제로 styles.css가 asset 목록에 올라오는지 확인하는 것으로 마쳤습니다.
이런 단순한 fix는 코드 리뷰를 받기도 애매합니다. "파일명 하나 빠진 걸 고침"이라 누구라도 한 번 보고 merge할 수 있습니다. 그러나 이런 류의 커밋은 재발 방지 체크리스트를 코드베이스에 남겨놓을 기회입니다. 그 이야기를 섹션 5에서 합니다.
4. Obsidian 플러그인의 릴리스 자산 표준
이 기회에 Obsidian이 플러그인 설치 시 어떤 파일을 기대하는지를 정리했습니다.
4.1 필수 파일
| 파일 | 역할 | 필수? |
|---|---|---|
main.js |
빌드된 JavaScript entry point | ✅ |
manifest.json |
플러그인 메타데이터 (id, name, version, minAppVersion) | ✅ |
styles.css |
커스텀 CSS | 필요할 때만 ✅ |
versions.json |
플러그인 버전별 minAppVersion 매핑 (vault 레벨에 존재) | ❌ (옵션) |
4.2 styles.css가 언제 필요한가
Obsidian 플러그인 API는 onload() 시점에 styles.css 파일이 있으면 자동으로 문서에 주입합니다. 플러그인 코드에서 document.head.appendChild(styleEl) 같은 것을 직접 하지 않아도 됩니다. 파일만 있으면 알아서 적용됩니다.
이 자동 주입은 편의이지만 동시에 함정이기도 합니다. 개발자는 styles.css가 존재한다는 것을 코드에서 의식적으로 다루지 않기 때문에, 파일이 빌드/릴리스 경로에서 빠져도 코드 리뷰에서는 눈에 띄지 않습니다.
4.3 Obsidian 플러그인 릴리스 체크리스트
앞으로 이런 실수를 방지하기 위한 체크리스트:
- [ ]
manifest.json의version필드가 릴리스 태그와 일치하는가 - [ ] **
manifest.json의minAppVersion**이 올바른가 (지원 최소 Obsidian 버전) - [ ]
main.js가 빌드됐는가 (npm run build후dist/main.js확인) - [ ]
styles.css가 프로젝트 루트에 있는가 (있으면 릴리스에 포함) - [ ] GitHub Actions
release.yml에 모든 필수 파일이 명시돼 있는가 - [ ] 릴리스 후
gh release view <tag>로 asset 목록 수동 확인 - [ ] 실제 사용자 vault에 설치해서 smoke test (로컬 개발 vault 말고 완전히 깨끗한 vault)
특히 마지막 두 개는 자동화할 수 없지만 필수적인 체크입니다. CI가 릴리스를 찍는 데까지는 자동화할 수 있어도 "사용자가 다운받았을 때 실제로 작동하는가"는 누군가 한 번은 수동으로 확인해야 합니다.
5. 재발 방지 — Workflow가 "바뀐 파일 목록"을 자동으로 감지하게
2줄 fix 자체는 간단하지만, 같은 류의 실수가 다시 일어나지 않게 하려면 워크플로우 자체가 방어적으로 설계돼야 합니다. 몇 가지 옵션이 있습니다.
5.1 Option A: 빌드 결과 디렉토리 통째로 업로드
- name: Upload all dist files
run: |
# dist/* 전체를 업로드
gh release upload "$tag" dist/* --clobber
장점: 새 파일이 추가돼도 자동으로 포함됨.
단점: 의도치 않은 파일(source map, backup)도 포함될 수 있음. dist/ 구성이 깨끗하지 않으면 위험.
5.2 Option B: 파일 목록을 manifest.json에서 읽기
- name: Read expected assets
id: assets
run: |
assets=$(jq -r '.files[]?' manifest.json | tr '\n' ' ')
echo "list=${assets:-main.js manifest.json}" >> $GITHUB_OUTPUT
- name: Upload assets
run: |
gh release upload "$tag" ${{ steps.assets.outputs.list }} --clobber
장점: 파일 목록이 manifest에 중앙화돼 있으면 코드·릴리스 양쪽이 일치.
단점: manifest.json에 files 필드는 표준 아님 (Obsidian이 읽지 않음). 관리 포인트가 또 하나 생김.
5.3 Option C: 체크리스트 스크립트 + CI 검증
- name: Validate release assets
run: |
required=(main.js manifest.json)
[ -f styles.css ] && required+=(styles.css)
for f in "${required[@]}"; do
if [ ! -f "$f" ]; then
echo "::error::Missing required asset: $f"
exit 1
fi
done
gh release upload "$tag" "${required[@]}" --clobber
장점: 파일별로 명시적 체크. styles.css가 있는 경우에만 조건적으로 포함. 빌드 결과가 의도와 다르면 즉시 실패.
단점: 워크플로우가 조금 길어짐.
이 프로젝트는 현재 Option A의 수정된 버전 — 파일명을 명시적으로 나열하되, 새 파일이 추가되면 수동으로 워크플로우도 업데이트 — 를 유지하고 있습니다. Option C로의 마이그레이션은 다음에 파일이 하나 더 추가될 때(예: 플러그인이 locale json을 도입할 때) 같이 하기로 했습니다.
5.4 최소한의 post-release 검증
워크플로우 마지막에 "방금 만든 릴리스에 기대하는 파일이 모두 들어갔나" 검증 스텝을 넣을 수도 있습니다.
- name: Verify release assets
run: |
tag="${GITHUB_REF#refs/tags/}"
expected=(main.js manifest.json styles.css)
actual=$(gh release view "$tag" --json assets -q '.assets[].name' | sort)
missing=""
for f in "${expected[@]}"; do
if ! echo "$actual" | grep -qx "$f"; then
missing="$missing $f"
fi
done
if [ -n "$missing" ]; then
echo "::error::Missing release assets:$missing"
exit 1
fi
이 스텝은 파일 누락을 릴리스가 published된 직후에 발견해서 워크플로우를 실패로 만듭니다. 수동 smoke test의 첫 단계를 자동화한 것입니다.
6. 관련 교훈 — "로컬에서 되는데요"의 가장 흔한 원인
이 버그는 "My machine works!"의 고전적 케이스입니다. 개발자 환경과 사용자 환경 사이에 빌드 → 배포 → 설치라는 세 단계가 있고, 각 단계에서 파일 목록이 달라질 수 있습니다.
| 환경 | 파일 출처 | styles.css 존재? |
|---|---|---|
| 개발 vault | 로컬 dist/ 직접 사용 |
✅ |
| CI 빌드 산출물 | npm run build 결과 |
✅ |
| GitHub Release asset | release.yml이 지정한 파일만 |
❌ (버그) |
| 사용자 vault | Obsidian이 Release에서 다운로드 | ❌ (버그 영향) |
버그가 있는 경로는 CI 빌드 → Release asset의 한 단계입니다. 빌드는 성공해서 파일이 만들어지지만, 그 파일을 release에 올리는 스텝에서 파일명이 누락된 것입니다.
일반화: 이런 버그를 방지하려면 "빌드 결과물 → 배포 자산"의 경계에서 파일 목록이 명시적으로 검증돼야 합니다. 빌드 결과물이 dist/에 있다고 해서, release.yml이 자동으로 dist/*를 전부 올릴 거라고 가정하면 안 됩니다. 이 단계의 경계는 명시적이고 검증 가능해야 합니다.
7. FAQ
Q: styles.css를 JavaScript에서 import하는 방식(webpack 등)으로 main.js에 inline할 수 없나요?
A: 가능합니다. esbuild나 webpack으로 CSS를 JS 번들에 주입하면 파일 하나로 줄일 수 있습니다. 다만 Obsidian 플러그인 빌드 설정이 이미 styles.css를 별도 출력으로 내는 구조였고, 다른 Obsidian 플러그인 커뮤니티와의 convention 호환성을 유지하려고 파일 분리 구조를 유지했습니다. "다른 Obsidian 플러그인들이 다 이렇게 한다"는 건 충분한 이유입니다 — 개발자들이 기대하는 convention을 깨면 PR 기여 장벽이 올라갑니다.
Q: GitHub Release의 asset 목록을 CI에서 자동으로 확인할 수 있나요?
A: 네. gh release view <tag> --json assets로 asset 목록을 JSON으로 받아서 파싱할 수 있습니다. 섹션 5.4에 예시가 있습니다. "기대하는 파일 목록 vs 실제 asset 목록"을 diff하면 빠진 파일을 즉시 감지할 수 있습니다.
Q: release-drafter의 draft release에 자산을 업로드하는 방식이 안정적인가요?
A: 지금까지는 안정적입니다. 워크플로우는 먼저 gh release list --json tagName,isDraft로 draft 릴리스를 조회하고, 있으면 그 release에 자산을 올린 뒤 gh release edit --draft=false로 퍼블리시합니다. 없으면 새 release를 즉시 만듭니다. 이 dual-path 구조는 release-drafter가 활성/비활성인 경우 모두를 커버합니다.
Q: 이 버그가 Obsidian 플러그인 외의 영역에도 적용되나요?
A: 네. Chrome Extension, VS Code Extension, 어떤 CI/CD 파이프라인이든 **"빌드 산출물과 배포 자산의 파일 목록이 다른 곳에 정의되는 구조"**면 동일한 버그가 발생할 수 있습니다. 일반화된 방지책은 "산출물 → 자산" 단계에서 파일 목록을 명시적으로 검증하는 CI 스텝입니다.
Q: 기존 사용자가 업그레이드하면 자동으로 고쳐지나요?
A: 네. v0.8.1 이상으로 업그레이드하면 Obsidian이 최신 asset(styles.css 포함)을 다운로드해서 플러그인 폴더에 저장합니다. 사용자가 할 일은 없고, 플러그인 설정에서 "Check for updates"를 누르거나 다음 Obsidian 재시작 때 자동으로 업데이트됩니다.
8. 참고 자료
- Obsidian Plugin Developer Docs — Release your plugin
- GitHub CLI —
gh release - release-drafter Action
- 검색 키워드: "obsidian plugin styles.css not loading"
- 검색 키워드: "github release missing asset"