Suede: git-subrepo based dependency management
git-Subrepo based dependency management
That's smooth... Like suede.
— You, hopefully (after using this workflow)
A workflow that relies on git-subrepo for project dependency management.
Aims to provide the benefits of vendored dependencies with the power of git-based version control.
Not convinced? Jump down to why.
In addition to git...
- git-subrepo: Enables us to more easily include git repositories as project dependencies (as compared to git submodules and/or subtrees)
- Github Actions: Enables us to keep our remote subrepo dependency branches (
mainandrelease) up to date with each other. See more about branch structure in Anatomy of a Suede Dependency. - Bash scripts: Automates common tasks like installing dependencies, extracting repository metadata, and downloading specific folders from remote repositories without requiring a full git clone.
It is also highly recommended to use:
- devcontainers: Enables us to easily spin up a (typically linux-based) development environment that has git-subrepo installed as a feature.
| Repo | Description |
|---|---|
| pmalacho-mit/dockview-svelte-suede | svelte port of the dockview layout management library |
| pmalacho-mit/sweater-vest-suede | svelte testing library |
| pmalacho-mit/serialized-renderer-suede | pixijs-based renderer that enables data-driven (as opposed to logic-driven) rendering |
| pmalacho-mit/svelte-url-parameterizer-suede | utility to automatically track svelte runes in the URL bar |
| pmalacho-mit/with-events-suede | |
| pmalacho-mit/mixin-suede | Robust & typesafe mixin library that supports conflict resolution |
| pmalacho-mit/svelte-snippet-renderer-suede | Robust mechanism for passing renderable content as props to svelte components |
A suede dependency repository has a two-branch structure that separates development from distribution:
The main branch serves as the primary development branch where all work happens. It contains:
- Source code: All development files, tests, documentation, examples, etc.
./release/folder: Contains only the distributable code that consumers will actually use. This is the code you want others to depend on, stripped of development-only files../release/.gitrepofile: A metadata file created by git-subrepo that tracks the relationship between the./releasefolder onmainand thereleasebranch. It contains:- The remote repository URL
- The branch name (
release) - The commit hash from the
releasebranch that the./releasefolder currently reflects - Parent commit information for tracking history
When you push changes to main, the subrepo-push-release GitHub Action automatically syncs the contents of ./release/ to the release branch.
The release branch is a clean, distribution-only branch that contains:
- Only distributable code: Just the files from the
./release/folder onmain .gitrepofile: Tracks the subrepo metadata for consumers who install this dependency
This branch is what consumers actually install. It's kept automatically synchronized with ./release/ on main via GitHub Actions, ensuring that the distributed code is always up-to-date.
Key principle: Never commit directly to the release branch. All changes should flow from main → release automatically, except for external contributions pushed via git subrepo push, which trigger a PR back to main for review. See more in Maintaining a Dependency.
To consume a dependency, make use of ./scripts/install-release.sh and specify the --repo flag (or -r shortahand) in the form <repo owner>/<repo name> (e.g., pmalacho-mit/suede).
bash <(curl https://suede.sh/install-release) --repo <owner/name> Note
The above leverages curl, process substition (bash <(...)), and our suede.sh script proxy to download and execute the install script in a single, concise line.
See alternative to using suede.sh script proxy
bash <(curl https://raw.githubusercontent.com/pmalacho-mit/suede/refs/heads/main/scripts/install-release.sh) --repo <owner/name> The install script will inspect the ./release/.gitrepo file of the dependency's main branch to determine the appropriate commit of its release branch to install (see more in Anatomy of a Suede Dependency).
It then will extract the release branch's content to a folder named the same as the dendency's repository (or use the --dest flag / -d shorthand to install the depenency to a different location).
If the installed dependency depends on other resources (e.g., an npm package or another suede dependency), install instructions will be printed to the console (see more in Dependencies of Dependencies).
Finally, git add & git commit the new files.
You then have the dependency's source code vendored into your repository. You can modify and track changes to it the same as any other code in your repository and only need to amend your typical development workflow when you want to:
To get the latest changes for your dependency, first confirm that your environment has the git subrepo command available. If not, see instructions on installing git-subrepo.
git subrepo --versionThen, simply run the git subrepo pull command, with the final argument being the location of your dependency.
git subrepo pull <path-to-dependency>For example:
git subrepo pull ./my-dependency
This will fetch and merge the newest commits from the dependency’s release branch into your subrepo folder.
One of the advantages of this workflow is that you can treat your dependency's code as if it were your own source code. If you need to modify the dependency (e.g., fix a bug or add a feature), you can edit the depdency's files directly and test those changes in the context of your project. All such changes will be tracked in your main project's history.
If you then want to make those changes available to all consumers of the dependency (and you have permissions to push to its repository), you can simply run the git subrepo push command, with the final argument being the location of your dependency.
git subrepo push <path-to-dependency>For example:
git subrepo push ./my-dependency
This will push your local changes to the dependency's remote release branch, which will trigger the subrepo-pull-into-main action and the following things will happen:
- Immediate revert: Your changs will immediately be reverted on the remote
releasebranch so that any consumer who performs the upgrading instructions won't receive unvetted changes.
Warning
Because your changes are immediately reverted, avoid performing a git subrepo pull (i.e., upgrading) until your changes are approved and incorporated. Otherwise, you risk "stomping" over your changes.
- Pull request into main: A pull request is created into
mainthat applies your changes to the./release/folder (and are seen by git-subrepo as happening "on top" of the revert commit in step 1). That way, your changes can be reviewed and tested. See more in maintaing a dependency.- When that PR is approved, your changes will flow back into the
releasebranch via the subrepo-push-release action.
- When that PR is approved, your changes will flow back into the
Follow the below steps when setting up a codebase that will behave as a dependency for one or more "consumer" projects.
- Create the repository from the template. Start by creating a new repository using the suede-dependency-template as a template (select Use this template ▼ > Create a new repository).
Important
On the next screen, you must toggle on Include all branches. This ensures that you get both the main and release branches from the template.
- Follow the setup steps in your repository's README. Once your repository is created from the template, its
README.mdwill instruct you on next steps, which include:- Enabling certain Github Action workflow permissions
- Dispatching the initialization workflow
- Share your dependency. Once you complete the setup steps, your repository can now be distributed as a suede dependency. The initialization workflow will automatically update your repo's
README.mdto instruct users on how to install your dependency, which will follow the format:bash <(curl https://suede.sh/install-release) --repo owner/name
After your dependency repository is set up, you can maintain and develop it as you would any other project, with a few conventions:
- Use the
mainbranch for all development. Treat themainbranch as the primary development branch where you add features, fix bugs, and iterate on the code. You can freely edit files on main, commit changes, and create sub-branches for feature development as needed. - Keep distributable code in the
./releasefolder. Only the code intended to be consumed by other projects should go in the./releasedirectory onmain. This folder will mirror the content of the release branch. Do not put other files (tests, examples, docs, etc.) inside./release. - Automatic syncing to the
releasebranch. Whenever you push changes tomain, the subrepo-push-release Github Action will automatically update thereleasebranch to match the latest state of the code in your./releasefolder. If all goes well, thereleasebranch will always contain the up-to-date distributable code after any changes onmain.
Note
The subrepo-push-release action will also update the reference in your ./release/.gitrepo file on main to point to the new commit on the release branch. Therefore, you will need to pull from main before pushing further changes.
- Avoid direct commits to the
releasebranch. Under normal circumstances, you should not need to work on thereleasebranch directly. All changes should flow frommain→releasevia the automated workflow. The only time you'd interact with release manually is if something went wrong and you need to fix merge conflicts (which should be rare). - Handle external contributions via PRs. As mentioned above in the Modifying section, users that consume your dependency can also push changes to its
releasebranch viagit subrepo push ...(assuming they have write access to your repository). This will trigger the subrepo-pull-into-main action which will create a pull request to update the content of the./releasefolder onmainbased on their commit to thereleasebranch (which will be immediately be reverted, to preserve the state of the remotereleasebranch). As a maintainer, you should review these PRs and merge them after appropriate testing. This way, contributions from others get incorporated into yourmainbranch (the source of truth) in a controlled manner, and then flow back intorelease.
In summary, do your day-to-day development on main (or a sub-branch), keep the ./release folder up-to-date with the code you want to distribute, and let the automation handle syncing that code to the release branch.
If your suede dependency relies on other libraries or modules, the subrepo-push-release Github Action will automatically capture these dependencies with the following conventions:
- A
./package.jsonfile at the root of themainbranch will have it's"dependencies"object copied to./release/.dependencies/package.json- Therefore, npm packages should be included in
"dependencies"if and only if they are required by yourreleasecode. All other dependencies should be installed as"devDependencies".
- Therefore, npm packages should be included in
- Any folders at the root of the
mainbranch that contain a.gitrepofile (indicating it's a subrepo) will have the.gitrepocontents copied to./release/.dependencies/<folder-name>.gitrepo(e.g., if yourmainbranch included./some-dependency/.gitrepo, the subrepo-push-release action will copy it's contents to./release/.dependencies/some-dependency.gitrepo)- Therefore, suede dependencies should only be installed in the root of your
mainbranch if and only if they are required by yourreleasecode. If instead you'remainbranch depends on other suede dependencies (say for testing or documentation), simply install those into a subfolder.
- Therefore, suede dependencies should only be installed in the root of your
Important
The ./release/.dependencies folder is maintained by the automation and you shouldn't need to edit it by hand.
These dependencies are then analyzed upon install and instructions for resolving them are printed to the terminal.
This manual step for consumers (to review and install sub-dependencies) is seen as a feature: it promotes awareness of exactly what your project is using under the hood, rather than nesting hidden dependencies. It ensures that nothing gets added to a consumer's project without them explicitly opting in.
See sample post-install instructions
NEXT STEPS:
The installed gitrepo has the following npm dependencies:
"dependencies": {
"@storybook/test": "^8.6.14",
"html-to-image": "^1.11.13",
"svelte": "^5.41.0",
"typescript": "^5.9.3"
}
Add these to your project's package.json and run 'npm install' or install them with the following command:
npm install @storybook/test@^8.6.14 html-to-image@^1.11.13 svelte@^5.41.0 typescript@^5.9.3
Install nested suede dependencies:
bash <(curl https://suede.sh/install-gitrepo) -d sweater-vest-suede/../dockview-svelte-suede sweater-vest-suede/.dependencies/dockview-svelte-suede.gitrepo
Commit the changes to your repository:
git add sweater-vest-suede package.json sweater-vest-suede/../dockview-svelte-suede
git commit -m 'Add suede dependency (release) pmalacho-mit/sweater-vest-suede@86abeeba0ba123167ae6f04639fc75b1508d59dc'To see the depenencies of an already installed dependency, run bash (< https://suede.sh/extract-dependencies) and provide the dependencies location as a positional argument, for example:
bash (< https://suede.sh/extract-dependencies) ./example-depepdency/See sample dependency
SUEDE DEPENDENCIES OF DEPENDENCY:
The installed gitrepo has the following npm dependencies:
"dependencies": {
"@storybook/test": "^8.6.14",
"html-to-image": "^1.11.13",
"svelte": "^5.41.0",
"typescript": "^5.9.3"
}
Add these to your project's package.json and run 'npm install' or install them with the following command:
npm install @storybook/test@^8.6.14 html-to-image@^1.11.13 svelte@^5.41.0 typescript@^5.9.3
Install nested suede dependencies:
bash <(curl https://suede.sh/install-gitrepo) -d sweater-vest-suede/../dockview-svelte-suede sweater-vest-suede/.dependencies/dockview-svelte-suede.gitrepo... todo: ...
... essentially: (1) copy ./.github/workflow/subrepo-push-release.yml of main branch of template to main branch, (2) create release branch as orphan, delete everything, copy over ./.github/workflow/subrepo-pull-into-main.yml and ./.gitingore from release branch of template, (3) on main, do a git subrepo clone of the release branch into the release folder ...
suede.sh is a Cloudflare Worker that provides cached, convenient access to the scripts in this repository. It serves as a proxy to the GitHub raw content URLs, with two key benefits:
- Simplified URLs: Instead of typing the full GitHub raw content URL, you can use shorter URLs like
https://suede.sh/install-release - Optional file extensions: The
.shextension can be omitted from requests (e.g.,https://suede.sh/utils/degitinstead ofhttps://suede.sh/utils/degit.sh) - Caching: Responses are cached via Cloudflare's CDN for faster access
Note
suede.sh is not utilized in any ./scripts or Github Action workflows, and instead the GitHub raw content URLs are used instead.
Throughout this documentation, you'll see commands like:
bash <(curl https://suede.sh/<script-name>)This is equivalent to:
bash <(curl https://raw.githubusercontent.com/pmalacho-mit/suede/refs/heads/main/scripts/<script-name>.sh)If you have concerns about executing scripts through a third-party proxy, you can always use the direct GitHub raw content URLs instead. Both approaches fetch the same script content, but the GitHub URL bypasses the suede.sh proxy entirely.
For example, replace:
curl https://suede.sh/install-releaseWith:
curl https://raw.githubusercontent.com/pmalacho-mit/suede/refs/heads/main/scripts/install-release.shThe source code for the suede.sh worker is available at github.com/pmalacho-mit/suede-cloudflare-worker for review.
Install git-subrepo
Use a devcontainer with a .devcontainer/devcontainer.json file that includes git-subrepo as a feature.
If you haven't worked with devcontainers before, checkout this tutorial.
Copy the contents of this file to .devcontainer/devcontainer.json or create your repository using git-subrepo-devcontainer-template as a template repository by selecting Use this template ▼ > Create a new repository
Install git subrepo on your system according to their installation instructions.
Managing dependencies for code you control presents unique challenges that traditional package managers aren't designed to solve. Suede addresses these challenges by combining the benefits of vendored dependencies with the power of git-based version control.
Package Managers (npm, pip, etc.): While package managers serve a purpose for stable, third-party dependencies from trusted sources, they're poorly suited for code you control and actively develop (and are increasingly becoming a liability due to supply chain attacks).
- Opaque dependencies: Most packages deliver pre-built, minified code that's difficult to inspect or understand. You have to trust (and reason about) black-box code in your project.
- Supply chain vulnerabilities: The centralized registry model creates attack vectors, which seem to be exploited more and more.
- Development friction: The publish-test-fix-republish cycle adds significant overhead when you're actively maintaining a dependency and need to iterate quickly.
- Version coordination: Maintaining perfect version alignment across multiple related projects or a monorepo requires constant attention and manual updates. The technologies developed to support these usecases (especially monorepos) are complex pieces of software, which require their own learning and maintenance.
Git Submodules seem like the natural solution for code you control, but they introduce their own problems:
- State mismatches: It's easy to push code that depends on submodule changes without also pushing and updating those submodule references, leading to broken builds for other developers.
- Branch complexity: Feature development often requires creating matching branches in both the parent repo and submodule(s), whuch then require carefully coordinating merges.
- Checkout friction: New contributors must remember to run
git submodule update --init --recursive, and the submodules don't automatically update when switching branches. - Detached HEAD states: Submodules frequently end up in detached HEAD state, confusing developers who aren't experts in git.
Git Subtrees improve on submodules by embedding dependency code directly into the parent repository, but they make bidirectional updates complex and can pollute your git history.
Suede uses git-subrepo to vendor dependency code directly into your repository while maintaining a clean bidirectional sync with the dependency's source. This gives you:
1. Simplified Development Workflow
- Edit dependency code directly in place, just like any other code in your project
- Test changes immediately in the real context where they'll be used
- All changes are tracked in your project's normal git history
- No branch coordination or submodule state management
2. Bidirectional Updates
- Pull updates from the dependency with
git subrepo pull - Push your local changes back to the dependency with
git subrepo push - Changes flow naturally in both directions without complex merge strategies
3. Complete Repository State
- Every commit in your repository contains all the code needed to build and run
- No hidden state in submodule pointers or external dependencies
git clonegives you a working repository immediately, no additional steps- Full, un-minified source code for all dependencies is present in your repo, making it easy to understand what your project depends on
4. Review Process
- Changes pushed via
git subrepo pushcan trigger pull requests for review - Maintainers can vet changes before they're merged into the dependency's
mainbranch - New installations always use the vetted version from
main
5. Clean Separation
- The two-branch structure keeps development artifacts (tests, examples, docs) separate from distributed code
- Consumers only get what they need, not your entire development environment
- Maintainers work on
mainas usual; automation handles distribution
Suede tries to get the best of both worlds: vendored dependencies (complete repository state, no external coordination) with source control and bidirectional updates (version tracking, easy syncing, git-based workflows).
... work in progress...
- Use symlinks or folder references: If your build or runtime expects dependencies in a certain location (e.g., a libs directory or within node_modules), you can create a symlink from that expected location to the ./my-dependency folder. This way, your project can import/require the dependency as if it were installed normally.
[!TIP] Make sure the location of your symlink is not
.gitignore'd. - Use path aliases (for languages like TypeScript): Many build systems or language toolchains allow you to define alias paths for imports. For example, in a TypeScript project, you could configure tsconfig.json to map an import like
"my-dependency/*"to your local./my-dependency/*(or whichever subdirectory contains the code). This allows you to import the dependency in code using a clean module name, while actually resolving to your vendored subrepo code.

