URI: 
       trelease.lisp - clic - Clic is an command line interactive client for gopher written in Common LISP
  HTML git clone git://bitreich.org/clic/ git://hg6vgqziawt5s4dj.onion/clic/
   DIR Log
   DIR Files
   DIR Refs
   DIR Tags
   DIR LICENSE
       ---
       trelease.lisp (9849B)
       ---
            1 #!/usr/bin/env clisp
            2 ;;;; -*- Mode: lisp; indent-tabs-mode: nil -*-
            3 
            4 (defpackage :release-script (:use #:cl #:regexp))
            5 (in-package :release-script)
            6 
            7 ;;;; Configuration ------------------------------------------------------------
            8 
            9 (defparameter *project-name* "babel")
           10 (defparameter *asdf-file* (format nil "~A.asd" *project-name*))
           11 
           12 (defparameter *host* "common-lisp.net")
           13 (defparameter *release-dir*
           14   (format nil "/project/~A/public_html/releases" *project-name*))
           15 
           16 (defparameter *version-file* "VERSION")
           17 (defparameter *version-file-dir*
           18   (format nil "/project/~A/public_html" *project-name*))
           19 
           20 ;;;; --------------------------------------------------------------------------
           21 
           22 ;;;; Utilities
           23 
           24 (defun ensure-list (x)
           25   (if (listp x) x (list x)))
           26 
           27 (defmacro string-case (expression &body clauses)
           28   `(let ((it ,expression)) ; yes, anaphoric, deal with it.
           29      (cond
           30        ,@(loop for clause in clauses collect
           31                `((or ,@(loop for alternative in (ensure-list (first clause))
           32                              collect (or (eq t alternative)
           33                                          `(string= it ,alternative))))
           34                  ,@(rest clause))))))
           35 
           36 (defparameter *development-mode* nil)
           37 
           38 (defun die (format-control &rest format-args)
           39   (format *error-output* "~?" format-control format-args)
           40   (if *development-mode*
           41       (cerror "continue" "die")
           42       (ext:quit 1)))
           43 
           44 (defun numeric-split (string)
           45   (if (digit-char-p (char string 0))
           46       (multiple-value-bind (number next-position)
           47           (parse-integer string :junk-allowed t)
           48         (cons number (when (< next-position (length string))
           49                        (numeric-split (subseq string next-position)))))
           50       (let ((next-digit-position (position-if #'digit-char-p string)))
           51         (if next-digit-position
           52             (cons (subseq string 0 next-digit-position)
           53                   (numeric-split (subseq string next-digit-position)))
           54             (list string)))))
           55 
           56 (defun natural-string-< (s1 s2)
           57   (labels ((aux< (l1 l2)
           58              (cond ((null l1) (not (null l2)))
           59                    ((null l2) nil)
           60                    (t (destructuring-bind (x . xs) l1
           61                         (destructuring-bind (y . ys) l2
           62                           (cond ((and (numberp x) (stringp y))
           63                                  t)
           64                                 ((and (numberp y) (stringp x))
           65                                  nil)
           66                                 ((and (numberp x) (numberp y))
           67                                  (or (< x y) (and (= x y) (aux< xs ys))))
           68                                 (t
           69                                  (or (string-lessp x y)
           70                                      (and (string-equal x y)
           71                                           (aux< xs ys)))))))))))
           72     (aux< (numeric-split s1)
           73           (numeric-split s2))))
           74 
           75 ;;;; Running commands
           76 
           77 (defparameter *dry-run* nil)
           78 
           79 (defun cmd? (format-control &rest format-args)
           80   (let ((cmd (format nil "~?" format-control format-args)))
           81     (with-open-stream (s1 (ext:run-shell-command cmd :output :stream))
           82       (loop for line = (read-line s1 nil nil)
           83             while line
           84             collect line))))
           85 
           86 ;; XXX: quote arguments.
           87 (defun cmd (format-control &rest format-args)
           88   (when *development-mode*
           89     (format *debug-io* "CMD: ~?~%" format-control format-args))
           90   (let ((ret (ext:run-shell-command (format nil "~?" format-control format-args))))
           91     (or (null ret)
           92         (zerop ret))))
           93 
           94 (defun cmd! (format-control &rest format-args)
           95   (or (apply #'cmd format-control format-args)
           96       (die "cmd '~?' failed." format-control format-args)))
           97 
           98 (defun maybe-cmd! (format-control &rest format-args)
           99   (if *dry-run*
          100       (format t "SUPPRESSING: ~?~%" format-control format-args)
          101       (apply #'cmd! format-control format-args)))
          102 
          103 ;;;;
          104 
          105 (defun find-current-version ()
          106   (subseq (reduce (lambda (x y) (if (natural-string-< x y) y x))
          107                   (or (cmd? "git tag -l v\\*")
          108                       (die "no version tags found. Please specify initial version.")))
          109           1))
          110 
          111 (defun parse-version (string)
          112   (mapcar (lambda (x)
          113             (parse-integer x :junk-allowed t))
          114           (loop repeat 3 ; XXX: parameterize
          115                 for el in (regexp-split "\\." (find-current-version))
          116                 collect el)))
          117 
          118 (defun check-for-unrecorded-changes (&optional force)
          119   (unless (cmd "git diff --exit-code")
          120     (write-line "Unrecorded changes.")
          121     (if force
          122         (write-line "Continuing anyway.")
          123         (die "Aborting.~@
          124               Use -f or --force if you want to make a release anyway."))))
          125 
          126 (defun new-version-number-candidates (current-version)
          127   (let ((current-version (parse-version current-version)))
          128     (labels ((alternatives (before after)
          129                (when after
          130                  (cons (append before (list (1+ (first after)))
          131                                (mapcar (constantly 0) (rest after)))
          132                        (alternatives (append before (list (first after)))
          133                                      (rest after))))))
          134       (loop for alt in (alternatives nil current-version)
          135             collect (reduce (lambda (acc next)
          136                               (format nil "~a.~a" acc next))
          137                             alt)))))
          138 
          139 (defun ask-user-for-version (current-version next-versions)
          140   (format *query-io* "Current version is ~A. Which will be the next one?~%"
          141           current-version)
          142   (loop for i from 1 and version in next-versions
          143         do (format *query-io* "~T~A) ~A~%" i version))
          144   (format *query-io* "? ")
          145   (finish-output *query-io*)
          146   (nth (1- (parse-integer (read-line) :junk-allowed t))
          147        next-versions))
          148 
          149 (defun git-tag-tree (version)
          150   (write-line "Tagging the tree...")
          151   (maybe-cmd! "git tag \"v~A\"" version))
          152 
          153 (defun add-version-to-system-file (version path-in path-out)
          154   (with-open-file (in path-in :direction :input)
          155     (with-open-file (out path-out :direction :output)
          156       (loop for line = (read-line in nil nil) while line
          157             do (write-line line out)
          158             when (string= #1="(defsystem " line
          159                           :end2 (min (length #1#) (length line)))
          160               do (format out "  :version ~s~%" version)))))
          161 
          162 (defun create-dist (version distname)
          163   (write-line "Creating distribution...")
          164   (cmd! "mkdir \"~a\"" distname)
          165   (cmd! "git archive master | tar xC \"~A\"" distname)
          166   (format t "Updating ~A with new version: ~A~%" *asdf-file* version)
          167   (let* ((asdf-file-path (format nil "~A/~A" distname *asdf-file*))
          168          (tmp-asdf-file-path (format nil "~a.tmp" asdf-file-path)))
          169     (add-version-to-system-file version asdf-file-path tmp-asdf-file-path)
          170     (cmd! "mv \"~a\" \"~a\"" tmp-asdf-file-path asdf-file-path)))
          171 
          172 (defun tar-and-sign (distname tarball)
          173   (write-line "Creating and signing tarball...")
          174   (cmd! "tar czf \"~a\" \"~a\"" tarball distname)
          175   (cmd! "gpg -b -a \"~a\"" tarball))
          176 
          177 (defparameter *remote-directory* (format nil "~A:~A" *host* *release-dir*))
          178 
          179 (defun upload-tarball (tarball signature remote-directory)
          180   (write-line "Copying tarball to web server...")
          181   (maybe-cmd! "scp \"~A\" \"~A\" \"~A\"" tarball signature remote-directory)
          182   (format t "Uploaded ~A and ~A.~%" tarball signature))
          183 
          184 (defun update-remote-links (tarball signature host release-dir project-name)
          185   (format t "Updating ~A_latest links...~%" project-name)
          186   (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz\""
          187               host tarball release-dir project-name)
          188   (maybe-cmd! "ssh \"~A\" ln -sf \"~A\" \"~A/~A_latest.tar.gz.asc\""
          189               host signature release-dir project-name))
          190 
          191 (defun upload-version-file (version version-file host version-file-dir)
          192   (format t "Uploading ~A...~%" version-file)
          193   (with-open-file (out version-file :direction :output)
          194     (write-string version out))
          195   (maybe-cmd! "scp \"~A\" \"~A\":\"~A\"" version-file host version-file-dir)
          196   (maybe-cmd! "rm \"~A\"" version-file))
          197 
          198 (defun maybe-clean-things-up (tarball signature)
          199   (when (y-or-n-p "Clean local tarball and signature?")
          200     (cmd! "rm \"~A\" \"~A\"" tarball signature)))
          201 
          202 (defun run (force version)
          203   (check-for-unrecorded-changes force)
          204   ;; figure out what version we'll be preparing.
          205   (unless version
          206     (let* ((current-version (find-current-version))
          207            (next-versions (new-version-number-candidates current-version)))
          208       (setf version (or (ask-user-for-version current-version next-versions)
          209                         (die "invalid selection.")))))
          210   (git-tag-tree version)
          211   (let* ((distname (format nil "~A_~A" *project-name* version))
          212          (tarball (format nil "~A.tar.gz" distname))
          213          (signature (format nil "~A.asc" tarball)))
          214     ;; package things up.
          215     (create-dist version distname)
          216     (tar-and-sign distname tarball)
          217     ;; upload.
          218     (upload-tarball tarball signature *remote-directory*)
          219     (update-remote-links tarball signature *host* *release-dir* *project-name*)
          220     (when *version-file*
          221       (upload-version-file version *version-file* *host* *version-file-dir*))
          222     ;; clean up.
          223     (maybe-clean-things-up tarball signature)
          224     ;; documentation.
          225     ;; (write-line "Building and uploading documentation...")
          226     ;; (maybe-cmd! "make -C doc upload-docs")
          227     ;; push tags and any outstanding changes.
          228     (write-line "Pushing tags and changes...")
          229     (maybe-cmd! "git push --tags origin master")))
          230 
          231 
          232 ;;;; Do it to it
          233 
          234 (let ((force nil)
          235       (version nil)
          236       (args ext:*args*))
          237   (loop while args
          238         do (string-case (pop args)
          239              (("-h" "--help")
          240               (write-line "No help, sorry. Read the source.")
          241               (ext:quit 0))
          242              (("-f" "--force")
          243               (setf force t))
          244              (("-v" "--version")
          245               (setf version (pop args)))
          246              (("-n" "--dry-run")
          247               (setf *dry-run* t))
          248              (t
          249               (die "Unrecognized argument '~a'" it))))
          250   (run force version))