Skip to content

Instantly share code, notes, and snippets.

@alexclst
Last active August 12, 2021 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexclst/04cc21f0cd48f144bc2fcc180dcdb449 to your computer and use it in GitHub Desktop.
Save alexclst/04cc21f0cd48f144bc2fcc180dcdb449 to your computer and use it in GitHub Desktop.
Git hook to deploy a WP plugin.
#! /bin/bash
#
# A script meant to run on the `post-recieve` Git hook to deploy to WordPress.org whenever a new version tag is pushed in.
# include common post-recieve functions
# from https://github.com/stephenh/git-central/blob/master/server/functions
root_dir=$(dirname $0)
. $root_dir/post-recieve-functions.sh
# cleans up before exiting
clean_and_exit() {
cd $root_dir
rm -rf "$repo_name"
cd $repo_remote_path
exit $1
}
# process the pushed ref
# based on https://stackoverflow.com/a/5810488
# based on https://github.com/GaryJones/wordpress-plugin-svn-deploy
process_ref() {
# get stdin parts
oldrev=$(git rev-parse $1)
newrev=$(git rev-parse $2)
refname="$3"
# setup with common functions
set_change_type
set_rev_types
set_describe_tags
# only process tags
case "$refname","$rev_type" in
refs/tags/*,commit)
# get repo details
# from https://stackoverflow.com/a/5584084
repo_name=$(basename "$PWD")
repo_name=${repo_name%.git}
repo_remote_path=$PWD
# collect the tag name
version=${refname##refs/tags/}
# ensure that the tag is the correct format
version_checked=$(echo $version | sed -e '/^[0-9]*\.[0-9]*\.[0-9]*$/d')
if ! [[ -z $version_checked ]]; then
clean_and_exit 0
fi
# log process start
echo "Preparing version $version of $repo_name for WordPress.org"
# generate temp directory
cd $root_dir
mkdir -p "$repo_name"
# checkout the intended tag of our project from Git
git_dir="$root_dir/$repo_name/git"
/usr/local/bin/git -c advice.detachedHead=false clone -b $version $repo_remote_path $git_dir
# Check version in readme.txt is the same as the main plugin file
plugin_version=$(grep -i "Version:" $git_dir/$repo_name.php | awk -F' ' '{print $NF}' | tr -d '\r')
readme_version=$(grep -i "Stable tag:" $git_dir/readme.txt | awk -F' ' '{print $NF}' | tr -d '\r')
if [ "$readme_version" = "trunk" ]; then
echo "Version in readme.txt & $repo_name.php don't match, but Stable tag is trunk. Let's continue..."
elif [ "$plugin_version" != "$readme_version" ]; then
echo "Version in readme.txt & $repo_name.php don't match. Aborting."
clean_and_exit 1
elif [ "$plugin_version" = "$readme_version" ]; then
echo "Versions match in readme.txt and $repo_name.php. Let's continue."
fi
# get svn details
# TODO: support options for these, how?
# maybe as arguments on this script
svn_url="https://plugins.svn.wordpress.org/$repo_name"
svn_user="__USERNAME__"
svn_pass="__PASSWORD__" # since WP.org svn doesn't work with ssh keys
# make the svn path
svn_dir="$root_dir/$repo_name/svn"
mkdir -p "$svn_dir"
echo "Creating local copy of svn repo trunk."
/usr/local/bin/svn checkout $svn_url $svn_dir --depth immediates
if ! [ -d "$svn_dir/trunk" ]; then
echo "SVN repo not found. Aborting. If this is a new plugin please first submit manually to WordPress.org and wait for them to approve the plugin and create the SVN repository."
clean_and_exit 1
fi
/usr/local/bin/svn update --quiet $svn_dir/trunk --set-depth infinity
/usr/local/bin/svn update --quiet $svn_dir/tags/$plugin_version --set-depth infinity
echo "Ignoring GitHub specific files"
svn_ignore="README.md
readme.md
Thumbs.db
.github
.git
.gitattributes
.gitignore
.gitmodules
.wordpress-org
composer.lock"
/usr/local/bin/svn propset svn:ignore \""$svn_ignore"\" "$svn_dir/trunk/"
echo ".........................................."
echo
echo "Deploying WordPress plugin"
echo
echo ".........................................."
echo
echo "Copy $version from git to the trunk of svn"
rm -rf $svn_dir/trunk/*
cp -r $git_dir/* $svn_dir/trunk
# If submodules exist, recursively check out their indexes
if [ -f ".gitmodules" ]; then
echo "Exporting each submodule from git to the trunk of svn"
git submodule init
git submodule update
git config -f .gitmodules --get-regexp '^submodule\..*\.path$' |
while read path_key path; do
#url_key=$(echo $path_key | sed 's/\.path/.url/')
#url=$(git config -f .gitmodules --get "$url_key")
#git submodule add $url $path
echo "This is the submodule path: $path"
echo "The following line is the command to checkout the submodule."
echo "git submodule foreach --recursive 'git checkout-index -a -f --prefix=$SVNPATH/trunk/$path/'"
git submodule foreach --recursive 'git checkout-index -a -f --prefix=$svn_dir/trunk/$path/'
done
fi
# Support for the /assets folder on the .org repo, in Git this will be /.wordpress-org
if [ -d $git_dir/.wordpress-org/ ]; then
echo "Moving assets."
# Make the directory if it doesn't already exist
mkdir -p $svn_dir/assets/
rm -rf $svn_dir/assets/*
cp $git_dir/.wordpress-org/* $svn_dir/assets/
svn add --force $svn_dir/assets/
fi
echo "Changing directory to SVN trunk and committing."
cd $svn_dir/trunk/
# Delete all files that should not now be added.
# Use $evn_ignore for `rm -rf`. Setting propset svn:ignore seems flaky.
echo "$svn_ignore" | awk '{print $0}' | xargs rm -rf
/usr/local/bin/svn status | grep -v "^.[ \t]*\..*" | grep "^\!" | awk '{print $2"@"}' | xargs /usr/local/bin/svn del
# Add all new files that are not set to be ignored
/usr/local/bin/svn status | grep -v "^.[ \t]*\..*" | grep "^?" | awk '{print $2"@"}' | xargs svn add
/usr/local/bin/svn commit --username=$svn_user --password=$svn_pass -m "Deploying $plugin_version release"
if [ -d $git_dir/.wordpress-org/ ]; then
echo "Changing directory to SVN assets and committing."
cd $svn_dir/assets/
# Delete all new files that are not set to be ignored
/usr/local/bin/svn status | grep -v "^.[ \t]*\..*" | grep "^\!" | awk '{print $2"@"}' | xargs svn del
# Add all new files that are not set to be ignored
/usr/local/bin/svn status | grep -v "^.[ \t]*\..*" | grep "^?" | awk '{print $2"@"}' | xargs svn add
/usr/local/bin/svn update --quiet --accept working $svn_dir/assets/*
/usr/local/bin/svn resolve --accept working $svn_dir/assets/*
/usr/local/bin/svn commit --username=$svn_user --password=$svn_pass -m "Updating assets"
fi
echo "Creating new svn tag and committing it."
cd $svn_dir/
# If current tag not empty then update readme.txt
if [ -n "$(ls -A tags/$plugin_version 2>/dev/null)" ]; then
echo "Updating readme.txt to tag $plugin_version"
/usr/local/bin/svn delete --force tags/$plugin_version/readme.txt
/usr/local/bin/svn copy trunk/readme.txt tags/$plugin_version
fi
/usr/local/bin/svn copy --quiet trunk/ tags/$plugin_version/
# Remove trunk directories from tag directory
/usr/local/bin/svn delete --force --quiet $svn_dir/tags/$plugin_version/trunk
/usr/local/bin/svn update --quiet --accept working $svn_dir/tags/$plugin_version
cd $svn_dir/tags/$plugin_version
/usr/local/bin/svn commit --username=$svn_user --password=$svn_pass -m "Tagging version $plugin_version"
# clean up after ourselves
echo ".........................................."
echo
echo "Update deployed to WordPress.org"
echo
echo ".........................................."
echo
clean_and_exit 0
;;
esac
}
# process all received refs
while read REF; do process_ref $REF; done
#!/bin/bash
# Sets: new_commits
# Assumes: $oldrev $newrev $refname
#
# This is for use in post receive hooks, as it assumes the refname has moved and
# is now newrev, we need to discard it. This is down with bash string replace,
# as it will replace only the first match, compared to the canonical "grep -v"
# approach which will throw out multiple matches if the same commit is referred
# to by multiple branches.
#
# Excellent, excellent docs from Andy Parkin's email script
#
##################################################
#
# Consider this:
# 1 --- 2 --- O --- X --- 3 --- 4 --- N
#
# O is $oldrev for $refname
# N is $newrev for $refname
# X is a revision pointed to by some other ref, for which we may
# assume that an email has already been generated.
# In this case we want to issue an email containing only revisions
# 3, 4, and N. Given (almost) by
#
# git rev-list N ^O --not --all
#
# The reason for the "almost", is that the "--not --all" will take
# precedence over the "N", and effectively will translate to
#
# git rev-list N ^O ^X ^N
#
# So, we need to build up the list more carefully. git rev-parse
# will generate a list of revs that may be fed into git rev-list.
# We can get it to make the "--not --all" part and then filter out
# the "^N" with:
#
# git rev-parse --not --all | grep -v N
#
# Then, using the --stdin switch to git rev-list we have effectively
# manufactured
#
# git rev-list N ^O ^X
#
# This leaves a problem when someone else updates the repository
# while this script is running. Their new value of the ref we're
# working on would be included in the "--not --all" output; and as
# our $newrev would be an ancestor of that commit, it would exclude
# all of our commits. What we really want is to exclude the current
# value of $refname from the --not list, rather than N itself. So:
#
# git rev-parse --not --all | grep -v $(git rev-parse $refname)
#
# Get's us to something pretty safe (apart from the small time
# between refname being read, and git rev-parse running - for that,
# I give up)
#
#
# Next problem, consider this:
# * --- B --- * --- O ($oldrev)
# \
# * --- X --- * --- N ($newrev)
#
# That is to say, there is no guarantee that oldrev is a strict
# subset of newrev (it would have required a --force, but that's
# allowed). So, we can't simply say rev-list $oldrev..$newrev.
# Instead we find the common base of the two revs and list from
# there.
#
# As above, we need to take into account the presence of X; if
# another branch is already in the repository and points at some of
# the revisions that we are about to output - we don't want them.
# The solution is as before: git rev-parse output filtered.
#
# Finally, tags: 1 --- 2 --- O --- T --- 3 --- 4 --- N
#
# Tags pushed into the repository generate nice shortlog emails that
# summarise the commits between them and the previous tag. However,
# those emails don't include the full commit messages that we output
# for a branch update. Therefore we still want to output revisions
# that have been output on a tag email.
#
# Luckily, git rev-parse includes just the tool. Instead of using
# "--all" we use "--branches"; this has the added benefit that
# "remotes/" will be ignored as well.
#
##################################################
set_new_commits() {
nl=$'\n'
# Get all the current branches, not'd as we want only new ones
new_commits=$(git rev-parse --not --branches)
# Strip off the not current branch
new_hash=$(git rev-parse $refname)
new_commits=${new_commits/^$new_hash/}
# Put back newrev without the not
new_commits=${new_commits}${nl}${newrev}
# Put in ^oldrev if it's not a new branch
if [ "$oldrev" != "0000000000000000000000000000000000000000" ] ; then
new_commits=${new_commits}${nl}^${oldrev}
fi
new_commits=${new_commits/$nl$nl/$nl}
new_commits=${new_commits/#$nl/}
}
# Sets: $change_type
# Assumes: $oldrev $newrev
#
# --- Interpret
# 0000->1234 (create)
# 1234->2345 (update)
# 2345->0000 (delete)
set_change_type() {
if [ "$oldrev" == "0000000000000000000000000000000000000000" ] ; then
change_type="create"
else
if [ "$newrev" == "0000000000000000000000000000000000000000" ] ; then
change_type="delete"
else
change_type="update"
fi
fi
}
# Sets: $newrev_type $oldrev_type $rev $rev_type
# Assumes: $newrev $oldrev
# --- Get the revision types
set_rev_types() {
newrev_type=$(git cat-file -t "$newrev" 2> /dev/null)
oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null)
if [ "$newrev" == "0000000000000000000000000000000000000000" ] ; then
rev_type="$oldrev_type"
rev="$oldrev"
else
rev_type="$newrev_type"
rev="$newrev"
fi
}
# Sets: $describe
# Assumes: $rev
#
# The email subject will contain the best description of the ref that we can build from the parameters
set_describe() {
rev_to_describe="$rev"
if [ "$1" != "" ] ; then
rev_to_describe="$1"
fi
describe=$(git describe $rev_to_describe 2>/dev/null)
if [ -z "$describe" ]; then
describe=$rev_to_describe
fi
}
# Sets: $describe_tags
# Assumes: $rev
#
# The email subject will contain the best description of the ref that we can build from the parameters
set_describe_tags() {
rev_to_describe="$rev"
if [ "$1" != "" ] ; then
rev_to_describe="$1"
fi
describe_tags=$(git describe --tags $rev_to_describe 2>/dev/null)
if [ -z "$describe_tags" ]; then
describe_tags=$rev_to_describe
fi
}
# Takes a lockfile path and command to execute once the lock is acquired.
#
# Retries every second for 5 minutes.
#
# with_lock "foo.lock" "echo a" # Works
# with_lock "foo.lock" "echo b1 ; echo b2" # Work
# several() {
# echo several1 ; echo several2
# }
# with_lock "foo.lock" several # Works
#
with_lock() {
lockfile="$1"
block="$2"
with_lock_has_lock=1
trap with_lock_unlock_if_held INT TERM EXIT
# used to use lockfile to try multiple times but it's not always available
mkdir "$lockfile"
with_lock_has_lock=$?
if [ $with_lock_has_lock -ne 0 ] ; then
exit $?
fi
eval "$block"
rmdir "$lockfile"
with_lock_has_lock=1
}
with_lock_unlock_if_held() {
if [[ "$with_lock_has_lock" -eq 0 ]] ; then
rm -f "$lockfile"
fi
}
display_error_message() {
echo "----------------------------------------------------" >&2
echo "" >&2
echo "" >&2
for ((i=1;i<=$#;i+=1)); do
eval message="$"$i
echo "$message" >&2
done
echo "" >&2
echo "" >&2
echo "----------------------------------------------------" >&2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment