Open-source project
SamKirkland/FTP-Deploy-Action avatar
SamKirkland/FTP-Deploy-Action

FTP-Deploy-Action defaults to an unencrypted login and a port 21 guess

Deploys a GitHub project to a FTP server using GitHub actions

5,247 stars444 forksTypeScriptMIT

At a glance

What is it?
A TypeScript GitHub Action that syncs a checkout to an FTP server on push, packaged as a committed bundle so a workflow needs nothing installed. It is a small, readable action whose two riskiest defaults are the protocol, which is cleartext until you change it, and the assumption that your host still speaks FTP at all.
Who is it for?
Adopt FTP-Deploy-Action when your host still hands out FTP credentials and you want a push to sync files without installing anything on the server. Leave it if the connection is unencrypted by default and your host offers SSH only, because the project itself points you at a separate action for that case.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 167 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.

Editorial analysis

There is nothing to install, the yml is the whole install

This action ships as a reference inside a workflow file, not as a package you add to a project. The example points at `/.github/workflows/main.yml`, and the trigger is a bare `on: push` with no branch filter, running on `ubuntu-latest` after a checkout:

yml
on: push
runs-on: ubuntu-latest

The deploy step itself is four keys:

yml
uses: SamKirkland/[email protected]
with:
  server: ftp.samkirkland.com
  username: myFtpUserName
  password: ${{ secrets.ftp_password }}

Because the trigger has no branch filter, every push on any branch syncs, and the checkout step at `actions/checkout@v6` decides what ends up on the server. That is fine for a static site whose whole repository is the site. For a repository that also holds source, tests and workflow files, the trigger and the sync directory both need narrowing first.

protocol defaults to ftp, which is the option with no encryption

The `protocol` setting has three accepted values and one default. `ftp` is the default and is described as providing no encryption. `ftps` is full encryption on the newest standard, also called explicit ftps. `ftps-legacy` is full encryption on the legacy standard, also called implicit ftps. A workflow that only fills in `server`, `username` and `password` therefore sends the credential and the file contents in clear.

The pairing of these two settings is where hosts get awkward. The `port` default is 21 and the example value in the settings table is 990, which belongs to the implicit form. Explicit `ftps` runs on the control connection and upgrades after login, while implicit `ftps` negotiates encryption before the login itself, so a host that only speaks one of them will fail on the other. The action cannot negotiate both for you: you pick the protocol and the port, and the README sends you to your host's documentation to confirm which pair is right.

Only the password belongs in secrets, the host and user stay in the file

Three settings are required, `server`, `username` and `password`, and only the last one is expected to come from the secrets store. The README is explicit about it: store `password` as a secret, and remember to escape quotes and spaces in the value. The seven setup steps end with adding a secret named `password` under the project Settings tab, then editing the workflow so the key reads `${{ secrets.ftp_password }}`.

That leaves the destination hostname and the FTP user sitting in plaintext in a file committed to the repository. That is a smaller leak than a password, and the username format in the settings table even shows an email style value, `[email protected]`, so it can identify a person as well as an account. If your provider supports per deployment credentials, note that this action has no key for choosing one: there is a single `username` and a single `password`, so rotating means editing the secret and rerunning.

A host that wants SSH means this action is the wrong tool

The requirements section sets the boundary in one line: you must have FTP access to your server, and if your host allows or requires SSH you should use the author's separate web-deploy action instead. That is a routing decision made before you write a line of workflow, not a fallback after a failure.

The reason is structural. This action speaks FTP and its bundled dependency is the FTP client, so there is no transport negotiation and no way to reach an SFTP endpoint. Hosts that have retired anonymous FTP, that hand out SFTP credentials only, or that expose SSH on a nonstandard port are all outside it. The same requirements section warns that some web hosts change the default port, which is another way of saying the target environment has moved on from the defaults. Check what your host actually offers before writing the `with:` block.

dist/index.js is committed, so dependency fixes wait for a tag

The manifest points `main` at `dist/index.js`, the build script is `ncc build src/main.ts --no-cache`, and `dist/` sits at the repository root next to `action.yml`. The action runtime is therefore a compiled bundle committed to the repository, which is exactly why a workflow needs no npm step.

The cost is the update path. The FTP client itself is a dependency, `@samkirkland/ftp-deploy`, and it arrives inside that bundle. A fix to it does not reach you when it merges; it reaches you when a new tag is cut, because the `uses:` line names a tag such as `@v4.4.0`. The release history explains the pacing: v4.3.5 on 2024-03-02, v4.3.6 on 2025-08-26, v4.4.0 on 2026-04-23, with the last push to the repository dated 2026-04-23. Two of those three releases are a patch train, and the year between the first two is long enough that a security advisory in the FTP client could sit unnoticed.

migration.md is the only note on what a major version changes

Upgrading past a major version is handled by one file: `migration.md` at the repository root. There is no changelog, and the release entries themselves carry nothing beyond a rocket emoji and the tag string, so the migration file is where you find out whether a key was renamed or a default moved.

That matters most for the two settings where a silent default change would be invisible in a diff. A workflow written against `ftp` and `port: 21` looks identical after an upgrade even if the defaults behind it changed, and the action would then behave differently without your file changing at all. The settings table is the other thing worth keeping a copy of, since it is the only place the accepted values for `protocol` and the meaning of each key are written down. The lint and test wiring is standard, `jest` with a `ts-jest` preset and `eslint src/**/*.ts`, and neither affects the deployed artifact.

local-dir defaults to the repository root, so the workflow file ships too

The one directory setting shown has an example of `./myFolderToPublish/` and a default of `./`, the repository root. Left alone, the sync publishes whatever the checkout produced, which includes `.github/` and any other file in the repository alongside the site.

Combined with the unfiltered `on: push` trigger, that gives you two ways to publish something you did not mean to. A branch push builds and uploads content from a branch that was never meant for production, and a dotfile added for local reasons goes up with it. Neither is a bug in the action; both are the shape of the default. The workflow example shows no `branches:` key and no `local-dir` key at all, so the two settings that would constrain a deployment are exactly the two the sample leaves out.

Editorial conclusion

Adopt FTP-Deploy-Action when your host still hands out FTP credentials and you want a push to sync files without installing anything on the server. Leave it if the connection is unencrypted by default and your host offers SSH only, because the project itself points you at a separate action for that case. Before the first run, set `protocol` to `ftps` or `ftps-legacy` rather than accepting the `ftp` default, confirm `port` with your host since 21 is a convention rather than a fact, and pin `uses:` to a tag you have read `migration.md` for.

Frequently asked questions

How do I use FTP-Deploy-Action in a GitHub Actions workflow?

Reference it as `uses: SamKirkland/[email protected]` inside a workflow file at `/.github/workflows/main.yml` and supply the required `server`, `username` and `password` keys. Nothing is installed on the runner, because the repository commits its own bundle at dist/index.js.

Does FTP-Deploy-Action encrypt the FTP connection?

Not by default. The `protocol` setting defaults to `ftp`, which the settings table describes as providing no encryption. Set it to `ftps` for the explicit form or `ftps-legacy` for the implicit form, and check the port, since it defaults to 21 and the table gives 990 as an example.

Can FTP-Deploy-Action deploy over SSH or SFTP?

No. Its requirement is FTP access to your server, and the README points you to the author's separate web-deploy action when your host allows or requires SSH. The action has no transport negotiation, so an SFTP-only host is outside what it can reach.

Which FTP port does FTP-Deploy-Action connect to?

The `port` setting defaults to 21, and the settings table gives 990 as an example value. The README notes that some web hosts change the default port and tells you to check your host's documentation before relying on either number.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. SamKirkland/FTP-Deploy-Action on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/samkirkland-ftp-deploy-action.svg)](https://hysenlabs.com/projects/samkirkland-ftp-deploy-action)