Vendoring Alternatives: a plain copy or a git submodule

Getting Started brings webform-relay in with v, which is the recommended and supported path. Nothing in taskfiles/deploy.yml actually requires it, though: every task resolves the vendored source through {{.TASKFILE_DIR}} and your project root through {{.ROOT_DIR}}, so any layout that puts the source in a directory and your .env / .env.project / webform.yaml in your project root works.

This page covers the two alternatives and what each costs you. Everything else — the dotenv split, bootstrap, deploy, rollback, teardown — is identical; follow Getting Started from step 3 onward. What differs is only how the source arrives, how it updates, and how the version is recorded.

Whichever you pick: don’t fork. A fork diverges immediately, and upstream bug fixes and IAM policy corrections never reach it short of a manual diff-and-reapply.

How it arrivesHow it updatesRecorded versionSupport
v (recommended)v add <url> <tag>v update <url> <new-tag>vendors.toml, pinned to an exact commitSupported: this is the path the tasks are designed and tested for
Plain copyrelease tarball or cp -r into your treeRe-copy and diff by handWhatever you write down yourselfWorks, but drift is on you
Git submodulegit submodule addgit submodule update --remote + commit the pointerThe submodule’s committed SHAWorks, at the cost of submodule ergonomics for everyone who clones

A plain copy

Copy the source into your project — from a release tarball, or from a shallow clone you then strip the .git from:

mkdir -p third_party/webform-relay
curl -sSL https://gitlab.com/frob/webform-relay/-/archive/<tag>/webform-relay-<tag>.tar.gz \
  | tar -xz --strip-components=1 -C third_party/webform-relay

(git archive --remote won’t work here — GitLab does not expose git-upload-archive over HTTPS.)

Then point your Taskfile at it:

version: '3'

dotenv: ['.env', '.env.project']

includes:
  relay:
    taskfile: ./third_party/webform-relay/taskfiles/deploy.yml
    dir: ./third_party/webform-relay

and copy the dotenv templates out of it:

cp third_party/webform-relay/.env.example  .env
cp third_party/webform-relay/.env.project  .env.project

Updating is re-copying the new release over the old directory and reading the diff yourself. Two things to do so that’s survivable:

  • Write the version down. Nothing in a copy records where it came from. Commit the tag and commit SHA in a README or a one-line file next to the copied tree, so a later update knows what it’s updating from.
  • Never edit the copied tree. A local edit is invisible until the next update either clobbers it or shows up as a confusing conflict. Customize through your own Taskfile instead — see If you need to customize a task.

This is the right choice if you’re air-gapped, mirroring releases into an internal artifact store, or otherwise can’t fetch from a git host at build time.


A git submodule

git submodule add https://gitlab.com/frob/webform-relay vendor/webform-relay
cd vendor/webform-relay && git checkout <tag> && cd -
git add vendor/webform-relay && git commit -m "chore: vendor webform-relay <tag>"
version: '3'

dotenv: ['.env', '.env.project']

includes:
  relay:
    taskfile: ./vendor/webform-relay/taskfiles/deploy.yml
    dir: ./vendor/webform-relay
cp vendor/webform-relay/.env.example  .env
cp vendor/webform-relay/.env.project  .env.project

The submodule’s committed SHA is the pinned version, and git submodule update --remote plus a commit of the new pointer is the update. The cost is submodule ergonomics, borne by everyone who touches the project:

  • A plain git clone of your project leaves the submodule empty, and the deploy tasks then fail with a missing-Dockerfile/missing-template.yaml error rather than anything that names the real cause. Clone with git clone --recurse-submodules, or run git submodule update --init --recursive after the fact.
  • CI must be told too. On GitLab, set GIT_SUBMODULE_STRATEGY: recursive in the job or globally; on GitHub Actions, with: submodules: recursive on actions/checkout. Without it, the deploy job fails the same opaque way.
  • The submodule sits at a detached HEAD by design. Committing inside it is possible and almost always a mistake — the commit belongs to nobody and is lost on the next update --remote.
  • v update has no equivalent guardrail here: git submodule update --remote follows the submodule’s tracked branch, so it can move you onto an unreleased commit. Pin a tag explicitly (git -C vendor/webform-relay checkout <tag>) and commit that.

What stays the same in all three

  • The two-directory split. The vendored/copied/submoduled tree is read-only source; your project root holds Taskfile.yml, .env, .env.project, and webform.yaml. See Two directories, not one.
  • dotenv: ['.env', '.env.project'] must be declared in your project’s own top-level Taskfile, never in the included file — Task refuses to load an included Taskfile that declares its own dotenv:. .env is listed first so it wins over the committed .env.project.
  • .env is gitignored; .env.project is committed. Both are copied out of the vendored source once, then maintained by you.
  • Task names, CONFIG= resolution, and every path in Getting Started behave identically — only the taskfile:/dir: pair in your includes: block changes.
  • Never edit the vendored/copied/submoduled source.
  • The limitations and known gaps apply unchanged.

If you need to customize a task

Don’t edit the vendored taskfiles/deploy.yml — it’ll be overwritten. Add a task with a different name to your own project Taskfile.yml instead, or open an issue/PR upstream if the customization is generally useful.

If you do need file-level vars: in your own Taskfile, avoid reusing a name like GO_MOUNTS, SAM_MOUNTS, AWS_ENV_FLAGS, TOOLS_IMAGE, or STACK_NAME — Task does not fully isolate an included Taskfile’s file-level vars from the includer’s when names collide, and taskfiles/deploy.yml’s own vars are all prefixed RELAY_ specifically to make that vanishingly unlikely from its side. A same-named var in your own Taskfile can still silently clobber one of Task’s built-in vars used internally (ROOT_DIR/TASKFILE_DIR themselves are safe — they’re not plain vars: entries — but don’t name your own var ROOT_DIR or TASKFILE_DIR either).