宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
以前、update.rdf関係を半自動生成するために頑張ったことがあって、各アドオンの紹介ページのHTMLからupdate.rdf(未署名)を作るあたりまでは自動化できてたんだけど、そこ止まりになっててまだまだ手動でやらなきゃいけないことが多くて、億劫で余計にリリースが滞る……という状況になっている。
それで、これじゃいかん!と思ってもっと自動化を進めることにして、とりあえずリポジトリのmaster/HEADを元に自動でテスト用ビルドを作るようにはした。
で、その知見を元にもう少し頑張って、開発版ではなくリリース版の方ももっと自動化するというチャレンジをしている。
目指しているのはこんな感じの流れ。
こういう風にしたいなあ、ということであれこれやってる所なんだけど、まだ完成には至っていない。なのでこのエントリは、夢を追ってる途中経過のメモということになる。もしタイトル見て期待した人がいたら、ガッカリさせちゃってすみません。→一通りの処理は動くようになったっぽい。あとはほんとにちゃんと期待通り動くのかどうか、実際リリースしてみて確かめないといけない。
Service Hookを受けるスクリプトは、今はこんな感じになってる。
#!/usr/bin/env ruby
require "rubygems" # this is required only for Ruby 1.8.
require "cgi"
require "shellwords"
require "json"
require "logger"
SYNC_SCRIPT = "/home/piro/shared/or/tools/upload_nightly_xpi.sh"
RELEASE_SCRIPT = "/home/piro/shared/or/tools/release_addon.sh"
SSH_KEY = "/path/to/secret_key"
BASE_DIR = "/home/piro/shared/xul"
USER = "piro"
logger = Logger.new("/dev/null")
logger.level = Logger::INFO
cgi = CGI::new
puts "Content-Type: text/plain\n\n"
begin
payload, = cgi.params["payload"]
payload = JSON.parse(payload)
project = payload["repository"]["name"]
project_dir = File.join(BASE_DIR, Shellwords.escape(project))
makefile = File.join(project_dir, "Makefile")
if File.exist?(project_dir) and File.exist?(makefile)
logger.info "build #{project}"
command_line = "sudo -u #{USER} -H #{SYNC_SCRIPT} -i #{SSH_KEY} -b #{BASE_DIR} -d #{project_dir}"
logger.info command_line
logger.info `#{command_line}`
logger.info "exit status: #{$?}"
command_line = "cd #{project_dir}; sudo -u #{USER} -H git describe"
logger.info command_line
current_commit = `#{command_line}`.strip
logger.info current_commit
logger.info "exit status: #{$?}"
if !current_commit.empty? && !current_commit.match(/\A.+\-[0-9]+-[0-9a-f]+\z/)
logger.info "=> release commit"
command_line = "sudo -u #{USER} -H #{RELEASE_SCRIPT} -i #{SSH_KEY} -b #{BASE_DIR} -n #{project}"
logger.info `#{command_line}`
logger.info "exit status: #{$?}"
else
logger.info "=> regular commit"
end
end
logger.info "ok"
p "ok"
rescue Exception => error
logger.error error
logger.info "ng"
p "ng"
end
sudoを使う必要があって、visudoで以下のルールを追加している。
www-data ALL=(ALL) NOPASSWD:ALL
git describeでmaster/HEADが一番新しいタグと同じ物を指しているかどうかという事を確認して、それをトリガーにリリース用スクリプトを起動させることを目論んでるんだけど、バッククォートで標準出力の結果を取れなくて「アルェー?」ってなって詰まってる。→これはspawnで解決できた。詳細は次のエントリで。
ともかく、ここから最初にキックされるupload_nightly_xpi.shは今はこうなってる。
#!/bin/bash
# exit on error
set -e
# MX-Tools等を同じディレクトリに置いているという前提で、
# このファイルのパスを基準にして、他のツールの位置を
# 指定するため、ディレクトリのフルパスを最初に得ておく。
tools_dir=$(cd $(dirname $0) && pwd)
remote_user=piro
remote_host=piro.sakura.ne.jp
remote_dist_dir=~/www/xul/xpi/nightly
actual_dist_dir=http://piro.sakura.ne.jp/xul/xpi/nightly
# ファイルの置き場所等はオプションで得る事にする。
while getopts ab:d:i: OPT
do
case $OPT in
# 変更が無くても強制的にアップロードする指定。テスト用。
"a" ) always=1 ;;
# keyfile.pem等を置いているディレクトリのパス。
"b" ) base_dir="$OPTARG" ;;
# リポジトリをcloneした先のディレクトリ。
"d" ) project_dir="$OPTARG" ;;
# SSH接続に使う秘密鍵のパス。
"i" ) secret_key="$OPTARG" ;;
esac
done
if [ "$project_dir" = '' ]; then
echo "no project directory specified"
exit 1
fi
if [ "$base_dir" = '' ]; then
echo "no base directory specified"
exit 1
fi
if [ "$secret_key" = '' ]; then
echo "no secret key specified"
exit 1
fi
# sedのオプションの違いを吸収しておく。
case $(uname) in
Darwin|*BSD|CYGWIN*) sed="sed -E" ;;
*) sed="sed -r" ;;
esac
# pullした時のメッセージを見て、変更があったかどうかを調べる。
cd $project_dir
pull_result=$(git pull --rebase)
updated=$(if [ "$pull_result" != "Already up-to-date." -a \
"$pull_result" != "Current branch master is up to date." ];\
then echo "1"; fi)
if [ "$updated" = "1" -o "$always" = "1" ]; then
echo "project has changes. let's build it."
"$base_dir/makexpi/make_nightly_xpi.sh" -d "$project_dir" -p "$base_dir/ k ey file.pub"
successfully_built=$?
echo $successfully_built
if [ "$successfully_built" = "0" ]; then
echo "successfully built."
package_name=$(cat $project_dir/Makefile | \
grep "PACKAGE_NAME" | \
head -n 1 | cut -d "=" -f 2 | \
$sed -e "s/\\s*//#")
update_rdf=${package_name}.update.rdf
# 古いupdate.rdfが残っていたら消す。
rm -f $update_rdf
version=$(cat nightly_version.txt)
# ビルドした後、リポジトリの内容を元に戻しておく。
# (次にpullする時に、未コミットの変更があるという
# 警告が出ないようにする。)
git reset --hard
file=$(ls *.xpi | grep -v "_noupdate" | head -n 1)
# キャッシュが使われる事の無いように、
# ユニークなダウンロード用URLにする。
update_link=${actual_dist_dir}/${file}?version=${version}
if [ "$file" != "" -a -f $file ]; then
echo "uploading..."
ssh_remote=${remote_user}@${remote_host}
# 公開先のディレクトリを作っておく。
ssh -i $secret_key ${ssh_remote} \
mkdir -p ${remote_dist_dir}/updateinfo
# XPIをアップロードする。
scp -i $secret_key ./$file \
${ssh_remote}:${remote_dist_dir}/
# XPIからupdate.rdfを自動生成し、それもアップロードする。
$tools_dir/mxtools/uhura -o $update_rdf \
-k $base_dir/keyfile.pem $file $update_link
scp -i $secret_key ./$update_rdf \
${ssh_remote}:${remote_dist_dir}/updateinfo/
echo "done."
fi
else
echo "failed to build."
exit 1
fi
else
echo "project has no change."
fi
exit 0
ここでキックしてる、ナイトリービルド的なXPIを作るスクリプトのmake_nightly_xpi.shは、公開リポジトリに置いてある。前のエントリではmake_nightly_xpi.shとupload_nightly_xpi.shは1つになってたんだけど、ちょっと汎用的に使えないかなと思って分けてみた。
これが終わった後に、post-receiver.rbでどうにかしてmaster/HEADが一番新しいタグと同じかどうかを調べて、うまいこと判別できたとして、それでキックされるrelease_addon.shはこんな感じになってる。
#!/bin/bash
# exit on error
set -e
tools_dir=$(cd $(dirname "$0") && pwd)
website_dir=$(dirname "$tools_dir")
remote_user=piro
remote_host=piro.sakura.ne.jp
actual_dist_dir=http://piro.sakura.ne.jp/xul/xpi
wiki_entries_dir=~/www/wiki/entries/extensions
# ファイルの置き場所等はオプションで得る事にする。
while getopts n:b:i: OPT
do
case $OPT in
# keyfile.pem等を置いているディレクトリのパス。
"b" ) base_dir="$OPTARG" ;;
"n" ) project_name="$OPTARG" ;;
# SSH接続に使う秘密鍵のパス。
"i" ) secret_key="$OPTARG" ;;
esac
done
if [ "$project_name" = '' ]; then
echo "no project name specified"
exit 1
fi
if [ "$base_dir" = '' ]; then
echo "no base directory specified"
exit 1
fi
if [ "$secret_key" = '' ]; then
echo "no secret key specified"
exit 1
fi
# sedのオプションの違いを吸収しておく。
case $(uname) in
Darwin|*BSD|CYGWIN*) sed="sed -E" ;;
*) sed="sed -r" ;;
esac
main() {
echo "starting to release $project_name"
prepare_environment
prepare_files
prepare_update_info
prepare_pages
commit
upload_wiki_entries
echo "done."
}
prepare_environment() {
# 何か変更が行われていたら、とりあえずstashに保存しておく
cd "$website_dir"
echo "website repository updating..."
git stash save
git pull --rebase
project_dir="$base_dir/$project_name"
}
prepare_files() {
# XPIとupdate.rdfを用意
cd "$project_dir"
echo "project repository updating..."
git pull --rebase
update_rdf="${project_name}.update.rdf"
# 古いupdate.rdfが残っていたら消す。
rm -f "$update_rdf"
# XPI作成
(cd $base_dir/makexpi &&
git pull --rebase)
"$base_dir/makexpi/make_release_xpi.sh" -d "$project_dir" -p "$base_dir/keyfile.pub"
successfully_built=$?
if [ "$successfully_built" != "0" ]; then
echo "ERROR: failed to build release version for $project_dir"
exit 1
fi
}
prepare_update_info() {
cd "$project_dir"
echo "update info generating..."
version=$(cat release_version.txt)
file=$(ls *.xpi | grep -v "_noupdate" | head -n 1)
# キャッシュが使われる事の無いように、
# ユニークなダウンロード用URLにする。
update_link=${actual_dist_dir}/${file}?version=${version}
# XPIからupdate.rdfを自動生成
$tools_dir/mxtools/uhura -o $update_rdf \
-k $base_dir/keyfile.pem $file $update_link
}
prepare_pages() {
echo "project pages updating..."
"$tools_dir/update_addon_page.rb" "$project_dir"
}
commit() {
cd "$project_dir"
echo "saving..."
cp "$file" "$website_dir/xul/xpi/"
cp "$update_rdf" "$website_dir/xul/xpi/updateinfo"
cd "$website_dir"
git add "$website_dir/xul/xpi/$file"
git add "$website_dir/xul/xpi/updateinfo/$update_rdf"
git commit -a -m "Release $project_name $version"
git push
}
upload_wiki_entries() {
echo "wiki entries uploading..."
en_source_path="$project_dir/last_release.en.md"
ja_source_path="$project_dir/last_release.ja.md"
en_entry_dist_dir="$wiki_entries_dir/$project_name/en-US"
ja_entry_dist_dir="$wiki_entries_dir/$project_name/ja"
ssh_remote=${remote_user}@${remote_host}
if [ -f $en_source_path ]; then
ssh -i $secret_key ${ssh_remote} \
mkdir -p $en_entry_dist_dir
scp -i $secret_key $project_dir/last_release.en.md \
${ssh_remote}:${en_entry_dist_dir}/${version}.txt
fi
if [ -f $ja_source_path ]; then
ssh -i $secret_key ${ssh_remote} \
mkdir -p $ja_entry_dist_dir
scp -i $secret_key $project_dir/last_release.ja.md \
${ssh_remote}:${ja_entry_dist_dir}/${version}.txt
fi
}
main
これだけで全部をまかなってるわけではなくて、XPIの生成とHTMLの更新は別のスクリプトに分けてる。
XPIは、make_nightly_xpi.shを元にしたmake_release_xpi.shで作ってる。こいつはgit describeの結果を元にして一番新しいタグをチェックアウトしてビルドする、という事だけを担当する。
HTMLは、以下の内容の update_addon_page.rb でやってる。
#!/usr/bin/env ruby
require "rubygems" # this is required only for Ruby 1.8.
require "redcarpet"
require "nokogiri"
require "fileutils"
require "date"
class History
VERSION_ITEMS_EXPRESSION = "/html/body/ul/li".freeze
VERSION_PARTS_EXPRESSION = "child::node()[not(local-name() = 'dl' or local- n ame() = 'ul' or local-name() = 'ol')]".freeze
VERSION_SUBITEMS_EXPRESSION = "child::*[local-name() = 'dl' or local-name() = ' ul' or local-name() = 'ol']".freeze
def initialize(params)
@project_name = params[:project_name]
@source_path = params[:source_path]
end
def source
@source ||= File.read(@source_path)
end
def to_html
return @html unless @html.nil?
markdown = Redcarpet::Markdown.new(Redcarpet::Render::XHTML)
html = markdown.render(source)
html.gsub!(/\n\n+/, "\n")
result = []
document = Nokogiri::HTML(html)
document.xpath(VERSION_ITEMS_EXPRESSION).each do |item|
version = item.xpath(VERSION_PARTS_EXPRESSION).collect do |version_part|
version_part.to_xhtml(:encoding => "UTF-8").sub(/\A\s+|\s+\z/, "")
end
result << "<dt>#{version.join("")}</dt>"
sub_items = item.xpath(VERSION_SUBITEMS_EXPRESSION).collect do |sub _item|
sub_item.to_xhtml(:encoding => "UTF-8").
gsub(/<ul><li>/, "<ul>\n<li>").
gsub(/^<li>/, "\t<li>")
end
result << "<dd>#{sub_items.join("")}</dd>".gsub(/^/, "\t")
end
@html = result.join("\n").gsub(/^/, "\t\t\t")
end
def last_release
return @last_release unless @last_release.nil?
releases = source.split(/^ - /)
releases.each do |release|
if release.match(/\A[0-9\.]+/)
@last_release = release
break
end
end
@last_release
end
def last_release_version
@last_release_version ||= last_release.split("\n").first.strip
end
def last_release_topics
return @last_release_topics unless @last_release_topics.nil?
@last_release_topics = last_release.split("\n")[1..-1].join("\n")
@last_release_topics.gsub!(/^\s+\*/, " *")
end
def meta_creation_date
return @meta_creation_date unless @meta_creation_date.nil?
date = DateTime.now.strftime("%m/%e/%Y %H:%M:%S")
date.sub!(/^0/, "")
@meta_creation_date = date
end
def last_release_wiki_entry
return @last_release_wiki_entry unless @last_release_wiki_entry.nil?
contents = []
contents << "#{@project_name} #{last_release_version}"
contents << "meta-creation_date: #{meta_creation_date}"
contents << ""
contents << ""
contents << ""
contents << last_release_topics
@last_release_wiki_entry = contents.join("\n")
end
def last_release_html
return @last_release_html unless @last_release_html.nil?
markdown = Redcarpet::Markdown.new(Redcarpet::Render::XHTML)
html = markdown.render(last_release_topics)
html.gsub!(/\n/, "")
html = "#{last_release_version}:#{html}"
@last_release_html = html
end
end
class PageUpdater
def initialize(params)
@project_dir = params[:project_dir]
@language = params[:language]
end
def tools_dir
@tools_dir ||= File.dirname(File.expand_path(__FILE__))
end
def website_dir
@website_dir ||= File.dirname(tools_dir)
end
def project_name
return @project_name unless @project_name.nil?
makefile_path = "#{@project_dir}/Makefile"
@project_name = `cat "#{makefile_path}" | \
grep "PACKAGE_NAME" | \
head -n 1 | \
cut -d "=" -f 2`.strip
@project_name
end
def version
@version ||= File.read("#{@project_dir}/release_version.txt").strip
end
def history_source_path
"#{@project_dir}/history.#{@language}.md"
end
def history
@history ||= History.new(:source_path => history_source_path,
:project_name => project_name)
end
def last_release_path_base
"#{@project_dir}/last_release.#{@language}"
end
def html_suffix
return @html_suffix unless @html_suffix.nil?
suffix = ".html"
suffix = "#{suffix}.#{@language}" if @language != "ja"
@html_suffix = suffix
end
def page_path_base
"#{website_dir}/xul"
end
def version_page_path
return @version_page_path unless @version_page_path.nil?
path = "#{page_path_base}/_#{project_name}#{html_suffix}"
unless File.exist?(path)
path = "#{page_path_base}/#{project_name}/index#{html_suffix}"
end
@version_page_path = path
end
def history_page_path
return @history_page_path unless @history_page_path.nil?
path = "#{page_path_base}/_#{project_name}#{html_suffix}"
unless File.exist?(path)
path = "#{page_path_base}/#{project_name}/history#{html_suffix}"
end
unless File.exist?(path)
path = "#{page_path_base}/#{project_name}/index#{html_suffix}"
end
@history_page_path = path
end
def apply_version(source)
before, middle = source.split(/<!--\s*BEGIN VERSION\s*-->/)
middle, after = middle.split(/<!--\s*END VERSION\s*-->/)
"#{before}<!--BEGIN VERSION-->#{version}<!--END VERSION-->#{after}"
end
def apply_history(source)
before, middle = source.split(/<!--\s*BEGIN HISTORY ITEMS\s*-->/)
middle, after = middle.split(/<!--\s*END HISTORY ITEMS\s*-->/)
"#{before}<!--BEGIN HISTORY ITEMS-->\n#{history.to_html}\t\t<!--END HI STO RY I TEMS-->#{after}"
end
def write_to(contents, path)
File.open(path, "w") do |file|
file.puts(contents)
end
end
def update
if version_page_path == history_page_path
page = File.read(version_page_path)
page = apply_version(page)
page = apply_history(page)
write_to(page, version_page_path)
else
version_page = File.read(version_page_path)
version_page = apply_version(version_page)
write_to(version_page, version_page_path)
history_page = File.read(history_page_path)
history_page = apply_history(history_page)
write_to(history_page, history_page_path)
end
write_to(history.last_release_wiki_entry, "#{last_release_path_base}.md")
write_to(history.last_release_html, "#{last_release_path_base}.html")
end
end
project_dir = ARGV.first
PageUpdater.new(:project_dir => project_dir, :language => "ja").update
PageUpdater.new(:project_dir => project_dir, :language => "en").update
こいつが、HTMLの更新と、Mozilla Add-ons用の更新履歴のコード片の生成までをやってくれる。予定。
ということで、Service Hookを受けたときにタグを取れてない、という所だけどうにかなれば割と動きそうな印象なんだけど、そうこうしてるうちに肉リリースの期限が終わってしまったので、続きはまた1ヶ月後くらいになりそうな予感がしています。
RubyとBashを行ったり来たりしてて、自分でもなんやねんこれ感がすごいある。あとコードがやっつけすぎるのも、どうなん感ある。それでも公開しちゃうフルディスクロージャ戦略です。
4月1日追記。なぜpost-receiver.rbでバッククォートでgit describeした結果を取得できないのかをすとうさんに教えて頂きました。このpost-receiver.rbはCGIスクリプトとして動いているため標準入出力がCGIの口と繋がっていて、そこから起動された外部のコマンド(子プロセス)の標準入出力もそれを引き継ぐので、git describeの結果が直接クライアント(GitHubのUserAgent)の方に返されてしまっているのだろう、とのことです。Ruby 1.9系で標準入出力を指定してspawnしてProcess.waitpidで終了を待つとよいだろう……とのアドバイスを頂きましたので、あとで試してみようと思います。→続き書きました。
2015年追記。Raspberry PiのLubuntu(Ubuntu)でこれを動かすようにするにあたって必要だった手順。
sudo apt-get install apache2 ruby2.0 cpanminus build-essential
~username/public_html/
以下に置いているので、 このパスを有効化。sudo a2enmod userdir
.htaccess
でOptions
を制御する必要があるので、vi /etc/apache2/mods-enabled/userdir.conf
でAllowOverride
にOptions
を追加する。sudo a2enmod cgi
sudo service apache2 restart
sudo cpan install Convert::ASN1 XML::Parser RDF::Core
の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2013-03-30_autobuild.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。
writeback message: Ready to post a comment.