Skip to content

Instantly share code, notes, and snippets.

@c0m1c5an5
Last active March 19, 2025 18:28
Show Gist options
  • Save c0m1c5an5/0a1315a135b37b00ba58d07881a1ec6a to your computer and use it in GitHub Desktop.
Save c0m1c5an5/0a1315a135b37b00ba58d07881a1ec6a to your computer and use it in GitHub Desktop.
Git wrapper to allow semantic-release to work when HEAD is behind remote.

I highly recommend using python-semantic-release instead. It provides full control over the release creation and does not have roadblocks in place preventing you from using it on non-latest commits. Still, if using semantic-release is a must, above scripts provide the means to do so.

#!/usr/bin/env sh
# Copyright (c) 2024 Maksym Kondratenko
# SPDX-License-Identifier: BSD-3-Clause
set -eu
git_bin="/usr/bin/git"
get_remote() {
"${git_bin}" config --get remote.origin.url || :
}
get_branch() {
"${git_bin}" symbolic-ref -q --short HEAD || :
}
if ! [ -x "${git_bin}" ]; then
echo >&2 "ERR: Git binary '${git_bin}' not executable."
exit 127
fi
if [ "${1:-}" == "ls-remote" ] &&
[ "${2:-}" == "--heads" ]; then
if [ "${3:-}" == "$(get_remote)" ]; then
shift 3
exec "${git_bin}" show-ref --heads "${@}"
fi
elif [ "${1:-}" == "push" ] &&
[ "${2:-}" == "--dry-run" ] &&
[ "${3:-}" == "--no-verify" ] &&
[ "${4:-}" == "$(get_remote)" ] &&
[ "${5:-}" == "HEAD:$(get_branch)" ]; then
exit 0
fi
exec "${git_bin}" "${@}"
# This snippet shows how to use the provided git wrapper
# to allow for execution when HEAD is behind remote branch.
mkdir -p ~/.local/opt/git-remote-mock
wget -O ~/.local/opt/git-remote-mock/git https://gist.githubusercontent.com/c0m1c5an5/0a1315a135b37b00ba58d07881a1ec6a/raw/07064587985e65d80fc3ee717551841330cdf832/git
chmod 755 ~/.local/opt/git-remote-mock/git
PATH="${HOME}/.local/opt/git-remote-mock:${PATH}" CI=true semantic-release
@c0m1c5an5
Copy link
Author

@jaklan It is most probable, that the $5 is different from what is expected.
Is the fifth argument to the wrapper call with [ "git", "--dry-run", "--no-verify", ... ] the same as the result of running git symbolic-ref -q --short HEAD in your repository? (The strace command above outputs the info required).

@jaklan
Copy link

jaklan commented Dec 2, 2024

Just as a follow-up - we have eventually decided to overwrite node_modules/semantic-release/lib/git.js directly instead. We needed to change 2 functions:

  • always return true in isBranchUpToDate():
    export async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
     return true;
    }
  • refs/heads/<placeholder> instead of branch in verifyAuth() (maybe it could just be replaced with another git function in the dry-run mode, but haven't tested):
    export async function verifyAuth(repositoryUrl, branch, execaOptions) {
      try {
        await execa(
          "git",
          ["push", "--dry-run", "--no-verify", repositoryUrl, `HEAD:refs/heads/placeholder`],
          execaOptions
        );
      } catch (error) {
        debug(error);
        throw error;
      }
    }

The above could be also reflected in a git script more or less this way:

#!/usr/bin/env bash

set -eu
git_bin="/usr/bin/git"

if ! [ -x "$git_bin" ]; then
 echo >&2 "ERROR: '$git_bin' is not executable"
 exit 127
fi

if [ "${1:-}" = "ls-remote" ] &&
  [ "${2:-}" = "--heads" ] &&
  [ -n "${3:-}" ] &&
  [ -n "${4:-}" ]; then
 exec $git_bin show-ref --heads "${4}"
elif [ "${1:-}" = "push" ] && 
    [ "${2:-}" = "--dry-run" ] &&
    [ "${3:-}" = "--no-verify" ] &&
    [ -n "${4:-}" ]; then
 exec $git_bin "${@:1:4}" "HEAD:refs/heads/placeholder"
else
 exec $git_bin "${@}"
fi

Because we changed the approach - I haven't really tested that, but maybe could be helpful for others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment