2025-02-19 13:29:32 +01:00
name : "FTP/SFTP file deployer"
description : "Fast and customizable deployment with parallel connections and proxy support. Deploy only changed files or do full sync."
branding :
icon : "upload"
color : "black"
inputs :
remote-protocol :
description : "Remote file transfer protocol (ftp, sftp)"
required : true
default : "sftp"
remote-host :
description : "Remote host"
required : true
remote-port :
description : "Remote port"
required : true
default : 22
remote-user :
description : "FTP/SSH username"
required : true
remote-password :
description : "FTP/SSH password"
required : false
default : ""
ssh-private-key :
description : "SSH private key of user"
required : false
proxy :
description : "Enable proxy for FTP connection (true, false)"
required : true
default : false
proxy-host :
description : "Proxy host"
required : false
proxy-port :
description : "Proxy port"
required : false
default : 22
proxy-forwarding-port :
description : "Proxy forwarding port"
required : false
default : 1080
proxy-user :
description : "Proxy username"
required : false
proxy-private-key :
description : "Proxy SSH private key of user"
required : false
local-path :
description : "Local path to repository"
required : true
default : .
remote-path :
description : "Remote path on host"
required : true
default : .
sync :
description : "File synchronization (delta, full)"
required : true
default : "delta"
sync-delta-excludes :
description : "Files to exclude from delta sync"
required : false
ssh-options :
description : "Additional command arguments for SSH client"
required : false
ftp-options :
description : "Additional command arguments for FTP client (lftp)"
required : false
ftp-mirror-options :
description : "Additional command arguments for mirroring (lftp)"
required : false
ftp-post-sync-commands :
description : "Additional ftp commands to run after synchronization (lftp)"
required : false
webhook :
description : "Send webhook event notifications"
required : false
artifacts :
description : "Upload logs to artifacts"
required : false
debug :
description : "Enable debug information (true, false)"
required : false
runs :
using : "composite"
steps :
- name : "Deploy"
shell : bash
run : |
echo "::group::Initialization"
function show_hr() {
printf '%.s_' {1..100} && echo ""
}
function send_webhook() {
local status="$1"
local post_data=$(jq --null-input \
--arg timestamp `date +%s` \
--arg status "${status}" \
--arg repository "${{ gitea.repository }}" \
--arg workflow "${{ gitea.workflow }}" \
--arg job "${{ gitea.job }}" \
--arg run_id "${{ gitea.run_id }}" \
--arg ref "${{ gitea.ref }}" \
--arg event_name "${{ gitea.event_name }}" \
--arg actor "${{ gitea.actor }}" \
--arg message "${{ gitea.event.head_commit.message }}" \
--arg revision "${{ gitea.sha }}" \
'{"timestamp": $timestamp, "status": $status, "repository": $repository, "workflow": $workflow, "job": $job, "run_id": $run_id, "ref": $ref, "event_name": $event_name, "actor": $actor, "message": $message, "revision": $revision}' )
curl --data "${post_data}" --header "Content-Type: application/json" --max-time 30 --show-error --silent --user-agent "Gitea Workflow" "${{inputs.webhook}}"
}
[ "${{inputs.webhook}}" != "" ] && echo "Webhook notification (start): $(send_webhook "start")"
echo "Check repository"
if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then
echo "::error::Git repository not found. Please ensure you have a checkout step before this step."
exit 1
fi
echo "Initialize inputs"
local_path_unslash=$(echo "${{inputs.local-path}}" | sed 's:/*$::')
local_path_slash="${local_path_unslash}/"
remote_path_unslash=$(realpath --canonicalize-missing '${{inputs.remote-path}}')
remote_path_slash="${remote_path_unslash}/"
input_remote_password="${{inputs.remote-password}}"
if [ "${{inputs.remote-password}}" == "" ]; then
input_remote_password="dummypassword"
fi
input_proxy="${{inputs.proxy}}"
proxy_cmd=""
if [ "${input_proxy}" == "true" ]; then
proxy_cmd="proxychains"
fi
input_sync=${{inputs.sync}}
if [ "${{gitea.event_name}}" == "workflow_dispatch" ] && [ -n "${{gitea.event.inputs.sync}}" ]; then
input_sync=${{gitea.event.inputs.sync}}
fi
echo "Validate inputs"
if [ "${{inputs.remote-protocol}}" != "sftp" ] && [ "${{inputs.remote-protocol}}" != "ftp" ]; then
echo "::error::Invalid protocol: ${{inputs.remote-protocol}}. Valid protocols are 'ftp' and 'sftp'."
exit 1
fi
if [ "${input_sync}" != "delta" ] && [ "${input_sync}" != "full" ]; then
echo "::error::Invalid synchronization: ${input_sync}. Valid types are 'delta' and 'full'."
exit 1
fi
echo "::endgroup::"
if [ "${{inputs.debug}}" == "true" ]; then
echo "::group::Debug"
echo "Context: gitea.event" && cat ${{gitea.event_path}} && show_hr
echo "Context: env" && echo "${{toJSON(env)}}" && show_hr
echo "Inputs:" && echo "${{toJSON(inputs)}}"
echo "::endgroup::"
fi
echo "::group::Install packages"
apt_install=""
apt_quiet="--quiet --quiet"
if [ "${{inputs.debug}}" == "true" ]; then
apt_quiet=""
fi
sudo apt-get ${apt_quiet} update && sudo apt-get ${apt_quiet} --no-install-recommends --yes install lftp ${proxy_cmd}
echo "::endgroup::"
echo "::group::Configurations"
config_ssh=~/.ssh/config
mkdir ~/.ssh && echo -e "ExitOnForwardFailure=yes\nStrictHostKeyChecking=no" > ${config_ssh} && chmod 600 ${config_ssh} && echo "File created: ${config_ssh}"
[ "${{inputs.debug}}" == "true" ] && cat ${config_ssh}
show_hr
netrc=~/.netrc
echo "machine ${{inputs.remote-host}} login ${{inputs.remote-user}} password ${input_remote_password}" > ${netrc} && chmod 600 ${netrc} && echo "File created: ${netrc}"
[ "${{inputs.debug}}" == "true" ] && cat ${netrc}
show_hr
if [ "${{inputs.remote-protocol}}" == "sftp" ] && [ "${{inputs.ssh-private-key}}" != "" ]; then
key_ssh=~/ssh_private_key
echo "${{inputs.ssh-private-key}}" > ${key_ssh} && chmod 600 ${key_ssh} && echo "File created: ${key_ssh}" && show_hr
fi
if [ "${input_proxy}" == "true" ]; then
if [ "${{inputs.proxy-private-key}}" != "" ]; then
key_proxy=~/proxy_private_key
echo "${{inputs.proxy-private-key}}" > ${key_proxy} && chmod 600 ${key_proxy} && echo "File created: ${key_proxy}" && show_hr
config_proxychains=~/.proxychains/proxychains.conf
mkdir ~/.proxychains && echo "strict_chain
quiet_mode
tcp_read_time_out 15000
tcp_connect_time_out 10000
[ ProxyList]
socks5 127.0.0.1 ${{inputs.proxy-forwarding-port}}" > ${config_proxychains} && echo "File created: ${config_proxychains}"
[ "${{inputs.debug}}" == "true" ] && cat ${config_proxychains}
show_hr
else
input_proxy="false"
echo "::warning::Invalid input 'proxy-private-key'. Skipping proxy connection."
fi
fi
echo "debug $([ "${{inputs.debug}}" == "true" ] && echo "9" || echo "false")
set cmd:trace $([ "${{inputs.debug}}" == "true" ] && echo "true" || echo "false")
set ftp:ssl-protect-data true
set ftp:sync-mode false
set log:enabled/xfer true
set log:file/xfer ~/transfer_log.txt
set log:show-time/xfer false
set mirror:overwrite true
set mirror:parallel-transfer-count 3
set mirror:set-permissions false
set net:max-retries 1
set net:persist-retries 0
set net:timeout 10
set sftp:auto-confirm true
set ssl:check-hostname false
set ssl:verify-certificate false
set xfer:parallel 3
${{inputs.ftp-options}}" > ~/.lftprc
if [ "${{inputs.remote-protocol}}" == "sftp" ] && [ "${{inputs.ssh-private-key}}" != "" ]; then
echo "set sftp:connect-program /usr/bin/ssh -a -x -i ~/ssh_private_key ${{inputs.ssh-options}}" >> ~/.lftprc
else
echo "set sftp:connect-program /usr/bin/ssh -a -x ${{inputs.ssh-options}}" >> ~/.lftprc
fi
echo "open ${{inputs.remote-protocol}}://${{inputs.remote-user}}@${{inputs.remote-host}}:${{inputs.remote-port}}" >> ~/.lftprc
echo "File created: ~/.lftprc"
[ "${{inputs.debug}}" == "true" ] && cat ~/.lftprc
echo "::endgroup::"
if [ "${input_proxy}" == "true" ]; then
echo "::group::Setup proxy"
if [ "${{inputs.proxy-user}}" != "" ] && [ "${{inputs.proxy-host}}" != "" ]; then
if ssh -A -D ${{inputs.proxy-forwarding-port}} -f -N -p ${{inputs.proxy-port}} -i ~/proxy_private_key ${{inputs.proxy-user}}@${{inputs.proxy-host}}; then
echo "Proxy connected" && show_hr && echo "Proxy IP address: $(${proxy_cmd} curl --max-time 10 --show-error --silent "http://checkip.amazonaws.com/")"
else
echo "::error::Proxy connection failed."
exit 1
fi
else
input_proxy="false"
echo "::warning::Invalid input 'proxy-user', 'proxy-host'. Skipping proxy connection."
fi
echo "::endgroup::"
fi
echo "::group::Prepare files"
echo "Event: ${{gitea.event_name}}
Revision : https://gitea.com/${{gitea.repository}}/commit/${{gitea.sha}}
Committer : ${{gitea.actor}}
Message : ${{gitea.event.head_commit.message}}" && show_hr
echo "${{gitea.sha}}" > "${local_path_slash}.deploy-revision" && echo "File created: ${local_path_slash}.deploy-revision" && cat "${local_path_slash}.deploy-revision" && show_hr
if [ "${input_sync}" == "delta" ]; then
touch ~/files_to_upload ~/files_to_delete
git_depth=$(git rev-list --count --all)
git_previous_commit=""
if [ "${git_depth}" -gt 1 ]; then
if [ "${{gitea.event_name}}" == "push" ]; then
git_previous_commit=${{gitea.event.before}}
elif [ "${{gitea.event_name}}" == "pull_request" ]; then
git_previous_commit=${{gitea.event.pull_request.base.sha}}
elif [ "${{gitea.event_name}}" == "workflow_dispatch" ]; then
git_previous_commit=$(git rev-parse ${{gitea.sha}}^)
else
echo "::error::Event not supported for delta synchronization: ${{gitea.event_name}}. Supported events are 'push', 'pull_request' and 'workflow_dispatch'."
exit 1
fi
else
echo "::error::Commit history not found for delta synchronization. Please ensure you have 'fetch-depth: 0' option in checkout action. Please ignore if this is an initial commit or newly created branch."
exit 1
fi
2025-02-19 13:55:07 +01:00
domain="code.klank.school"
2025-02-19 13:29:32 +01:00
echo "Previous Revision: https://${domain}/${{gitea.repository}}/commit/${git_previous_commit}" && show_hr
# ${proxy_cmd} lftp -c "set log:enabled/xfer false; get -O ~ \"${remote_path_slash}.deploy-revision\"; exit 0"
# echo -n "Remote Revision: " && [ -f ~/.deploy-revision ] && cat ~/.deploy-revision || echo ""
# show_hr
if git cat-file -t ${git_previous_commit} &>/dev/null; then
git diff --diff-filter=ACMRT --name-only ${git_previous_commit}..${{gitea.sha}} -- ${local_path_unslash} ':!/.git*' ${{inputs.sync-delta-excludes}} > ~/files_to_upload
git diff-tree --diff-filter=D --name-only -t ${git_previous_commit}..${{gitea.sha}} -- ${local_path_unslash} ':!/.git*' ${{inputs.sync-delta-excludes}} > ~/files_to_delete
sed --in-place --regexp-extended "s#(.*)#realpath --canonicalize-missing --relative-to=$local_path_unslash \1#e" ~/files_to_upload
sed --in-place --regexp-extended "s#(.*)#realpath --canonicalize-missing --relative-to=$local_path_unslash \1#e" ~/files_to_delete
echo "File created: ~/files_to_upload" && cat ~/files_to_upload && show_hr
echo "File created: ~/files_to_delete" && cat ~/files_to_delete && show_hr
if [ "${{inputs.artifacts}}" == "true" ]; then
echo "Copy transfer artifacts" && mkdir ~/transfer_files && rsync --verbose --files-from=$HOME/files_to_upload ${local_path_slash} ~/transfer_files/
fi
else
echo "::warning::Invalid base commit for delta synchronization: ${git_previous_commit}. Please ignore if this is an initial commit or newly created branch."
fi
fi
echo "::endgroup::"
echo "::group::Transfer files"
echo "Protocol: ${{inputs.remote-protocol}}
Synchronization : ${input_sync}
Local path : ${local_path_unslash}
Remote path : ${remote_path_unslash}"
[ "${input_sync}" == "delta" ] && echo -e "Upload files: $(wc --lines < ~/files_to_upload)\nDelete files: $(wc --lines < ~/files_to_delete)"
show_hr
touch "${local_path_slash}.deploy-running"
if [ "${input_sync}" == "full" ]; then
${proxy_cmd} lftp -c "put -O \"${remote_path_unslash}\" \"${local_path_slash}.deploy-running\";
mirror --exclude-glob=.git*/ --max-errors=10 --reverse ${{inputs.ftp-mirror-options}} ${local_path_unslash} ${remote_path_unslash};
rm -f \"${remote_path_slash}.deploy-running\";
${{inputs.ftp-post-sync-commands}}"
else
${proxy_cmd} lftp -c "lcd \"${local_path_unslash}\";
put -O \"${remote_path_unslash}\" \"${local_path_slash}.deploy-running\";
mput -d -O \"${remote_path_unslash}\" .deploy-revision $(awk '{ printf "\"%s\" ", $0 }' ~/files_to_upload);
rm -f \"${remote_path_slash}.deploy-check\" $(awk -v REMOTEPATH=\"${remote_path_slash}\" '{ printf "\"%s%s\" ", REMOTEPATH, $0 }' ~/files_to_delete);
rm -f \"${remote_path_slash}.deploy-running\";
${{inputs.ftp-post-sync-commands}}"
fi
[ -f ~/transfer_log.txt ] && cat ~/transfer_log.txt
echo "::endgroup::"
echo "::group::Cleanup"
[ "${input_proxy}" == "true" ] && sudo pkill ssh
rm --force --verbose ~/.netrc ~/proxy_private_key ~/ssh_private_key
[ "${{inputs.artifacts}}" != "true" ] && rm --force --verbose ~/transfer_log.txt
[ "${{inputs.webhook}}" != "" ] && echo "Webhook notification (finish): $(send_webhook "finish")"
echo "::endgroup::"
exit 0
- name : "Upload artifacts"
uses : actions/upload-artifact@v4
with :
name : "transfer_artifacts"
path : |
~/transfer_log.txt
~/transfer_files
if-no-files-found : ignore