Added
-
Server-side concurrency lock to serialize concurrent
deployments (closes the risk fromPROPOSAL.md§5 #6: "two
simultaneous workflows to the same FTP can corrupt each
other"). The default is OFF to preserve the v2.7.0
behaviour bit-for-bit. Opt in with the new input
concurrency_lock: "true".- uses: airvzxf/ftp-deployment-action@v2 with: server: ${{ secrets.FTP_SERVER }} user: ${{ secrets.FTP_USERNAME }} password: ${{ secrets.FTP_PASSWORD }} concurrency_lock: "true"
Mechanism. Before the mirror, lftp issues
quote MKD <path>to create a sentinel directory on the FTP server.
RFC 959MKDandRMDare implemented by every FTP
server (vsftpd, proftpd, Pure-FTPd, sftp-via-FTP-gateways,
etc.) andmkdir(2)is atomic on virtually every UNIX-like
filesystem (returningEEXISTif the directory already
exists). The race window between two clients is
microseconds, and the worst-case outcome — the lock
briefly staying held by a dead runner — is caught by the
configurableconcurrency_lock_timeout. We chose
MKD/RMDbecause lftp has no built-in server-side
lockcommand (it only hasfile:use-lockfor local
files, which we verified against the lftp 4.9.3 source
and thesrc/commands.ccstatic command table).Inputs.
concurrency_lock(defaultfalse) — opt-in switch.concurrency_lock_path(default.lftp-deployment.lock)
— sentinel directory; validated with the same
validate_pathrules aslocal_dir/remote_dir
(rejects.., leading dash, control chars, and shell
metacharacters).concurrency_lock_timeout(default300) — maximum
seconds to wait for the lock when another run is
currently holding it.0means "fail immediately when
held" (no polling).concurrency_lock_poll_interval(default5) — seconds
betweenquote MKDattempts. Rejected when0(would
cause a division-by-zero in the iteration count).
Release. The lock is released in three layered ways:
(1) an inlinequote RMD <path>in the lftp-escript
right beforequit;; (2) a fallbackrun_lftp_lock_release
call in the EXIT trap (so a signal-killed lftp still
releases the lock, using the .netrc that is also about to
be removed); (3) the documentation in the README explains
the manualquote RMDrecovery path for the rare case of
a stale lock (holder died before any of the above could
run). Whenconcurrency_lockisfalse(the default),
run_lftp_lock_releaseshort-circuits on an empty lock
path and never invokes lftp, so the no-op path is bit-for-
bit identical to v2.7.0.When to use it. For the common case of a single
workflow deploying to one FTP, the GitHub Actions
concurrency:block is the recommended approach
(documented as Option A in the new README section "Concurrency /
deployment lock") because it works across runners and
regions, requires no extra inputs, and is platform-managed.
Theconcurrency_lockinput is the fallback for users who
cannot add aconcurrency:block (e.g. multiple distinct
workflows pointing to the same FTP, or deploys driven by a
tool the user does not own).
Internal
lib.sh: new functionsbuild_lock_acquire_script,
build_lock_release_script, andrun_lftp_lock_release.
All three read the new inputs via_indirection(the
project's single point of dynamic variable-name lookup);
no secondevalsite is introduced.lib.sh:run_lftp_oncenow takes two extra parameters
(lock acquire / release fragments). When the lock is
disabled, both are empty strings, so the composed
lftp-escript is bit-for-bit identical to v2.7.0.lib.sh:print_inputs_dumpnow lists the four new
inputs in both debug and non-debug modes.entrypoint.sh: the new inputs are defaulted to their
action.ymldefaults;validate_path/
validate_intare run only when the lock is enabled,
to keep the validation surface tight for the common
case (concurrency_lock=false). The EXIT trap is
extended to callrun_lftp_lock_releasebefore
removing the .netrc (so lftp can still authenticate
the release call).- Tests: 11 new bats unit tests in
tests/unit/lock.bats
cover the empty/non-empty branches, thetimeout=0
short-circuit, theceil(timeout/poll)iteration count
for both 300/5 and 300/7, the lock path verbatim pass-
through, and the no-op paths ofrun_lftp_lock_release.
3 new smoke tests intests/smoke.shverify the
default-off path emits no lock fragments in the lftp
script, and that..inconcurrency_lock_pathand
0inconcurrency_lock_poll_intervalare both
rejected with exit 2. tests/contract.shwas unchanged: it auto-discovers the
four new inputs from the newlib.sh::print_inputs_dump
list and from the staticINPUT_*references in
entrypoint.sh/lib.sh, and matches them against
action.yml(now 31 declared inputs).- Total test count: 118 unit + 33 smoke + 1 contract + 3
release-smoke = 155 tests, all green.