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 -p ~/.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" domain="code.klank.school" echo "Event: ${{gitea.event_name}} Revision: https://${domain}/${{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 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