Skip to content
Draft
Show file tree
Hide file tree
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
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Tests

on:
pull_request:
push:
branches: [master, main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Lint shell
run: shellcheck start.sh tests/*.sh
- name: Unit tests
run: bash tests/unit.sh
- name: E2E tests
run: bash tests/e2e.sh
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ jobs:
| force_with_lease | boolean | false | Determines if force-with-lease push is used. Please specify the corresponding branch inside `ref` section of the checkout action e.g. `ref: ${{ github.head_ref }}`. Be aware, if you want to update the branch and the corresponding tag please use the `force` parameter instead of the `force_with_lease` option. |
| atomic | boolean | true | Determines if atomic push is used. |
| push_to_submodules | string | 'on-demand' | Determines if --recurse-submodules=<strategy> is used. The value defines the used strategy. |
| push_only_tags | boolean | false | Determines if the action should only push the tags, default false |
| push_only_tags | boolean | false | Determines if the action should only push the tags (never the branch), default false |
| tags | boolean | false | Determines if `--tags` is used. |
| directory | string | '.' | Directory to change to before pushing. |
| repository | string | '' | Repository name. <br /> Default or empty repository name represents <br /> current github repository. <br /> If you want to push to other repository, <br /> you should make a [personal access token](https://github.com/settings/tokens) <br /> and use it as the `token` input. |
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ inputs:
description: 'Determines if --tags is used'
required: false
push_only_tags:
description: 'Determines if the action should only push the tags, default false'
description: 'Determines if the action should only push the tags (never the branch), default false'
required: false
directory:
description: 'Directory to change to before pushing.'
Expand Down
2 changes: 1 addition & 1 deletion start.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@ const main = async () => {

main().catch(err => {
console.error(err);
process.exit(-1);
process.exit(1);
})
82 changes: 38 additions & 44 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ INPUT_PUSH_ONLY_TAGS=${INPUT_PUSH_ONLY_TAGS:-false}
INPUT_DIRECTORY=${INPUT_DIRECTORY:-"."}
INPUT_PUSH_TO_SUBMODULES=${INPUT_PUSH_TO_SUBMODULES:-""}
INPUT_PULL=${INPUT_PULL:-false}
_ATOMIC_OPTION=""
_FORCE_OPTION=""
REPOSITORY=${INPUT_REPOSITORY:-$GITHUB_REPOSITORY}

if ${INPUT_PUSH_ONLY_TAGS}; then
Expand All @@ -26,40 +24,20 @@ fi
exit 1;
};

if ${INPUT_FORCE} && ${INPUT_FORCE_WITH_LEASE}; then
if [ "${INPUT_FORCE}" = "true" ] && [ "${INPUT_FORCE_WITH_LEASE}" = "true" ]; then
echo "Please, specify only force or force_with_lease and not both.";
exit 1;
fi

if ${INPUT_ATOMIC}; then
_ATOMIC_OPTION="--atomic"
fi

if ${INPUT_FORCE}; then
_FORCE_OPTION="--force"
fi

if ${INPUT_FORCE_WITH_LEASE}; then
_FORCE_OPTION="--force-with-lease"
fi

if ${INPUT_TAGS} || ${INPUT_PUSH_ONLY_TAGS}; then
_TAGS="--tags"
fi

if [ -n "${INPUT_PUSH_TO_SUBMODULES}" ]; then
_INPUT_PUSH_TO_SUBMODULES="--recurse-submodules=${INPUT_PUSH_TO_SUBMODULES}"
fi

cd ${INPUT_DIRECTORY}
cd "${INPUT_DIRECTORY}"

if git symbolic-ref --quiet HEAD > /dev/null 2>&1; then
_DETACHED_HEAD=false
else
_DETACHED_HEAD=true
fi

if ${_DETACHED_HEAD}; then
if [ "${_DETACHED_HEAD}" = "true" ]; then
echo "Warning: The repository is in a detached HEAD state.";
echo "This happens when actions/checkout checks out a tag, a commit SHA, or a";
echo "pull-request merge commit rather than a real branch.";
Expand All @@ -69,21 +47,29 @@ if ${_DETACHED_HEAD}; then
echo "the push destination — it does not check out a branch in your workspace.";
fi

# Resolved once and shared by pull and push. The token is not embedded in the
# URL (it would show up in process arguments and git error messages); it is
# handed to git through its environment as a host-scoped auth header instead.
if [ "${INPUT_SSH}" = "true" ]; then
remote_repo="git@${INPUT_GITHUB_URL}:${REPOSITORY}.git"
else
remote_repo="${INPUT_GITHUB_URL_PROTOCOL}//${INPUT_GITHUB_URL}/${REPOSITORY}.git"
_auth=$(printf 'oauth2:%s' "${INPUT_GITHUB_TOKEN}" | base64 | tr -d '\n')
_n=${GIT_CONFIG_COUNT:-0}
export "GIT_CONFIG_KEY_${_n}=http.${INPUT_GITHUB_URL_PROTOCOL}//${INPUT_GITHUB_URL}/.extraheader"
export "GIT_CONFIG_VALUE_${_n}=AUTHORIZATION: basic ${_auth}"
export GIT_CONFIG_COUNT=$((_n + 1))
fi

if [ "${INPUT_PULL}" != "false" ]; then
if ${_DETACHED_HEAD}; then
if [ "${_DETACHED_HEAD}" = "true" ]; then
echo "Error: 'pull' is enabled but the repository is in detached HEAD state."
echo "git pull only works when a branch is currently checked out."
echo "Either disable the 'pull' input or check out a branch explicitly"
echo "(e.g. add 'ref: \${{ github.head_ref }}' to the actions/checkout step)."
exit 1
fi

if ${INPUT_SSH}; then
_pull_remote="git@${INPUT_GITHUB_URL}:${REPOSITORY}.git"
else
_pull_remote="${INPUT_GITHUB_URL_PROTOCOL}//oauth2:${INPUT_GITHUB_TOKEN}@${INPUT_GITHUB_URL}/${REPOSITORY}.git"
fi

case "${INPUT_PULL}" in
rebase|true)
_pull_strategy="--rebase"
Expand All @@ -101,23 +87,31 @@ if [ "${INPUT_PULL}" != "false" ]; then
esac

echo "Pulling from ${INPUT_BRANCH} (strategy: ${INPUT_PULL}) before push..."
git pull ${_pull_strategy} "${_pull_remote}" "${INPUT_BRANCH}"
git pull "${_pull_strategy}" "${remote_repo}" "${INPUT_BRANCH}"
fi

if ${INPUT_SSH}; then
remote_repo="git@${INPUT_GITHUB_URL}:${REPOSITORY}.git"
else
remote_repo="${INPUT_GITHUB_URL_PROTOCOL}//oauth2:${INPUT_GITHUB_TOKEN}@${INPUT_GITHUB_URL}/${REPOSITORY}.git"
# Build the push argument list; "$@" keeps each argument intact (no word splitting).
set --
if [ "${INPUT_PUSH_ONLY_TAGS}" = "true" ]; then
# Never pushes the branch, only tags.
set -- "${remote_repo}"
elif [ "${INPUT_FORCE_WITH_LEASE}" != "true" ]; then
set -- "${remote_repo}" "HEAD:${INPUT_BRANCH}"
fi

if ${INPUT_PUSH_ONLY_TAGS}; then
ADDITIONAL_PARAMETERS="${remote_repo}"
elif ! ${INPUT_FORCE_WITH_LEASE}; then
ADDITIONAL_PARAMETERS="${remote_repo} HEAD:${INPUT_BRANCH}"
[ -n "${INPUT_PUSH_TO_SUBMODULES}" ] && set -- "$@" "--recurse-submodules=${INPUT_PUSH_TO_SUBMODULES}"

# --atomic is incompatible with --force-with-lease combined with --tags.
if [ "${INPUT_ATOMIC}" = "true" ] && ! { [ "${INPUT_FORCE_WITH_LEASE}" = "true" ] && [ "${INPUT_TAGS}" = "true" ]; }; then
set -- "$@" "--atomic"
fi

if ${INPUT_FORCE_WITH_LEASE} && ${INPUT_TAGS}; then
_ATOMIC_OPTION=""
set -- "$@" "--follow-tags"

[ "${INPUT_FORCE}" = "true" ] && set -- "$@" "--force"
[ "${INPUT_FORCE_WITH_LEASE}" = "true" ] && set -- "$@" "--force-with-lease"
if [ "${INPUT_TAGS}" = "true" ] || [ "${INPUT_PUSH_ONLY_TAGS}" = "true" ]; then
set -- "$@" "--tags"
fi

git push $ADDITIONAL_PARAMETERS $_INPUT_PUSH_TO_SUBMODULES $_ATOMIC_OPTION --follow-tags $_FORCE_OPTION $_TAGS;
git push "$@"
97 changes: 97 additions & 0 deletions tests/e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash
# End-to-end tests: run start.js (and thus start.sh) with real git against a
# local bare repository. git's insteadOf rewrites the action's HTTPS URL to it.
ROOT=$(cd "$(dirname "$0")/.." && pwd)
. "$ROOT/tests/lib.sh"

SANDBOX=$(mktemp -d)
trap 'rm -rf "$SANDBOX"' EXIT
export HOME="$SANDBOX/home"; mkdir -p "$HOME"
export GIT_CONFIG_NOSYSTEM=1
git config --global user.name tester
git config --global user.email tester@example.test
git config --global init.defaultBranch main
git config --global protocol.file.allow always

BARE="$SANDBOX/remote.git"
URL="https://example.test/owner/repo.git"
git config --global "url.file://$BARE.insteadOf" "$URL"

# fresh_world: new bare remote with one commit on main, plus a work clone.
fresh_world() {
rm -rf "$BARE" "$SANDBOX/seed" "$SANDBOX/work"
git init -q --bare "$BARE"
git init -q "$SANDBOX/seed"
(cd "$SANDBOX/seed" && echo base > f && git add f && git commit -qm base \
&& git push -q "$BARE" HEAD:main)
git clone -q "$BARE" "$SANDBOX/work"
}
commit_in() { (cd "$1" && echo "$2" > "$2" && git add "$2" && git commit -qm "$2"); }
remote_head() { git -C "$BARE" rev-parse "${1:-main}"; }

# action [VAR=value...]: run the action from the work clone.
action() {
OUT=$(cd "$SANDBOX/work" && env INPUT_BRANCH=main INPUT_TOKEN=tok \
INPUT_GITHUB_URL=https://example.test INPUT_REPOSITORY=owner/repo \
GITHUB_REPOSITORY=owner/repo "$@" node "$ROOT/start.js" 2>&1)
CODE=$?
}

echo "plain push"
fresh_world; commit_in "$SANDBOX/work" a
action
assert_eq "exits 0" 0 "$CODE"
assert_eq "remote main updated" "$(git -C "$SANDBOX/work" rev-parse HEAD)" "$(remote_head)"

echo "non-fast-forward is rejected"
fresh_world; commit_in "$SANDBOX/work" a; commit_in "$SANDBOX/seed" b
(cd "$SANDBOX/seed" && git push -q "$BARE" HEAD:main)
action
assert_eq "exits 1" 1 "$CODE"

echo "force overwrites diverged remote"
action INPUT_FORCE=true
assert_eq "exits 0" 0 "$CODE"
assert_eq "remote main replaced" "$(git -C "$SANDBOX/work" rev-parse HEAD)" "$(remote_head)"

echo "pull rebase integrates remote changes"
fresh_world; commit_in "$SANDBOX/work" a; commit_in "$SANDBOX/seed" b
(cd "$SANDBOX/seed" && git push -q "$BARE" HEAD:main)
action INPUT_PULL=rebase
assert_eq "exits 0" 0 "$CODE"
assert_eq "remote has both commits" 3 "$(git -C "$BARE" rev-list --count main)"

echo "tags"
fresh_world; commit_in "$SANDBOX/work" a; git -C "$SANDBOX/work" tag v1
action INPUT_TAGS=true
assert_eq "exits 0" 0 "$CODE"
assert_eq "tag pushed" "$(git -C "$SANDBOX/work" rev-parse v1)" "$(git -C "$BARE" rev-parse v1)"

echo "directory input"
fresh_world; commit_in "$SANDBOX/work" a
mkdir "$SANDBOX/elsewhere"
OUT=$(cd "$SANDBOX/elsewhere" && env INPUT_BRANCH=main INPUT_TOKEN=tok INPUT_GITHUB_URL=https://example.test \
INPUT_REPOSITORY=owner/repo INPUT_DIRECTORY="$SANDBOX/work" node "$ROOT/start.js" 2>&1); CODE=$?
assert_eq "exits 0" 0 "$CODE"
assert_eq "pushed from directory" "$(git -C "$SANDBOX/work" rev-parse HEAD)" "$(remote_head)"

echo "push_only_tags"
fresh_world; commit_in "$SANDBOX/work" a; git -C "$SANDBOX/work" tag v2
action INPUT_PUSH_ONLY_TAGS=true INPUT_TAGS=true
assert_eq "exits 0" 0 "$CODE"
assert_eq "tag pushed" "$(git -C "$SANDBOX/work" rev-parse v2)" "$(git -C "$BARE" rev-parse v2)"
assert_eq "branch not pushed" 1 "$(git -C "$BARE" rev-list --count main)"

echo "detached HEAD"
fresh_world; git -C "$SANDBOX/work" checkout -q --detach
action
assert_contains "warns" "$OUT" "detached HEAD"
action INPUT_PULL=true
assert_eq "pull on detached HEAD exits 1" 1 "$CODE"

echo "validation"
fresh_world
action INPUT_TOKEN= INPUT_GITHUB_TOKEN=
assert_eq "missing token exits 1" 1 "$CODE"

summary
22 changes: 22 additions & 0 deletions tests/lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
# Minimal assertion helpers shared by unit.sh and e2e.sh.
PASS=0
FAIL=0

ok() { PASS=$((PASS + 1)); echo " ok - $1"; }
fail() { FAIL=$((FAIL + 1)); echo " FAIL - $1"; [ -n "${2:-}" ] && echo " $2"; }

assert_eq() { # name expected actual
if [ "$2" = "$3" ]; then ok "$1"; else fail "$1" "expected '$2', got '$3'"; fi
}
assert_contains() { # name haystack needle
case "$2" in *"$3"*) ok "$1" ;; *) fail "$1" "'$3' not found in: $2" ;; esac
}
assert_not_contains() { # name haystack needle
case "$2" in *"$3"*) fail "$1" "'$3' unexpectedly found in: $2" ;; *) ok "$1" ;; esac
}

summary() {
echo; echo "passed: $PASS, failed: $FAIL"
[ "$FAIL" -eq 0 ]
}
Loading
Loading