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
@jaklan
Copy link

jaklan commented Nov 20, 2024

@c0m1c5an5 yes, I followed the steps (just fixing the PATH as it was incorrect). And I can confirm it's used, because I added some "debug echos" to the git script, but the issue is it doesn't pass if [ "${1:-}" == "ls-remote" ] && [ "${2:-}" == "--heads" ]; condition and goes straight to exec "${git_bin}" "${@}". I'm currently debugging why the condition is not met.

@jaklan
Copy link

jaklan commented Nov 20, 2024

Okay, now I see the first git command being triggered is:

  semantic-release:git ExecaError: Command failed with exit code 1: git push --dry-run --no-verify '<repo-url> 'HEAD:main'

and that already fails, before running any git ls-remote command. Tested with both CI=true and without.

@jaklan
Copy link

jaklan commented Nov 20, 2024

It seems to be related to these lines:

/**
 * Verify the write access authorization to remote repository with push dry-run.
 *
 * @param {String} repositoryUrl The remote repository URL.
 * @param {String} branch The repository branch for which to verify write access.
 * @param {Object} [execaOpts] Options to pass to `execa`.
 *
 * @throws {Error} if not authorized to push.
 */
export async function verifyAuth(repositoryUrl, branch, execaOptions) {
  try {
    await execa("git", ["push", "--dry-run", "--no-verify", repositoryUrl, `HEAD:${branch}`], execaOptions);
  } catch (error) {
    debug(error);
    throw error;
  }
}

https://github.com/semantic-release/semantic-release/blob/master/lib/git.js#L195C1-L211C2

@c0m1c5an5
Copy link
Author

@jaklan Does this version work for you?

#!/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}" "${@}"

@jaklan
Copy link

jaklan commented Nov 20, 2024

@c0m1c5an5 I had to change it to:

elif [ "${1:-}" == "push" ] && 
     [ "${2:-}" == "--dry-run" ] && 
     [ "${3:-}" == "--no-verify" ]; then
  exit 0
fi

but now works, thanks! I will test it further in CI now (and verify what was the issue with $4 and / or $5)

@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