release.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. #!/bin/sh
  2. #
  3. # Put into Public Domain, by Nicolas Sebrecht
  4. #
  5. # Create new releases in OfflineIMAP.
  6. # TODO: https://developer.github.com/v3/repos/releases/#create-a-release
  7. # https://developer.github.com/libraries/
  8. # https://github.com/turnkeylinux/octohub
  9. # https://github.com/michaelliao/githubpy (onefile)
  10. # https://github.com/sigmavirus24/github3.py
  11. # https://github.com/copitux/python-github3
  12. # https://github.com/PyGithub/PyGithub
  13. # https://github.com/micha/resty (curl)
  14. # TODO: move configuration out and source it.
  15. # TODO: implement rollback.
  16. __VERSION__='v0.3'
  17. SPHINXBUILD=sphinx-build
  18. MAILING_LIST='offlineimap-project@lists.alioth.debian.org'
  19. GITHUB_FILE_LINK_PREFIX='https://raw.githubusercontent.com/OfflineIMAP/offlineimap'
  20. DOCSDIR='docs'
  21. ANNOUNCE_MAGIC='#### Notes '
  22. CHANGELOG_MAGIC='{:toc}'
  23. CHANGELOG='Changelog.md'
  24. CACHEDIR='.git/offlineimap-release'
  25. WEBSITE='website'
  26. WEBSITE_LATEST="${WEBSITE}/_data/latest.yml"
  27. TMP_CHANGELOG_EXCERPT="${CACHEDIR}/changelog.excerpt.md"
  28. TMP_CHANGELOG_EXCERPT_OLD="${TMP_CHANGELOG_EXCERPT}.old"
  29. TMP_CHANGELOG="${CACHEDIR}/changelog.md"
  30. TMP_ANNOUNCE="${CACHEDIR}/announce.txt"
  31. True=0
  32. False=1
  33. Yes=$True
  34. No=$False
  35. DEBUG=$True
  36. #
  37. # $1: EXIT_CODE
  38. # $2..: message
  39. function die () {
  40. n=$1
  41. shift
  42. echo $*
  43. exit $n
  44. }
  45. function debug () {
  46. if test $DEBUG -eq $True
  47. then
  48. echo "DEBUG: $*" >&2
  49. fi
  50. }
  51. #
  52. # $1: question
  53. # $2: message on abort
  54. #
  55. function ask () {
  56. echo
  57. echo -n "--- $1 "
  58. read -r ans
  59. test "n$ans" = 'n' -o "n$ans" = 'ny' && return $Yes
  60. test "n$ans" = "ns" -o "n$ans" = 'nn' && return $No
  61. die 1 "! $2"
  62. }
  63. #
  64. # $1: message
  65. # $1: path to file
  66. #
  67. function edit_file () {
  68. ask "Press Enter to $1"
  69. test $? -eq $Yes && {
  70. $EDITOR "$2"
  71. reset
  72. }
  73. }
  74. function fix_pwd () {
  75. debug 'in fix_pwd'
  76. cd "$(git rev-parse --show-toplevel)" || \
  77. die 2 "cannot determine the root of the repository"
  78. }
  79. function prepare_env () {
  80. debug 'in prepare_env'
  81. mkdir "$CACHEDIR" 2>/dev/null
  82. test ! -d "$CACHEDIR" && die 5 "Could not make cache directory $CACHEDIR"
  83. }
  84. function check_dirty () {
  85. debug 'in check_dirty'
  86. git diff --quiet 2>/dev/null && git diff --quiet --cached 2>/dev/null || {
  87. die 4 "Commit all your changes first!"
  88. }
  89. }
  90. function welcome () {
  91. debug 'in welcome'
  92. cat <<EOF
  93. You will be prompted to answer questions.
  94. Answer by:
  95. - 'y' : yes, continue (default)
  96. - '<Enter>' : yes, continue
  97. - 'n' : no
  98. - 's' : skip (ONLY where applicable, otherwise continue)
  99. Any other key will abort the program.
  100. EOF
  101. ask 'Ready?'
  102. }
  103. function checkout_next () {
  104. debug 'in checkout_next'
  105. git checkout --quiet next || {
  106. die 6 "Could not checkout 'next' branch"
  107. }
  108. }
  109. function merge_maint () {
  110. debug 'in merge_maint'
  111. git merge --quiet -Xours maint || {
  112. die 7 "Could not merge 'maint' branch"
  113. }
  114. }
  115. function get_version () {
  116. debug 'in get_version'
  117. echo "v$(./offlineimap.py --version)"
  118. }
  119. function update_offlineimap_version () {
  120. debug 'in update_offlineimap_version'
  121. edit_file 'update the version in __init__.py' offlineimap/__init__.py
  122. }
  123. #
  124. # $1: previous version
  125. #
  126. function get_git_history () {
  127. debug 'in get_git_history'
  128. git log --format='- %h %s. [%aN]' --no-merges "${1}.."
  129. }
  130. #
  131. # $1: previous version
  132. #
  133. function get_git_who () {
  134. debug 'in get_git_who'
  135. git shortlog --no-merges -sn "${1}.." | \
  136. sed -r -e 's, +([0-9]+)\t(.*),- \2 (\1),'
  137. }
  138. #
  139. # $1: new version
  140. # $2: shortlog
  141. function changelog_template_part1 () {
  142. debug 'in changelog_template_part1'
  143. cat <<EOF
  144. // vim: expandtab ts=2 syntax=markdown
  145. // WARNING: let at least one empy line before the real content.
  146. //
  147. // Write a new Changelog entry.
  148. //
  149. // Comments MUST start at the beginning of the lile with two slashes.
  150. // They will by be ignored by the template engine.
  151. //
  152. ### OfflineIMAP $1 ($(date +%Y-%m-%d))
  153. #### Notes
  154. // Add some notes. Good notes are about what was done in this release from the
  155. // bigger perspective.
  156. // HINT: explain most important changes.
  157. #### Authors
  158. EOF
  159. }
  160. function changelog_template_part2 () {
  161. debug 'in changelog_template_part2'
  162. cat <<EOF
  163. #### Features
  164. // Use list syntax with '- '
  165. #### Fixes
  166. // Use list syntax with '- '
  167. #### Changes
  168. // Use list syntax with '- '
  169. // The preformatted log was added below. Make use of this to fill the sections
  170. // above.
  171. EOF
  172. }
  173. #
  174. # $1: new version
  175. # $2: previous version
  176. #
  177. function update_changelog () {
  178. debug 'in update_changelog'
  179. # Write Changelog excerpt.
  180. if test ! -f "$TMP_CHANGELOG_EXCERPT"
  181. then
  182. changelog_template_part1 "$1" > "$TMP_CHANGELOG_EXCERPT"
  183. get_git_who "$2" >> "$TMP_CHANGELOG_EXCERPT"
  184. changelog_template_part2 >> "$TMP_CHANGELOG_EXCERPT"
  185. get_git_history "$2" >> "$TMP_CHANGELOG_EXCERPT"
  186. edit_file "the Changelog excerpt" $TMP_CHANGELOG_EXCERPT
  187. # Remove comments.
  188. grep -v '//' "$TMP_CHANGELOG_EXCERPT" > "${TMP_CHANGELOG_EXCERPT}.nocomment"
  189. mv -f "${TMP_CHANGELOG_EXCERPT}.nocomment" "$TMP_CHANGELOG_EXCERPT"
  190. fi
  191. # Write new Changelog.
  192. cat "$CHANGELOG" > "$TMP_CHANGELOG"
  193. debug "include excerpt $TMP_CHANGELOG_EXCERPT to $TMP_CHANGELOG"
  194. sed -i -e "/${CHANGELOG_MAGIC}/ r ${TMP_CHANGELOG_EXCERPT}" "$TMP_CHANGELOG"
  195. debug 'remove trailing whitespaces'
  196. sed -i -r -e 's, +$,,' "$TMP_CHANGELOG" # Remove trailing whitespaces.
  197. debug "copy to $TMP_CHANGELOG -> $CHANGELOG"
  198. cp -f "$TMP_CHANGELOG" "$CHANGELOG"
  199. # Check and edit Changelog.
  200. ask "Next step: you'll be asked to review the diff of $CHANGELOG"
  201. while true
  202. do
  203. git diff -- "$CHANGELOG" | less
  204. ask 'edit Changelog?' $CHANGELOG
  205. test ! $? -eq $Yes && break
  206. # Asked to edit the Changelog; will loop again.
  207. $EDITOR "$CHANGELOG"
  208. done
  209. }
  210. #
  211. # $1: new version
  212. #
  213. function git_release () {
  214. debug 'in git_release'
  215. git commit -as -m"$1"
  216. git tag -a "$1" -m"$1"
  217. git checkout master
  218. git merge next
  219. git checkout next
  220. }
  221. function get_last_rc () {
  222. git tag | grep -E '^v([0-9][\.-]){3}rc' | sort -V | tail -n1
  223. }
  224. function get_last_stable () {
  225. git tag | grep -E '^v([0-9][\.])+' | grep -v '\-rc' | sort -V | tail -n1
  226. }
  227. function update_website_releases_info() {
  228. cat > "$WEBSITE_LATEST" <<EOF
  229. # DO NOT EDIT MANUALLY: it is generated by a script (release.sh)
  230. stable: $(get_last_stable)
  231. rc: $(get_last_rc)
  232. EOF
  233. }
  234. #
  235. # $1: new version
  236. #
  237. function update_website () {
  238. debug 'in update_website'
  239. ask "update API of the website? (require $SPHINXBUILD)"
  240. if test $? -eq $Yes
  241. then
  242. # Check sphinx is available.
  243. $SPHINXBUILD --version > /dev/null 2>&1
  244. if test ! $? -eq 0
  245. then
  246. echo "Oops! you don't have $SPHINXBUILD installed?"
  247. echo "Cannot update the webite documentation..."
  248. echo "You should install it and run:"
  249. echo " $ cd docs"
  250. echo " $ make websitedoc"
  251. echo "Then, commit and push changes of the website."
  252. ask 'continue'
  253. return
  254. fi
  255. # Check website sources are available.
  256. cd website
  257. if test ! $? -eq 0
  258. then
  259. echo "ERROR: cannot go to the website sources"
  260. ask 'continue'
  261. return
  262. fi
  263. # Stash any WIP in the website sources.
  264. git diff --quiet 2>/dev/null && git diff --quiet --cached 2>/dev/null || {
  265. echo "There is WIP in the website repository, stashing"
  266. echo "git stash create 'WIP during offlineimap API import'"
  267. git stash create 'WIP during offlineimap API import'
  268. ask 'continue'
  269. }
  270. cd .. # Back to offlineimap.git.
  271. update_website_releases_info
  272. cd "./$DOCSDIR" # Enter the docs directory in offlineimap.git.
  273. # Build the docs!
  274. make websitedoc && {
  275. # Commit changes in a branch.
  276. cd ../website # Enter the website sources.
  277. branch_name="import-$1"
  278. git checkout -b "$branch_name"
  279. git add '_doc/versions'
  280. git commit -a -s -m"update for offlineimap $1"
  281. echo "website: branch '$branch_name' ready for a merge in master!"
  282. }
  283. ask 'website updated locally; continue'
  284. fi
  285. }
  286. function git_username () {
  287. git config --get user.name
  288. }
  289. function git_usermail () {
  290. git config --get user.email
  291. }
  292. #
  293. # $1: new version
  294. #
  295. function announce_header () {
  296. cat <<EOF
  297. Message-Id: <$(git log HEAD~1.. --oneline --pretty='%H.%t.release.%ce')>
  298. Date: $(git log HEAD~1.. --oneline --pretty='%cD')
  299. From: $(git_username) <$(git_usermail)>
  300. To: $MAILING_LIST
  301. Subject: [ANNOUNCE] OfflineIMAP $1 released
  302. OfflineIMAP $1 is out.
  303. Downloads:
  304. http://github.com/OfflineIMAP/offlineimap/archive/${1}.tar.gz
  305. http://github.com/OfflineIMAP/offlineimap/archive/${1}.zip
  306. Pip:
  307. wget "${GITHUB_FILE_LINK_PREFIX}/${1}/requirements.txt" -O requirements.txt
  308. pip install -r ./requirements.txt --user git+https://github.com/OfflineIMAP/offlineimap.git@${1}
  309. EOF
  310. }
  311. function announce_footer () {
  312. cat <<EOF
  313. --
  314. $(git_username)
  315. EOF
  316. }
  317. #
  318. # $1: new version
  319. # $2: previous version
  320. #
  321. function build_announce () {
  322. announce_header "$1" > "$TMP_ANNOUNCE"
  323. grep -v '^### OfflineIMAP' "$TMP_CHANGELOG_EXCERPT" | \
  324. grep -v '^#### Notes' >> "$TMP_ANNOUNCE"
  325. sed -i -r -e "s,^$ANNOUNCE_MAGIC,," "$TMP_ANNOUNCE"
  326. sed -i -r -e "s,^#### ,# ," "$TMP_ANNOUNCE"
  327. announce_footer >> "$TMP_ANNOUNCE"
  328. }
  329. function edit_announce () {
  330. edit_file 'edit announce' "$TMP_ANNOUNCE"
  331. }
  332. #
  333. # run
  334. #
  335. function run () {
  336. debug 'in run'
  337. fix_pwd
  338. check_dirty
  339. prepare_env
  340. checkout_next
  341. clear
  342. welcome
  343. if test -f "$TMP_CHANGELOG_EXCERPT"
  344. then
  345. head "$TMP_CHANGELOG_EXCERPT"
  346. ask "A previous Changelog excerpt (head above) was found, use it?"
  347. if test ! $? -eq $Yes
  348. then
  349. mv -f "$TMP_CHANGELOG_EXCERPT" "$TMP_CHANGELOG_EXCERPT_OLD"
  350. fi
  351. fi
  352. previous_version="$(get_version)"
  353. message="Safety check: release after version:"
  354. ask "$message $previous_version ?"
  355. update_offlineimap_version
  356. new_version="$(get_version)"
  357. ask "Safety check: make a new release with version: '$new_version'" "Clear changes and restart"
  358. update_changelog "$new_version" "$previous_version"
  359. build_announce "$new_version" "$previous_version"
  360. edit_announce
  361. # Wait for the mainline and announce to be built to not include commits from
  362. # maint.
  363. merge_maint
  364. git_release $new_version
  365. # Wait for all the Changelogs to be up-to-date in next.
  366. update_website $new_version
  367. }
  368. run
  369. cat <<EOF
  370. Release is ready!
  371. Make your checks and push the changes for both offlineimap and the website.
  372. Announce template stands in '$TMP_ANNOUNCE'.
  373. Command samples to do manually:
  374. - git push <remote> master next $new_version
  375. - python setup.py sdist && twine upload dist/* && rm -rf dist MANIFEST
  376. - cd website
  377. - git checkout master
  378. - git merge $branch_name
  379. - git push <remote> master
  380. - cd ..
  381. - git send-email $TMP_ANNOUNCE
  382. Have fun! ,-)
  383. EOF
  384. # vim: expandtab ts=2 :