Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 98 additions & 16 deletions auto-deploy/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@

//Make a determiniation if the incoming operation is right for this system.
if($gitSystem && $HOSTNAME && $OPERATION){
$refs=explode("/",$req['ref']);
// The ref is interpolated into shell commands further down (`git checkout
// <ref>`) and this endpoint is unauthenticated, so accept only a plain,
// well-formed ref name and refuse anything else. Without this, a crafted
// payload could smuggle shell metacharacters through to shell_exec().
$incoming_ref = isset($req['ref']) ? trim($req['ref']) : "";
if(!preg_match('#^refs/(heads|tags)/[A-Za-z0-9._/-]+$#', $incoming_ref)){
_log("Refusing to act on a malformed or unsupported ref: " . json_encode($incoming_ref) . "\n");
exit;
}

$refs=explode("/",$incoming_ref);
$incomingOp=$refs[1]; // should be tags or heads

//Some initial validation, make sure this is a configured repo and stuff
Expand All @@ -86,16 +96,82 @@
}

//Make sure we're doing a TAG or BRANCH as appropriate
if(($incomingOp == "tags" && $OPERATION = "TAG") || ($incomingOp == "heads" && $refs[2] == $OPERATION)){
//possibly need to clone it or pull it first:

$out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION);
_debug("$out\n");

//The following will ensure we're either on the right branch, or the right tag:
$out = do_checkout_branch($req,$deploy_to, $OPERATION);
_debug("$out\n");

// NOTE: the first clause below is `==`, not `=`. It was previously an
// assignment, which made the condition true for EVERY tag push regardless of
// how this host is configured -- and, worse, reassigned $OPERATION to "TAG"
// for the rest of the request. The effect was that a tag push drove a
// branch-tracking host (our test server) down the tag code path, leaving its
// checkout detached at that tag.
if(($incomingOp == "tags" && $OPERATION == "TAG") || ($incomingOp == "heads" && $refs[2] == $OPERATION)){

// Order matters, and it differs between the two cases:
//
// - TAG: fetch first. The tag usually doesn't exist locally yet, so
// checking it out before fetching would fail.
// - BRANCH: check out the branch first. `git pull` aborts outright
// from a detached HEAD ("You are not currently on a branch"), and
// the old fetch-then-checkout order could never recover from that:
// the pull failed, the local branch stayed behind, and the deploy
// ran anyway (see below) -- so the container rebuilt from stale
// source while looking freshly deployed.
$repo_exists = is_dir($deploy_to) && file_exists($deploy_to . "/.git");

if($OPERATION != "TAG" && $repo_exists){
$out = do_checkout_branch($req,$deploy_to, $OPERATION);
_debug("$out\n");

$out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION);
_debug("$out\n");
}
else{
//possibly need to clone it or pull it first:
$out = do_clone_or_pull(DEPLOY_KEY,$url,$deploy_to, $OPERATION);
_debug("$out\n");

//The following will ensure we're either on the right branch, or the right tag:
$out = do_checkout_branch($req,$deploy_to, $OPERATION);
_debug("$out\n");
}

// Guard against silently deploying stale source. Historically the
// container build below ran unconditionally after checkout/pull, so a
// failed pull (detached HEAD, merge conflict, network/ssh error) would
// rebuild from STALE source while looking freshly deployed. Confirm the
// checked-out HEAD is the commit that was actually pushed ($req['after'])
// before building. If the payload carries no usable SHA we fall back to
// deploying (preserves prior behavior rather than risk blocking a deploy).
// (On rapid successive pushes HEAD may already be a *newer* commit than
// this event's 'after'; skipping here is safe -- the newer push's hook
// deploys the newer state.)
//
// IMPORTANT: 'after' is the ref's new VALUE, which for an ANNOTATED tag is
// the tag OBJECT's sha -- not the commit's. `git checkout refs/tags/X`
// leaves HEAD at the COMMIT, so a raw sha comparison would abort every
// annotated-tag deploy, i.e. every production release (2.27.0/.2/.3 are all
// annotated). Peel with `^{commit}` before comparing.
$expected_sha = isset($req['after']) ? strtolower(trim($req['after'])) : "";
$has_sha = preg_match('/^[0-9a-f]{40}$/', $expected_sha) && !preg_match('/^0+$/', $expected_sha);

$head_sha = "";
$expected_commit = "";
if($has_sha){
$head_sha = strtolower(trim(shell_exec("bash -c 'cd $deploy_to; git rev-parse HEAD' 2>/dev/null")));
$expected_commit = strtolower(trim(shell_exec(
"bash -c 'cd $deploy_to; git rev-parse --verify --quiet {$expected_sha}^{commit}' 2>/dev/null")));
}

// Fail SAFE: abort only when we positively know HEAD is the wrong commit.
// A sha we can't resolve locally (fetch failed, unusual payload, ...) falls
// through to the historical deploy-anyway behavior rather than blocking a
// release on a check we aren't sure about.
if($head_sha !== "" && $expected_commit !== "" && $head_sha !== $expected_commit){
_log("ABORTING DEPLOY: checked-out HEAD ($head_sha) does not match the ".
"pushed commit ($expected_commit, from ref value $expected_sha) -- ".
"the pull/checkout above likely ".
"failed. Refusing to rebuild the container from stale source.\n");
exit;
}

//If we're using docker, then do someother stuff
if($USE_DOCKER){

Expand Down Expand Up @@ -195,7 +271,7 @@ function do_checkout_branch($req, $deployto, $operation){
*/
function determine_branch_name( $request) {
// push request, branch is in request[ref]
if (isset($reqest['ref'])) {
if (isset($request['ref'])) {
// strip out the refs/head nonsense -- doesn't look like bare
// branch is listed anywhere in the request
return preg_replace("|refs/heads/|", "", $request['ref']);
Expand Down Expand Up @@ -280,18 +356,24 @@ function do_clone($key,$url,$path) {
* Log a message, only here to make some messages easy to turn off.
*/
function _trace($message) {
if ($trace = TRUE) {
// NOTE: was `if ($trace = TRUE)` -- an assignment, not a comparison, so it
// was always true and could never be turned off (same =/== footgun fixed in
// the deploy condition above). Default to on to preserve prior behavior;
// config.php can silence it with `define('AUTODEPLOY_TRACE', false);`.
if (defined('AUTODEPLOY_TRACE') ? AUTODEPLOY_TRACE : TRUE) {
_log($message);
}
}
}

/**
* Log a message, only here to make some messages easy to turn off.
*/
function _debug($message) {
if ($debug = TRUE) {
// Same fix as _trace(): `$debug = TRUE` was an assignment, always true.
// Default on; silence via `define('AUTODEPLOY_DEBUG', false);` in config.php.
if (defined('AUTODEPLOY_DEBUG') ? AUTODEPLOY_DEBUG : TRUE) {
_log($message);
}
}
}

/**
Expand Down
Loading