Skip to content

Instantly share code, notes, and snippets.

@tmarsteel
Created September 17, 2017 15:37
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 tmarsteel/7789e628ec52321c40908b61e97f6511 to your computer and use it in GitHub Desktop.
Save tmarsteel/7789e628ec52321c40908b61e97f6511 to your computer and use it in GitHub Desktop.
PHP Script for pull deployment from git scm
#!/usr/bin/php5.5-cli
<?php
/**
This script checks out a commit from a git-repository into a directory revision/$hash within the context
directory. Then creates the symbolic links defined in $links within that directory and swiches the symbolic
link $CURRENT_DEPLOYMENT_POINTER_LINK to that new directory.
*/
// CONFIGURATION
// !! ALL PATHS MUST BE ABSOLUTE
# repository from which to checkout; authentication is supposed to happen using the ssh key of the target machine
$REPOSITORY_URL = "git@github.com/me/myrepo";
# directory where all the deployments reside
# TRAILING SLASH
$CONTEXT_DIRECTORY = "/var/www/my-project";
# the symbolic link that points to the currently deployed revision
$CURRENT_DEPLOYMENT_POINTER_LINK = "/var/www/my-project/html";
# this file is used to block concurrent operations with this script
$LOCK_FILE_NAME = $CONTEXT_DIRECTORY . ".deployment-lock";
# where a local copy of the repository is stored; you do NOT need to change this unless there is a conflic with file
# names; TRAILING SLASH!
$LOCAL_REPO_COPY_DIR = "/var/www/my-project/repository";
# symlinks to create; key => link name within the deployment directory, value = target
$SYMLINKS = array(
"files" => "/var/www/my-project/shared/files"
);
// END OF CONFIGURATION
// the file handle to the lock file
$LOCK_HANDLE = null;
if (!defined("STDIN") || !defined("STDOUT") || !defined("STDERR")) {
exit("not in cli");
}
if (version_compare(PHP_VERSION, '5.5') < 0) {
fwrite(STDERR, "[ERROR] Minimum PHP version is 5.5, using " . PHP_VERSION . PHP_EOL);
exit(1);
}
// assure file permissions
if (!file_exists($CONTEXT_DIRECTORY) || !is_dir($CONTEXT_DIRECTORY)) {
fwrite(STDERR, "[ERROR] Context directory " . $CONTEXT_DIRECTORY . " does not exist or is not a directory" . PHP_EOL);
exit(1);
}
assertReadable($CONTEXT_DIRECTORY);
assertWritable($CONTEXT_DIRECTORY);
if (file_exists($LOCAL_REPO_COPY_DIR)) {
assertReadable($LOCAL_REPO_COPY_DIR);
assertWritable($LOCAL_REPO_COPY_DIR);
}
if (file_exists($LOCK_FILE_NAME)) {
assertWritable($LOCK_FILE_NAME);
}
else {
assertWritable(dirname($LOCK_FILE_NAME));
}
// assure git is installed
if (runCommand(["git", "--version"], false) !== 0) {
fwrite(STDERR, "[ERROR] git appears not to be installed (`git --version` returned a non-zero exit code)" . PHP_EOL);
exit(1);
}
// parse commands
$args = array_slice($GLOBALS["argv"], 1);
$selfName = $GLOBALS["argv"][0];
if (count($args) == 0 || $args[0] == "--help") {
printHelpAndExit();
}
$FORCE_UPDATE_LOCAL_COPY = false;
if ($args[0] == "-f" || $args[0] == "--force") {
$FORCE_UPDATE_LOCAL_COPY = true;
$args = array_slice($args, 1);
}
// determine the ref to pass to checkout
$CHECKOUT_REF = "";
$CHECKOUT_REF_IS_COMMIT = false;
if ($args[0] == "commit") {
if (!isset($args[1])) {
fwrite(STDERR, "[ERROR] No commit hash given, see " . $selfName . " --help" . PHP_EOL);
exit(1);
}
$givenHash = $args[1];
if (!preg_match("/^([0-9a-f]{7}|[0-9a-f]{40})$/", $givenHash)) {
fwrite(STDERR, "[ERROR] Invalid commit hash; hexadecimal with 7 or 32 characters needed" . PHP_EOL);
exit(1);
}
$CHECKOUT_REF = $givenHash;
$CHECKOUT_REF_IS_COMMIT = true;
}
else if ($args[0] == "tag") {
if (!isset($args[1])) {
fwrite(STDERR, "[ERROR] No tag name given, see " . $selfName . " --help" . PHP_EOL);
exit(1);
}
$CHECKOUT_REF = "tags/" . $args[1];
}
else if ($args[0] == "branch") {
if (!isset($args[1])) {
fwrite(STDERR, "[ERROR] No branch name given, see " . $selfName . " --help" . PHP_EOL);
exit(1);
}
$CHECKOUT_REF = $args[1];
}
else {
fwrite(STDERR, "[ERROR] Unknown checkout type " . $args[0] . ", see " . $selfName . " --help" . PHP_EOL);
exit(1);
}
// assure the repository is present
if (!file_exists($LOCAL_REPO_COPY_DIR)) {
if (!mkdir($LOCAL_REPO_COPY_DIR, 0750)) {
fwrite(STDOUT, "[ERROR] Failed to create directory " . $LOCAL_REPO_COPY_DIR . " with permissions rwxr-x---" . PHP_EOL);
exit();
}
chdir($LOCAL_REPO_COPY_DIR);
runCommand(["git", "init"]);
runCommand(["git", "remote", "add", "origin", $REPOSITORY_URL]);
}
acquireLockOrQuit();
// make sure the "origin" remote points to the correct URL
chdir($LOCAL_REPO_COPY_DIR);
runCommand(["git", "remote", "set-url", "origin", $REPOSITORY_URL]);
// update repo
fwrite(STDOUT, "[INFO] updating local repository" . PHP_EOL);
runCommand(["git", "fetch", "origin"]);
// determine the target commit
$gitOutput = [];
$verifyRef = $CHECKOUT_REF_IS_COMMIT? $CHECKOUT_REF : "remotes/origin/" . $CHECKOUT_REF;
runCommand(["git", "rev-parse", "--verify", $verifyRef], true, $gitOutput);
$TARGET_COMMIT = isset($gitOutput[0]) ? trim($gitOutput[0]) : "";
if (!preg_match("/^[0-9a-fA-F]{40}$/", $TARGET_COMMIT)) {
fwrite(STDOUT, "[ERROR] Failed to resolve ref " . $CHECKOUT_REF . " to a commit hash");
exit(1);
}
fwrite(STDOUT, "[INFO] Ref " . $CHECKOUT_REF . " is commit " . $TARGET_COMMIT . PHP_EOL);
$TARGET_DIRECTORY = $CONTEXT_DIRECTORY . "revisions/" . $TARGET_COMMIT . "/";
$copyCommitContentsToTargetDirectory = $FORCE_UPDATE_LOCAL_COPY;
// assure target exists and has permissions
if (file_exists($TARGET_DIRECTORY)) {
if (!is_dir($TARGET_DIRECTORY)) {
fwrite(STDERR, "[ERROR] " . $TARGET_DIRECTORY . " exists and is not a directory" . PHP_EOL);
exit(1);
}
assertWritable($TARGET_DIRECTORY);
if ($FORCE_UPDATE_LOCAL_COPY) {
$copyCommitContentsToTargetDirectory = true;
}
else {
fwrite(STDOUT, "[INFO] Commit " . $TARGET_COMMIT . " already checked out -- reusing" . PHP_EOL);
}
}
else {
if (!mkdir($TARGET_DIRECTORY, 0705, true)) {
fwrite(STDERR, "[ERROR] Failed to create directory " . $TARGET_DIRECTORY . " with permissions rwx---r-x" . PHP_EOL);
exit(1);
}
$copyCommitContentsToTargetDirectory = true;
}
// assure the hash directory is filled with content
if ($copyCommitContentsToTargetDirectory) {
// fetch stuff from remote
fwrite(STDOUT, "[INFO] Checking out commit " . $TARGET_COMMIT . " from origin" . PHP_EOL);
runCommand(["git", "checkout", "--force", $TARGET_COMMIT]);
runCommand(["git", "reset", "--hard"]);
fwrite(STDOUT, "[INFO] Copying commit contents over to " . $TARGET_DIRECTORY . PHP_EOL);
// copy stuff over
runCommand(["cp", "-R", $LOCAL_REPO_COPY_DIR . ".", $TARGET_DIRECTORY]);
// remove git data from target directory
runCommand(["rm", "-rf", $TARGET_DIRECTORY . ".git"]);
// create all the symlinks
fwrite(STDOUT, "[INFO] Creating symlinks in " . $TARGET_DIRECTORY . PHP_EOL);
foreach ($SYMLINKS as $linkName => $target) {
runCommand(["ln", "-sf", $target, $TARGET_DIRECTORY . $linkName]);
fwrite(STDOUT, "[INFO] Link " . $linkName . " -> " . $target . " created" . PHP_EOL);
}
}
fwrite(STDOUT, "[INFO] Switching symlink " . $CURRENT_DEPLOYMENT_POINTER_LINK . " to " . $TARGET_DIRECTORY . PHP_EOL);
// switch the link
chdir(dirname($CURRENT_DEPLOYMENT_POINTER_LINK));
if (file_exists($CURRENT_DEPLOYMENT_POINTER_LINK)) {
runCommand(["unlink", basename($CURRENT_DEPLOYMENT_POINTER_LINK)]);
}
runCommand(["ln", "-s", rtrim($TARGET_DIRECTORY, "/"), basename($CURRENT_DEPLOYMENT_POINTER_LINK)]);
fwrite(STDOUT, "[INFO] Deployment of " . $CHECKOUT_REF . " complete" . PHP_EOL);
releaseLock();
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS
function printHelpAndExit() {
global $selfName;
fwrite(STDOUT, "----- DEPLOYMENT SCRIPT ------" . PHP_EOL);
fwrite(STDOUT, "Usage: " . $selfName . " [-f|--force] (commit|branch|tag) git-revision" . PHP_EOL);
fwrite(STDOUT, " " . $selfName . " commit 7fc914c" . PHP_EOL);
fwrite(STDOUT, " " . $selfName . " branch my-branch" . PHP_EOL);
fwrite(STDOUT, " " . $selfName . " tag v1.2" . PHP_EOL);
fwrite(STDOUT, "If the given commit has already been checked out it will be reused." . PHP_EOL);
fwrite(STDOUT, "Specify the --force flag to check out that commit again." . PHP_EOL);
exit();
}
function runCommand(array $parts, $failOnNonZeroExitStatus = true, array &$output = null) {
$escaped = [];
foreach ($parts as $part) {
$escaped []= escapeshellarg($part);
}
$exitStatus = 0;
$command = implode(" ", $escaped);
// fwrite(STDOUT, "[DEBUG] Running \"" . $command . "\" in " . getcwd() . PHP_EOL);
exec($command, $output, $exitStatus);
if ($failOnNonZeroExitStatus && $exitStatus != 0) {
fwrite(STDERR, "[ERROR] Command \"" . $command . "\" failed with status " . $exitStatus . PHP_EOL);
exit($exitStatus);
}
return $exitStatus;
}
function assertWritable($dir) {
if (!is_writeable($dir)) {
fwrite(STDERR, "[ERROR] Missing write permissions on " . $dir . PHP_EOL);
exit(1);
}
}
function assertReadable($dir) {
if (!is_readable($dir)) {
fwrite(STDERR, "[ERROR] Missing read permissions on " . $dir . PHP_EOL);
exit(1);
}
}
function acquireLockOrQuit() {
global $LOCK_FILE_NAME, $LOCK_HANDLE;
$LOCK_HANDLE = fopen($LOCK_FILE_NAME, "w");
if ($LOCK_HANDLE === false) {
fwrite(STDERR, "[ERROR] Failed to acquire lock " . $LOCK_FILE_NAME . PHP_EOL);
exit(1);
}
}
function releaseLock() {
global $LOCK_FILE_NAME, $LOCK_HANDLE;
if ($LOCK_HANDLE != null) {
fclose($LOCK_HANDLE);
}
if (file_exists($LOCK_FILE_NAME)) {
unlink($LOCK_FILE_NAME);
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment