SVN에서 릴리즈 관리하기
잡동사니
소프트웨어 형상 관리(SCM)는 버전 관리 기능을 포함하고 있스비다. 하지만 이 말이 SCM을 이용하면 릴리즈 버전 관리가 바로 된다는 뜻은 아닙니다. 이 문서는 널리 쓰이는 Subversion에서 릴리즈를 관리하는 방법에 대해 설명합니다.
목차 |
준비 사항
저장소
먼저 릴리즈할 대상이 되는 저장소(repository)가 필요합니다. 이 문서의 내용을 적용할 적당한 저장소가 없거나, 연습을 해본 뒤 적용하고 싶다면 다음 절차를 통해 새로 생성할 수 있습니다.(아래에서 필요한 파일이 포함된 저장소 반출 파일을 받아 사용할 수도 있습니다.)
$ svnadmin create /path/to/repository
보다 자세한 설명은 피라시스닷컴의 문서를 참조하시기 바랍니다. MS 윈도 환경에서는 TortoiseSVN을 통해 자신의 컴퓨터에 저장소를 생성할 수 있습니다.
배포 경로
생성될 파일을 HTTP 등을 통해 다운로드 받을 수 있게 할 곳이 필요합니다. 여기서는 /path/to/download라 가정합니다.
빌드 파일
이 문서의 목적은 수작업이 아니라 한 번의 명령으로 릴리즈 파일을 생성하는 것이기 때문에 스크립트에서 결과물을 만들어 낼 수 있는 빌드 파일이 필요합니다. 여기서는 저장소의 /trunk/build.xml 파일이 Ant 빌드 파일이라 가정합니다.
배포 파일
배포를 하기위해 생성되는 파일은 모두 /trunk/dist 디렉터리 아래에 생성되며 각 파일은 다음과 같다고 가정합니다.
-
${PROJECT}.jar - 바이너리 배포 파일입니다.
-
${PROJECT}-src.zip - 소스 압축 파일입니다.
-
${PROJECT}-javadoc.zip - JavaDoc 압축 파일입니다.
버전 붙이기 규칙
버전은 마침표(.)로 구분 되는 세 자리 숫자로 합니다. 첫 번째는 메이저(major), 두 번째는 마이너(minor) 버전이고 세 번째는 패치 번호입니다. 또한 일반적이지는 않지만 패치는 전체 길이를 두 자리로 하며 자리수에 맞춰 0을 채우기로 합니다.
-
1.2.03 - 이 버전은 첫 번째 메이저 출시의 두 번째 마이너 버전이며, 패치는 세 번째라는 뜻입니다.
관련 파일
- 파일:Rmis-r4.dump
- 이 파일은 위에서 설명한 Ant 빌드 파일과 'Hello, World'를 출력하는
foo.BarJava 파일이 포함되어 있습니다. 다음 명령어로 미리 준비된(svnadmin create명령어로 생성된) 저장소에 반입할 수 있습니다. -
$ svnadmin load /path/to/repository < Rmis-r4.dump
- 이 프로젝트의 프로젝트 이름(
${PROJECT})는 'helloworld'입니다.
릴리즈
가지(branch) 기준
프로젝트가 잘 진행이 되고, 이제 제품을 출시할 시기가 되었습니다. 개발의 한 주기가 끝나고 출시를 하면 다음 버전 개발을 시작하기 때문에 기존의 소스에 많은 변경이 가해지게 됩니다. 따라서 기존의 소스를 줄기(trunk)에 그대로 남겨두면 버그 수정과 같은 작업을 하기 어려워집니다. 따라서 출시를 하기 전에 저장소에 가지(branch)를 새로 생성하기로 합시다.
새로 생성할 브랜치 이름은 접두어 REL-을 붙이기로 합니다. 다만 SVN에는 가지 이름에 제약이 없기 때문에 버전명을 그대로 사용하기로 합니다. 그리고 한 줄기 안에서는 패치가 여러 번 이뤄질 것이기 때문에 가지 이름에는 마이너 버전까지만 포함하기로 합니다. 따라서 첫 출시라면 /branch/REL-1.0가 되겠지요.
가지치기(branching)
위해서 정한 규칙에 따라서 첫 출시를 위한 가지치는 SVN 명령어는 다음과 같습니다. 단, 아직 실행하지는 마시기 바랍니다. 우리의 목적은 이 작업을 각각 수작업으로 하는 게 아니라 자동화하는 것에 있기 때문입니다.
svn copy file:///path/to/repo/trunk file:///path/to/repo/branches/REL-1.0
여기서 각 상황(프로젝트, 릴리즈 번호 등)에 따라 바뀔 수 있는 부분은 저장소 경로인 file:///path/to/repo와 릴리즈 번호인 1.0입니다. 이 값들을 각각 $URL, $MAJOR, $MINOR라 합시다.
패치
꼬리표(tag) 기준
가지가 각 출시를 위한 배경이었다면 꼬리표는 각 출시와 직접 연관이 있습니다. 즉 패치를 거듭하여 출시를 여러 번 하더라도 가지는 하나만 필요한 반면, 꼬리표는 출시된 각 제품의 소스를 온전히 가지고 있어야 하기 때무에 패치를 한 만큼 꼬리표도 생성이 됩니다.
출시를 위한 꼬리표도 접두어를 REL-로 사용하기로 합시다. 그리고 꼬리표는 출시된 각 제품 하나하나와 직접 연관이 되기 때문에 세 자리 버전명을 모두 사용하기로 합시다. 따라서 맨 첫 출시를 위한 꼬리표는 /tags/REL-1.0.00가 됩니다.
꼬리표 달기(tagging)
꼬리표 달기 역시 SNV의 copy 명령어를 사용합니다. 역시 다음 명령어를 바로 실행하지 마시기 바랍니다.
svn copy file:///path/to/repo/branches/REL-1.0 file:///path/to/repo/tags/REL-1.0.00
여기서도 바뀔 수 있는 부분은 #가지치기(branching)에서 나온 부분 이외에 패치 수인 00가 있습니다.
스크립트 작성
이제 위에서 나온 내용을 토대로 스크립트를 작성해 보겠습니다. 스크립트 이름은 version.sh라 하고 위에서 언급한 기능을 한 파일에서 처리합시다.
먼저 해당 시스템에 svn 명령어가 있는지 확인해야겠지요. 이 스크립트는 다음과 같습니다.
#!/bin/sh # SVN 명령어를 찾습니다. SVN=`which svn 2 > /dev/null` # SVN 명령어를 발견하지 못하면 에러 메시지지를 출력하고 종료합니다. if [ -z $SVN]; then echo "svn is not found" > &2 exit 1 fi
다음으로 필요한 기능은 출시 줄기를 작성하는 기능입니다. 이 기능은 release는 인자과 합께 사용하기로 합시다. 또 줄기 작성을 위해서는 작성할 줄기가 메이저 업그레이드인지, 마이너 업그레이드인지 알아야 하므로 추가로 major와 minor 인자 중 하나를 받게 됩니다. 따라서 첫 출시를 위한 명령어는 다음과 같게 됩니다.
$ version.sh release major
메이저든 마이너든 버전을 올리기 위해서는 가장 최근 버전을 가져와야 합니다. 그러기 위해서는 전체 가지 목록을 알아야 하고요. 이를 위한 명령어는 다음과 같습니다.
svn ls file:///path/to/repository/branches
메이저 버전 가져오기
위에서 가져온 목록 중에서 REL-로 시작하는 줄기의 이름 중 메이저라면 가장 큰 첫 번째 숫자를, 마이너라면 지정된 메이저 릴리즈 중에서 가장 큰 두 번째 숫자를 가져와야 합니다. 줄기 디렉터리(/branches)의 목록을 가져옵니다.
MAJOR=0 # 최초 버전을 설정합니다. for BRANCH in $(svn ls $URL/branches) do ; done
그리고 이 중에서 REL-로 시작하는 목록만 걸러 냅니다.
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}') # 뒤에 붙는 슬래시를 제거합니다.
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}') # 뒤에 버전을 분리하기 위해 남겨 놓습니다.
if [ $PREFIX = 'REL' ]; then
;
fi
여기서 나머지 부분(REMAIN)을 가지고 메이저 버전을 가져온 다음 현재 가지고 있는 메이저 버전과 비교하여 더 큰 값을 가지고 있습니다.
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}') # 메이저 버전 가져오기
if [ $_MAJOR -gt $MAJOR ]; then # 버전을 비교하여
MAJOR=$_MAJOR # 더 큰 버전 저장
fi
마지막으로 최종적으로 나온 버전에 1을 더해 줍니다.
MAJOR=$(echo "$MAJOR+1" | bc)
이로써 함수로 정의한 새로 출시할 메이저 버전을 가져오는 코드는 다음과 같습니다.
function get_major {
MAJOR=0
for BRANCH in $(svn ls $URL/branches)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
if [ $_MAJOR -gt $MAJOR ]; then
MAJOR=$_MAJOR
fi
fi
done
MAJOR=$(echo "$MAJOR+1" | bc)
echo $MAJOR
}
위 함수는 다음과 같이 사용합니다.
MAJOR=$(get_major)
마이너 버전 가져오기
마이너 버전 가져오기 메이지 버전과 비슷합니다. 다만 줄기 목록에서 메이저 버전을 포함해서 걸러내야 한다는 점과, 해당 메이저 버전이 없을 경우 실행을 중단해야 한다는 점이 다릅니다.
먼저 기본 마이너 버전을 지정합니다. 단, 마이너 버전은 1이 아니라 0부터 시작하므로 기본 값은 -1로 설정(뒤에서 1을 더해줄 것이기 때문에)합시다. 그리고 마지막에 버전이 그대로 -1이라면 마이너 업그레이드를 할 메이저 버전이 없는 것이기 때문에 오류 메시지를 출력하고 종료하도록 합니다.
function get_minor {
MAJOR=$1
MINOR=-1
# ...
if [ $MINOR -lt 0 ]; then
echo "Major version $MAJOR was not found"
exit 1
fi
MINOR=$(echo "$MINOR+1" | bc)
echo $MINOR
}
중간에 버전을 비교하기 앞서 메이저 버전을 비교하려면 다음과 같습니다.
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
_MINOR=$(echo $REMAIN | awk -F '.' '{print $2}')
if [ $_MAJOR = $MAJOR ]; then
if [ $_MINOR -gt $MINOR ]; then
MINOR=$_MINOR
fi
fi
그리고 전체 함수 코드는 다음과 같습니다.
function get_minor {
MAJOR=$1
MINOR=-1
for BRANCH in $(svn ls $URL/branches)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
_MINOR=$(echo $REMAIN | awk -F '.' '{print $2}')
if [ $_MAJOR = $MAJOR ]; then
if [ $_MINOR -gt $MINOR ]; then
MINOR=$_MINOR
fi
fi
fi
done
if [ $MINOR -lt 0 ]; then
echo "Major version $MAJOR was not found">&2
exit 65
fi
MINOR=$(echo "$MINOR+1" | bc)
echo $MINOR
}
위 함수는 다음과 같이 사용합니다.
# 메이저 버전을 세 번째 인자로 받습니다. if [ $# -lt 3 ]; then usage exit $? fi MAJOR=$3 # 함수를 호출하고 MINOR=$(get_minor $MAJOR) # 결과 코드가 0이 아니라면 오류: 해당하는 메지저 릴리즈 줄기가 없는 경우 if [ $? -ne 0 ]; then exit $? fi
가지치기
위에서 정의한 get_major 함수와 get_minor 함수를 이용하여 메이저, 마이너 버전을 구했으면 SVN의 copy 명령어로 가지를 생성합니다.
svn copy $URL/trunk $URL/branches/REL-$MAJOR.$MINOR
패치 버전 가져오기
패치 버전 역시 마이너 버전의 경우와 유사합니다. 단 유효한 패치 요청이라 하더라도 꼬리표 디렉터리(/tags)에 존재하지 않을 수도 있기 때문에 버전 확인과 패치 버전 생성을 나눠야 한다는 것입니다. 먼저 패치 버전 확인입니다.
svn ls $URL/branches/REL-$MAJOR.$MINOR if [ $? -ne 0 ]; then echo "Release $MAJOR.$MINOR does not exist" exit $? fi
다음 패치 버전 가져오기입니다. 마이너 버전 가져오기와 차이점은 꼬리표 디렉터리를 기준으로 탐색하고, 비교를 마이너 버전까지 하며, 앞 버전이 존재하지 않아서 유효하다는 것과 버전 자리수를 2로 맞춰 0을 채운다는 점입니다.
function get_patch {
MAJOR=$1
MINOR=$2
PATCH=-1
for BRANCH in $(svn ls $URL/tags)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
_MINOR=$(echo $REMAIN | awk -F '.' '{print $2}')
_PATCH=$(echo $REMAIN | awk -F '.' '{print $3}')
if [ $_MAJOR = $MAJOR ]; then
if [ $_MINOR = $MINOR ]; then
if [ $_PATCH -gt $PATCH ]; then
PATCH=$_PATCH
fi
fi
fi
fi
done
PATCH=$(echo "$PATCH+1" | bc | awk '{printf "%02d", $1}')
echo $PATCH
}
위 함수와 버전 확인은 다음과 같이 사용합니다.
# 메이저, 마이너 버전을 두 번째, 세 번째 인자로 받습니다. if [ $# -lt 3 ]; then usage exit $? fi MAJOR=$2 MINOR=$3 # 버전이 존재하는지 확인 svn ls $URL/branches/REL-$MAJOR.$MINOR &>/dev/null if [ $? -ne 0 ]; then echo "Release $MAJOR.$MINOR does not exist" exit $? fi # 패치 버전 가져오기 PATCH=$(get_patch $MAJOR $MINOR)
꼬리표 달기
위에서 정의한 get_patch 함수를 이용하여 패치 버전을 구했으면 SVN의 copy 명령어로 가지를 생성합니다.
svn copy $URL/branches/REL-$MAJOR.$MINOR $URL/tags/REL-$MAJOR.$MINOR.$PATCH
주의할 점은 복사 원본이 줄기가 아니라 가지라는 점입니다.
배포 파일 생성
패치가 이뤄지면 배포 파일을 생성해야 합니다. 배포 파일을 생성하기 위한 작업 경로를 $WORK라 합시다. 먼저 패치용 꼬리표에서 소스 파일을 체크아웃 받아 빌드합니다.
$SVN co $URL/tags/REL-$MAJOR.$MINOR.$PATCH $WORK ant -f $WORK/build.xml
그리고 생성된 각 파일($WORK/dist/*)의 이름에 버전을 붙여주고 다운로드 디렉터리($DOWNLOAD)로 복사합니다.
for FILE in $WORK/dist/* do FILE=$(basename $FILE) VERSIONED=$(echo $FILE | sed "s/$PROJECT/$PROJECT-$MAJOR.$MINOR.$PATCH/g") echo $VERSIONED cp $WORK/dist/$FILE $DOWNLOAD/$VERSIONED done rm -rf $WORK # 작업 폴더 삭제
전체 코드
이로써 완성된 전체 코드는 다음과 같습니다. 아래 코드는 파일:Version.sh에서 다운로드 받을 수 있습니다.
#!/bin/sh
URL=
PROJECT=
DOWNLOAD=
WORK=
SVN=`which svn 2>/dev/null`
if [ -z $SVN ]; then
echo "$SVN is not found" >&2
exit 1
fi
function usage {
echo "Usage:"
echo " `basename $0` release major: create major release"
echo " `basename $0` release minor \$MAJOR: create minor release of \$MAJOR release"
echo " `basename $0` patch \$MAJOR \$MINOR: create patch of \$MAJOR.\$MINOR"
exit 65
}
function get_major {
MAJOR=0
for BRANCH in $($SVN ls $URL/branches)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
if [ $_MAJOR -gt $MAJOR ]; then
MAJOR=$_MAJOR
fi
fi
done
MAJOR=$(echo "$MAJOR+1" | bc)
echo $MAJOR
}
function get_minor {
MAJOR=$1
MINOR=-1
for BRANCH in $($SVN ls $URL/branches)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
_MINOR=$(echo $REMAIN | awk -F '.' '{print $2}')
if [ $_MAJOR = $MAJOR ]; then
if [ $_MINOR -gt $MINOR ]; then
MINOR=$_MINOR
fi
fi
fi
done
if [ $MINOR -lt 0 ]; then
echo "Major version $MAJOR was not found">&2
exit 65
fi
MINOR=$(echo "$MINOR+1" | bc)
echo $MINOR
}
function get_patch {
MAJOR=$1
MINOR=$2
PATCH=-1
for BRANCH in $($SVN ls $URL/tags)
do
BRANCH=$(echo $BRANCH | awk -F '/' '{print $1}')
PREFIX=$(echo $BRANCH | awk -F '-' '{print $1}')
REMAIN=$(echo $BRANCH | awk -F '-' '{print $2}')
if [ $PREFIX = 'REL' ]; then
_MAJOR=$(echo $REMAIN | awk -F '.' '{print $1}')
_MINOR=$(echo $REMAIN | awk -F '.' '{print $2}')
_PATCH=$(echo $REMAIN | awk -F '.' '{print $3}')
if [ $_MAJOR = $MAJOR ]; then
if [ $_MINOR = $MINOR ]; then
if [ $_PATCH -gt $PATCH ]; then
PATCH=$_PATCH
fi
fi
fi
fi
done
PATCH=$(echo "$PATCH+1" | bc | awk '{printf "%02d", $1}')
echo $PATCH
}
case "$1" in
release)
case "$2" in
major)
MAJOR=$(get_major)
$SVN copy $URL/trunk $URL/branches/REL-$MAJOR.0 -m "Create release branch: $MAJOR.0"
if [ $? -ne 0 ]; then
echo "Failed to: svn copy $URL/trunk $URL/branches/REL-$MAJOR.0"
exit $?
fi
echo $MAJOR
;;
minor)
if [ $# -lt 3 ]; then
usage
exit $?
fi
MAJOR=$3
MINOR=$(get_minor $MAJOR)
if [ $? -ne 0 ]; then
exit $?
fi
$SVN copy $URL/trunk $URL/branches/REL-$MAJOR.$MINOR -m "Create release branch: $MAJOR.$MINOR"
if [ $? -ne 0 ]; then
echo "Failed to: svn copy $URL/trunk $URL/branches/REL-$MAJOR.$MINOR"
exit $?
fi
echo $MINOR
;;
*)
usage
exit $?
;;
esac
;;
patch)
if [ $# -lt 3 ]; then
usage
exit $?
fi
MAJOR=$2
MINOR=$3
$SVN ls $URL/branches/REL-$MAJOR.$MINOR
if [ $? -ne 0 ]; then
echo "Release $MAJOR.$MINOR does not exist"
exit $?
fi
PATCH=$(get_patch $MAJOR $MINOR)
$SVN copy $URL/branches/REL-$MAJOR.$MINOR $URL/tags/REL-$MAJOR.$MINOR.$PATCH -m "Create release branch: $MAJOR.$MINOR.$PATCH"
if [ $? -ne 0 ]; then
echo "Failed to: svn copy $URL/branches/REL-$MAJOR.$MINOR $URL/tags/REL-$MAJOR.$MINOR.$PATCH"
exit $?
fi
$SVN co $URL/tags/REL-$MAJOR.$MINOR.$PATCH $WORK
ant -f $WORK/build.xml
for FILE in $WORK/dist/*
do
FILE=$(basename $FILE)
VERSIONED=$(echo $FILE | sed "s/$PROJECT/$PROJECT-$MAJOR.$MINOR.$PATCH/g")
echo $VERSIONED
cp $WORK/dist/$FILE $DOWNLOAD/$VERSIONED
done
rm -rf $WORK
echo $PATCH
;;
*)
usage
exit $?
esac

