Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release to Maven Central

# Cutting a release is a tag push: `git tag v0.1.0 && git push origin v0.1.0`.
# This publishes every library module to Maven Central (Central Portal, signed
# with the org GPG key) and opens a GitHub Release with generated notes.
#
# Requires these org-level secrets (already configured on devslab-kr):
# MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD — Central Portal token
# SIGNING_KEY / SIGNING_KEY_ID / SIGNING_KEY_PASSWORD — ASCII-armored GPG key
# If the org secrets are scoped to "selected repositories", grant this repo access.

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
publish:
name: Publish to Maven Central
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Java 21 (GraalVM CE)
uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm-community'
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Publish to Maven Central
run: ./gradlew publishToMavenCentral --no-daemon --no-configuration-cache --stacktrace
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
83 changes: 83 additions & 0 deletions CHANGELOG.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 변경 이력 (Changelog)

이 프로젝트의 주요 변경 사항을 기록합니다.

형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[유의적 버전(SemVer)](https://semver.org/lang/ko/)을 준수합니다.

라이브러리 메이저는 Spring Boot 메이저와 정렬됩니다: `4.x.y`는 Spring Boot 4.x를 대상으로 합니다.

English: [CHANGELOG.md](CHANGELOG.md)

## [Unreleased]

### Added
- **Maven Central 배포** — 모든 라이브러리 모듈을 vanniktech maven-publish 플러그인으로
Maven Central에 배포합니다 (Central Portal, 서명, `v*` 태그 시 자동 릴리스).
`release.yml`이 아티팩트를 배포하고 GitHub Release를 생성합니다.
- **플러그형 캐시** (ADR 0002) — `devslab.kit.cache.type` = `in-memory` / `redis` /
`none`. Redis 백엔드가 JSON 직렬화를 직접 책임지므로 (`Serializable` 불필요, 직렬화기
설정 불필요) 사용자별 메뉴 캐시도 자체 맵 대신 이 공유 캐시 매니저를 사용합니다.
- **최초 관리자 부트스트랩** (ADR 0001) — opt-in, 프로퍼티 기반 프로비저닝으로 빈 데이터베이스가
영구적인 백도어 없이 사용 가능한 대시보드에 도달합니다. `devslab.kit.bootstrap.*`(기본 OFF)가
첫 부팅 시 테넌트, 전체 `admin.*` 권한 세트를 가진 `PLATFORM_ADMIN` 역할, 관리자 사용자 1명을
멱등하게 생성합니다. 빈 비밀번호는 강력한 랜덤 비밀번호를 생성해 한 번 로깅하며, prod 안전장치는
`prod`/`production` 프로파일에서 약한 비밀번호를 거부합니다.
- **비밀번호 강제 변경** — 사용자 계정의 `must_change_password` 플래그(`V11`)를 `CurrentUser`,
JWT 클레임, 로그인 응답으로 노출합니다. 셀프 서비스 `POST /admin/api/v1/auth/change-password`가
이전 비밀번호를 검증하고 새 비밀번호를 설정한 뒤 플래그를 지우고 토큰을 재발급합니다.
- **부트스트랩 상태 프로브** — 인증 없는 `GET /admin/api/v1/bootstrap/status`가
`{ initialized: boolean }`을 반환합니다. 향후 가이드형 최초 실행 / 설정 마법사의 분기점입니다
(ADR 0001 §6).

### Fixed
- **JWT 검증이 주입된 `Clock`을 사용하도록 수정** — `JjwtAuthTokenService.parse()`가 토큰 만료를
주입된 시계가 아니라 실제 시스템 시계로 검증하고 있었습니다. 이로 인해 고정 시계로는 검증을
테스트할 수 없었고 `issue()`와 비대칭이었습니다. 운영 동작은 그대로입니다 (런타임은 양쪽 모두
`Clock.systemUTC()` 사용).

### Changed
- `sample-app`이 `SampleSeedRunner`를 끄고 스타터의 `devslab.kit.bootstrap.*` 러너로
전환했습니다 (로컬 개발 형태: `admin/admin`, `must-change-password=false`).

### Added (초기 스캐폴드)
- 초기 프로젝트 스캐폴드 (Spring Boot 4 + Java 21 + Gradle).
- 기본 의존성: Spring Web MVC, Spring Security, Spring Data JPA, Spring Data Redis,
Flyway (PostgreSQL), Spring Boot Actuator, GraalVM Native, Testcontainers (PostgreSQL + Redis),
Docker Compose 지원.
- 기본 패키지 `kr.devslab.kit`.
- Gradle 멀티모듈 분리 (계획 문서 §5):
`devslab-kit-core`, `-{identity,access,tenant,menu,audit}-{api,core}`,
`-autoconfigure`, `-spring-boot-starter`, `-sample-app`.
- 코어 값 객체: `UserId`, `TenantId`, `RoleId`, `PermissionId`, `MenuId`, `PublicId`,
`DevslabKitException`.
- Tenant: `TenantContext`, `TenantContextHolder`, `TenantResolver`, `TenantMode` (api) +
`DefaultTenantContextHolder`, `FixedTenantResolver` (core). AutoConfig override 패턴을
증명하는 데 사용한, 완전히 배선된 첫 수직 슬라이스.
- Identity (api): `CurrentUser`, `CurrentUserProvider`, `UserStatus`, `LoginCommand`,
`LoginResult`, `UserAccountView`, `PasswordHasher`, `LoginFailureReason`,
`AccountLoginException`, `LoginSucceededEvent`, `LoginFailedEvent`,
`UserAccountCreatedEvent`.
Identity (core): `PlatformUserAccountEntity` + `JpaPlatformUserAccountRepository`,
`BCryptPasswordHasher`, `LocalLoginService`, `PlatformUserAccountService`,
`DefaultCurrentUserProvider`, `V1__platform_user_account.sql`.
- Access (api): `Permission`, `Role`, `PermissionChecker`, `PermissionDeniedException`.
Access (core): `Platform{Role,Permission,UserRole,RolePermission}Entity` + Jpa 리포지토리,
`UserRoleService`, `RolePermissionService`, `DefaultPermissionChecker`, `V2__platform_access.sql`.
- Menu (api): `MenuItem`, `MenuTree`, `MenuProvider`.
Menu (core): `PlatformMenuEntity` + `JpaPlatformMenuRepository`, `MenuTreeBuilder`,
`PermissionBasedMenuFilter`, `DefaultMenuProvider`, `V3__platform_menu.sql`.
- Audit (api): `AuditEvent`, `AuditActor`, `AuditAction`, `AuditTarget`, `AuditEventPublisher`.
Audit (core): `PlatformAuditLogEntity` + `JpaPlatformAuditLogRepository`, `AuditLogService`
(Jackson 직렬화 메타데이터), `DefaultAuditEventPublisher`, `V4__platform_audit_log.sql`.
- `DevslabKitProperties` (`devslab.kit.*` 접두사) + `@ConditionalOnMissingBean` override를 갖춘
5개 `AutoConfiguration`: `Tenant`, `Identity`, `Access`, `Menu`, `Audit`.
- `devslab-kit-sample-app`이 8개 스타터 빈(`TenantResolver`, `TenantContextHolder`,
`CurrentUserProvider`, `PasswordHasher`, `LocalLoginService`, `PermissionChecker`,
`MenuProvider`, `AuditEventPublisher`)과 BCrypt 왕복을 스모크 테스트합니다.

### Notes
- 아직 `0.1.0` 이전 스캐폴드이며, `0.1.0` 전에 공개 표면이 바뀔 수 있습니다.
- SB 4.1-SNAPSHOT / Java 25 대신 Spring Boot 4.0.6(릴리스)와 Java 21을 대상으로 합니다.
SB SNAPSHOT + Java 25 조합이 일부 환경에서 IntelliJ Gradle 통합을 깨뜨려, 릴리스 버전으로
고정해 import 경로를 예측 가능하게 유지합니다. SB 4.1.x 릴리스가 나오면 Java 25를 재검토합니다.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

The library major aligns with the Spring Boot major: `4.x.y` targets Spring Boot 4.x.

한국어: [CHANGELOG.ko.md](CHANGELOG.ko.md)

## [Unreleased]

### Added
- **Maven Central publishing** — every library module ships to Maven Central via
the vanniktech maven-publish plugin (Central Portal, signed, auto-release on a
`v*` tag). `release.yml` publishes the artifacts and opens a GitHub Release.
- **Pluggable cache** (ADR 0002) — `devslab.kit.cache.type` = `in-memory` /
`redis` / `none`. The Redis backend owns JSON serialization (no `Serializable`,
no serializer wiring), and the per-user menu cache now rides this shared cache
manager instead of its own map.
- **First-admin bootstrap** (ADR 0001) — opt-in, property-driven provisioning so
a fresh database can reach a usable dashboard without a permanent backdoor.
`devslab.kit.bootstrap.*` (OFF by default) idempotently creates a tenant, a
Expand All @@ -25,6 +34,13 @@ The library major aligns with the Spring Boot major: `4.x.y` targets Spring Boot
returning `{ initialized: boolean }`, the branch point for a future guided
first-run / setup wizard (ADR 0001 §6).

### Fixed
- **JWT validation now honours the injected `Clock`** — `JjwtAuthTokenService.parse()`
validated token expiry against the real system clock instead of the injected one,
making validation untestable with a fixed clock and asymmetric with `issue()`.
Production behaviour is unchanged (the runtime uses `Clock.systemUTC()` on both
paths).

### Changed
- `sample-app` switched off its `SampleSeedRunner` onto the starter's
`devslab.kit.bootstrap.*` runner (local-dev shape: `admin/admin`,
Expand Down
47 changes: 46 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import com.vanniktech.maven.publish.MavenPublishBaseExtension

// vanniktech maven-publish on the buildscript classpath so this root script can
// `import` its extension type and configure publishing for the library modules.
// (Declaring it `apply false` in plugins {} does not reliably expose the classes
// to the root script's compilation under Gradle 9.x — buildscript {} does.)
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("com.vanniktech:gradle-maven-publish-plugin:0.36.0")
}
}

plugins {
id("org.springframework.boot") version "4.0.6" apply false
id("io.spring.dependency-management") version "1.1.7" apply false
Expand All @@ -6,14 +22,19 @@ plugins {

allprojects {
group = "kr.devslab"
version = "0.0.1-SNAPSHOT"
version = "0.1.0-SNAPSHOT"

repositories {
mavenCentral()
}
}

// Every module ships to Maven Central except the runnable reference app.
val nonPublishedModules = setOf("devslab-kit-sample-app")

subprojects {
val proj = this

apply(plugin = "java-library")
apply(plugin = "io.spring.dependency-management")

Expand Down Expand Up @@ -45,4 +66,28 @@ subprojects {
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

// --- Maven Central publishing for the library modules (not the sample app) ---
if (proj.name !in nonPublishedModules) {
apply(plugin = "com.vanniktech.maven.publish")

configure<MavenPublishBaseExtension> {
// SONATYPE_HOST / SONATYPE_AUTOMATIC_RELEASE and the POM_* metadata
// come from gradle.properties. signAllPublications() enables GPG
// signing (with the org key) for releases and is a no-op for
// -SNAPSHOT, so publishToMavenLocal needs no key.
//
// Do NOT also set RELEASE_SIGNING_ENABLED in gradle.properties: it
// configures signing too, and combining the two double-sets the
// signing property, which Gradle 9 rejects as "value is final".
signAllPublications()
coordinates(proj.group.toString(), proj.name, proj.version.toString())
pom {
name.set(proj.name)
// Each module sets its own `description` in build.gradle.kts; read
// it lazily since module scripts evaluate after this block runs.
description.set(proj.provider { proj.description ?: "devslab-kit :: ${proj.name}" })
}
}
}
}
22 changes: 22 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# --- Maven Central publishing (vanniktech maven-publish) ---------------------
# Publishes through the new Central Portal and auto-promotes the deployment, so a
# `v*` tag push releases without a manual "close + release" in the portal UI.
SONATYPE_HOST=CENTRAL_PORTAL
SONATYPE_AUTOMATIC_RELEASE=true

# Shared POM metadata. Per-module <name>/<description> are set in build.gradle.kts
# (each module's own `description`). These apply to every published artifact.
POM_INCEPTION_YEAR=2026
POM_URL=https://github.com/devslab-kr/devslab-kit

POM_LICENSE_NAME=The Apache License, Version 2.0
POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENSE_DIST=repo

POM_DEVELOPER_ID=devslab-kr
POM_DEVELOPER_NAME=devslab
POM_DEVELOPER_URL=https://github.com/devslab-kr

POM_SCM_URL=https://github.com/devslab-kr/devslab-kit
POM_SCM_CONNECTION=scm:git:git://github.com/devslab-kr/devslab-kit.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/devslab-kr/devslab-kit.git
Loading