The other day I had issues with my FTP server. The ftp action that I was using didn’t catch onto the error where the server was unable to establish a separate channel for transferring the directory information. I decided to change the FTP Action from ftp-action to FTP-Deploy-Action.
| Library | ftp-action | FTP-Deploy-Action |
|---|---|---|
| Version | releases/v2 | v4.3.5 |
| Author | sebastianpopp | SamKirkland |
| Full Version | sebastianpopp/ftp-action@releases/v2 | SamKirkland/FTP-Deploy-Action@v4.3.5 |
| SFTP | Yes | Yes |
| Delete all option before upload | Yes | Yes |
| Dry Run | No | Yes |
| Exclude Glob | No | Yes |
| Timeout Option | No | Yes |
| State | No | Yes |
| Issues | local .env file causes issuesHangs on server errors | Fails without .ftp-deploy-sync-state.json |
The action was a little difficult to setup at first. It turns out that you must add a .ftp-deploy-sync-state.json file before it will upload files. This allows it to hash all of the files and determine what has changed. Rather than uploading everything, it only uploads the changed files based on the current file state. This is great when using the composer dependency manager for php as the vendor folder doesn’t change unless you are adding or removing a package. Unfortunately, the documentation isn’t clear that this is required. Here is my YAML in how to use it:
- name: Prepare FTP
run: touch src/.ftp-deploy-sync-state.json
- name: Deploy via FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ vars.DEV_FTP_HOST }}
username: ${{ secrets.DEV_FTP_USER }}
password: ${{ secrets.DEV_FTP_PASSWORD }}
local-dir: 'src/'
timeout: 2000
You’ll notice that all I am doing is creating a file if it doesn’t exist. It doesn’t have any content.
The FTP state file is just a JSON file listing folders, file names and hashes
{
"description": "DO NOT DELETE THIS FILE. This file is used to keep track of which files have been synced in the most recent deployment. If you delete this file a resync will need to be done (which can take a while) - read more: https://github.com/SamKirkland/FTP-Deploy-Action",
"version": "1.0.0",
"generatedTime": 1717356129387,
"data": [
{
"type": "file",
"name": ".ftp-deploy-sync-state.json",
"size": 0,
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"type": "file",
"name": ".htaccess",
"size": 1351,
"hash": "af335f19862bccf6b8d76beb03ffdeb6e29b109de0872d0e246af2c2f6708e0c"
},
{
"type": "folder",
"name": "common"
}
}
The other hiccup was that it didn’t recognize src as a folder until I added a trailing slash. Small potatoes.
One thing I love about it is the timeout. Given that I want to fail fast, I prefer low timeouts as anything longer often indicates a problem and just eats up minutes of limited build time I have available.
I had to increase my job timeout to 3 minutes given how long it takes to upload everything (5 MB) during the first run without a state synchronization file on the server. The fact that the FTP action has its own timeout in milliseconds gives me much comfort that things are not going to get hung up for too long if a problem arises.
Another thing I did was to block access to
- Any file beginning with a dot
- Vendors folder
- Non-php extensions
Since the vendors folder is created by composer during the build, I had to add a step to create an .htaccess file during the build before the files are uploaded via FTP.
- name: Block HTTP access to vendor
run: |
touch src/vendor/.htaccess
echo "Deny from all" >> src/vendor/.htaccess

Forbidden
As for all other files, I was able to modify the root .htaccess file under source control.
# Prevent accessing any file beginning wit a dot
<FilesMatch "^\.">
Order allow,deny
Deny from all
</FilesMatch>
# Block direct access to files that do not have a .php extension
<FilesMatch "\.(?!php)[a-z0-9]+$">
Require all denied
</FilesMatch>
If I need to add files like robots.txt later, I’ll have to update the file accordingly. The API server generally doesn’t have anything worthy for a search engine to index as most endpoints require posted content resulting in an error.
{
"error":"Method not allowed."
}
FTP-Deploy-Action is much more verbose. The original action seemed to be a quick-and-dirty method of simply allowing you to deploy via FTP. This other version seems to be focused on build servers optimizing for speed and verbosity. The logs let you know when the last deployment occurred, the number of files, what is new, what is being replaced, and what is being ignored.
---------------------------------------------------------------- 🚀 Thanks for using ftp-deploy. Let's deploy some stuff! ---------------------------------------------------------------- If you found this project helpful, please support it by giving it a ⭐ on Github --> https://github.com/SamKirkland/FTP-Deploy-Action or add a badge 🏷️ to your projects readme --> https://github.com/SamKirkland/FTP-Deploy-Action#badge ---------------------------------------------------------------- Last published on 📅 Sunday, June 2, 2024 at 7:12 PM ---------------------------------------------------------------- Local Files: 1,391 Server Files: 1,390 ---------------------------------------------------------------- Calculating differences between client & server ---------------------------------------------------------------- 📄 Upload: vendor/.htaccess 🔁 File replace: .htaccess 🔁 File replace: push/.htaccess 🔁 File replace: vendor/composer/autoload_psr4.php 🔁 File replace: vendor/composer/autoload_static.php ⚖️ File content is the same, doing nothing: .ftp-deploy-sync-state.json ⚖️ File content is the same, doing nothing: common/base64url.php ... replacing ".htaccess" replacing "push/.htaccess" replacing "vendor/composer/autoload_psr4.php" replacing "vendor/composer/autoload_static.php" ---------------------------------------------------------------- 🎉 Sync complete. Saving current server state to "./.ftp-deploy-sync-state.json" ---------------------------------------------------------------- Time spent hashing: 426 milliseconds Time spent connecting to server: 1 second Time spent deploying: 2.1 seconds (5.5 kB/second) - changing dirs: 152 milliseconds - logging: 36 milliseconds ---------------------------------------------------------------- Total time: 4.4 seconds ----------------------------------------------------------------
