Suede: git-subrepo based dependency management
Tip
🟢 The deploy token (SUEDE_DEPENDENCY_TEMPLATE_PAT) is good for 68 more days (2026-11-08).
Last checked: 2026-08-31 (UTC).
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)
- Python 3.9+: The installer is a single dependency-free file,
scripts/suede.py. Everythinginstall,check,list,diff,removeandextractdo lives there. - 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: Thin, readable wrappers around the above — the install bootstrap, the publish flow, and the tools a consumer gets inside an installed dependency.
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
./release/.suede/.dependencies/folder: The published manifest — one.gitrepopointer per release dependency, plus apackage.jsonand/orrequirements.txtnaming the third-party packages your code needs. It is generated bysuede extractand refreshed on every publish; never edit it by hand../.suede/core/folder: The maintainer's tools and the scripts CI runs, vendored as a subrepo of this library. Update them withgit subrepo pull .suede/corerather than by editing them../release/.suede/core/folder: The consumer-facing tools (sync,upstream,diff), vendored insiderelease/so they ship with it. They live onmainlike everything else you develop, and reach thereleasebranch through it — update them withgit subrepo pull release/.suede/core, never by checkingreleaseout../.suede/.dependencies/separatorfile: One line recording your project's separator. It lives at the root and is never copied intorelease/— a consumer's separator is their own choice.
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.suede/core/: The consumer-facing tools —sync,upstreamanddiff— which ship inside the dependency and end up at<dependency>/.suede/core/in every consumer's repository.
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 flow from main → release automatically. Contributions from consumers arrive as pull requests against main, never as direct writes to release — see Maintaining a Dependency.
To consume a dependency, run the install bootstrap and specify the --repo flag in the form <repo owner>/<repo name> (e.g., pmalacho-mit/suede).
bash <(curl -fsSL https://suede.sh/install/release) --repo <owner/name>Important
Keep the -f. Without it a failed download (a 404 page, a proxy error) is
handed to bash and executed.
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 -fsSL https://raw.githubusercontent.com/pmalacho-mit/suede/refs/heads/main/scripts/install/release.sh) --repo <owner/name>The one-liner is a bootstrap: it finds a Python 3.9+, downloads
scripts/suede.py, and runs it. Nothing is installed on
your system, and git does all the network access — so private repositories,
SSH keys and credential helpers work with no extra setup.
The installer resolves the dependency's release branch, then its
dependencies, and so on, and shows you the whole plan before touching anything:
PLAN
install my-app.sweater-vest-suede @ 86abeeb (requested)
install my-app.dockview-svelte-suede @ 4f10c2a (required by sweater-vest-suede)
reuse my-app.mixin-suede @ 9bb0e41 (already present)
link sweater-vest-suede.dockview-svelte-suede -> ./my-app.dockview-svelte-suede
Proceed? [Y/n]
Everything in the closure is installed once, flat at the repo root, named
$repo.<dependency>; each dependent gets a symlink to it. Two dependents
wanting the same dependency at different commits is a conflict, and the
installer offers the resolutions rather than picking one. See
DEPENDENCIES-OF-DEPENDENCIES.md for why,
and INSTALL.md for the full algorithm.
Installs are staged, not committed — review them, then git commit, or
pass --commit.
The default installs a release dependency: prefixed, flat at the root, and recorded in your manifest so your own consumers can resolve it. Two flags pick the other kinds — see DEPENDENCIES-OF-DEPENDENCIES.md for what they are.
bash <(curl -fsSL https://suede.sh/install/release) --repo <owner/name> --devA development dependency — a test harness, a fixture, an example app.
Installed at the root under its own name with no $repo. prefix, so the
classification rule reads it as dev and extract never ships it. Its own
dependencies are installed rather than doubled as yours, and an edge that
something already installed can satisfy points there instead of being copied.
Its npm and PyPI packages go to devDependencies and requirements-dev.txt,
neither of which is published.
bash <(curl -fsSL https://suede.sh/install/release) --repo <owner/name> --vendorA vendored release dependency — the source ships with your release
branch instead of a pointer to it. It lands at release/<name>, and so does
everything it depends on: vendored code ships whole, so a sibling outside
release/ would reach your consumers as a dangling link. That holds even when
you already have the same commit installed at the root, because the root copy
does not ship. Nothing is recorded — there is no pointer to record.
Both kinds create one entry per dependency, not two. The ownership rule
above — a folder plus a link each — exists so the name in a shipped manifest is
backed by real bytes, and neither kind ships such a name. So a transitive
install simply takes the name its dependent asks for
(sweater-vest-suede.dockview-svelte-suede/), and only a second dependent
wanting the same pin needs a link. Pass --root-owned for the release
arrangement instead, when you want a name that doesn't depend on who asked for
it.
A dependency also declares what it needs from npm and PyPI. The installer
merges those declarations into your package.json and requirements.txt —
adding what is missing, and never touching a lockfile. (Under --dev, into
devDependencies and requirements-dev.txt instead.)
A package you already declare at a different version stops the install rather than being resolved silently: unifying two version ranges is a judgment call about your code, not the installer's. The refusal names the way past it:
BLOCKED
python dependency sqlmodel: a dependency asks for sqlmodel>=0.0.14, your
requirements.txt declares sqlmodel==0.0.9.
Unify the versions yourself - suede will not guess - or re-run with
--allow-conflicting-packages to keep your own declarations and install the
rest anyway.
With --allow-conflicting-packages, your declaration is kept verbatim, every
non-conflicting package still merges, and each conflict is reported as a
warning — the dependency now runs against a version it never saw, which is your
call to make. --no-npm and --no-python skip either merge entirely.
suede check # audit the tree: undeclared, missing and dangling entries
suede list [--json] # every dependency, its classification, target and pin
suede diff # release dependencies that no longer match their pin
suede remove <entry> # drop an entry; reports what becomes orphaned, deletes nothing
suede extract # write release/.suede/.dependencies/ (what the publish flow runs)Inside a suede dependency these are already vendored on main — run them as
bash .suede/core/suede <command>. In a plain consumer repository there is
nothing to vendor, so reach the same file directly:
python3 <(curl -fsSL https://suede.sh/suede) checkUseful install flags: --dry-run, --plan-json, --yes, --commit,
--on-conflict coexist|unify-newest|defer, --allow-conflicting-packages,
--no-npm, --no-python, --separator, --target, --name, --quiet. Exit
codes: 0 success, 2 usage, 3 precondition, 4 unresolved conflict, 5
check found a failure.
While it works, the installer narrates what it is fetching on stderr — every
slow part of an install is a network round trip, and all of them happen before
there is a plan to show. --quiet turns that off; the plan itself always goes
to stdout, so piping is unaffected.
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 a dependency, first confirm that your environment has the git subrepo command available. If not, see instructions on installing git-subrepo.
git subrepo --versionThen run the sync script that shipped inside the dependency. It takes no target — the dependency it pulls is the one it lives in:
bash <path-to-dependency>/.suede/core/syncFor example:
bash ./my-app.some-suede/.suede/core/sync
This fetches and merges the newest commits from the dependency's release branch into your subrepo folder and applies them as a single commit.
Tip
sync is a wrapper around git subrepo pull that does two things a bare
pull will not: it runs from the repository root with a root-relative path, so
where you are does not matter, and it resolves a symlink to the real folder
first — git subrepo pull on a symlink path fails outright, and the edge
entries between your dependencies are symlinks by default.
Anything you pass is handed straight to git subrepo pull, so
bash <path-to-dependency>/.suede/core/sync --force works as git-subrepo
documents it.
To see what a sync would bring before running one, ask the dependency:
bash <path-to-dependency>/.suede/core/diff --syncThat compares your copy — local edits and all — against the current tip of the
dependency's release branch, names both commits, and says outright when your
pin already is the tip. See reviewing your own changes
for the other direction.
Important
git subrepo pull requires a clean working tree, while installing does not.
If you have just installed something, commit before syncing.
To view the changes that were committed, run:
git diff HEAD~1 HEADOne 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 dependency'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.
Because the dependency's history is not in your repository, git log on that folder shows your commits but nothing to compare them against. diff fills that in:
bash <path-to-dependency>/.suede/core/diffIt compares the commit your .gitrepo pins against your copy as it stands — uncommitted edits and brand-new files included, ignored files and the .gitrepo itself excluded. The + lines are yours, and they are exactly what upstream would propose. Extra arguments go to git diff, so --stat and --name-only work; it exits 0 when there is no difference and 1 when there is.
If you then want to offer those changes back to the dependency, commit them and run the upstream script that shipped inside it:
bash <path-to-dependency>/.suede/core/upstreamThe working tree must be clean first — commit the changes you want to send.
What happens next:
- Your commits are split out via
git subrepoand pushed to a deterministic branch on the dependency's remote:downstream/<owner>/<repo>-<your-commit>. - A pull request is opened into
mainby the suede-downstream-to-main action, which replays your change onto the current release and transplants it underrelease/on top ofmain. Maintainers can test, fix and merge it there. Once merged, it flows back out toreleasethrough the normal publish path. - Your local state is restored, so a later sync stays safe.
Each commit becomes its own proposal; re-running on the same commit is a no-op. Pass -r/--remote <name> to push to a remote other than the one tracked in the dependency's .gitrepo.
Important
The dependency's release branch is never modified by this flow, so other
consumers are unaffected by an unreviewed change. For the same reason, do not
git subrepo push onto a dependency's release branch directly — that writes
unvetted code onto the branch everyone installs from.
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, which vendors
.suede/coreonto both branches, connects./releaseto thereleasebranch via git-subrepo, and publishes the first release
Tip
scripts/create/dependency.sh does steps 1
and 2 in one command (it needs an authenticated gh):
./scripts/create/dependency.sh <name> [public|private] [--org <org>]
- 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 -fsSL 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 mirrors the content of thereleasebranch. Do not put other files (tests, examples, docs, etc.) inside./release. - Automatic publishing. Whenever a change under
release/lands onmain, the subrepo-push-release action runs.suede/core/push-release.sh, which regenerates the manifest, runs the publish guard, and then syncs./releaseout to thereleasebranch.
Note
The publish also updates ./release/.gitrepo on main to point at the new commit on the release branch, so pull from main before pushing further changes.
- The guard, and why a publish can be refused. A release dependency ships as a pointer, so before that pointer goes out the guard checks it is honest (
suede diff— nothing has drifted from its pinned commit) and that nothing is resolved implicitly (suede check). If either fires, the reason lands in the job summary and thereleasebranch is not touched, so consumers stay on the last honest version. Run the same checks locally before you push:Ifbash .suede/core/diff.sh # any release dependency with local modifications bash .suede/core/suede check # any implicit or dangling resolution
diffreports divergence you have three honest options: revert the changes, upstream them, or vendor the dependency withbash .suede/core/vendor.sh <entry>so the source actually ships. - Avoid direct commits to the
releasebranch. All changes flow frommain→releasevia the automated workflow. The only time you'd interact withreleasemanually is if something went wrong and you need to fix merge conflicts (which should be rare). - Handle contributions as pull requests. Consumers propose changes with
upstream, which opens a PR intomainwithout ever touchingrelease. Review and merge those as you would any other PR; merging republishes through the normal path. - Update the vendored machinery with one command, on
main..suede/core,release/.suede/core,.github/workflowsandrelease/.github/workflowsare all subrepos of this library. Get fixes by pulling them, never by editing the files in place:bash .suede/core/sync.sh
Note
There is a reason this is a script rather than four git subrepo pulls. The workflow subrepos were cloned into the template your repository was created from, and a repository made from a template starts a fresh history — so their recorded parent names a commit that does not exist in it, and a plain pull refuses. (They live in the template because an Action is restricted in what it may do to .github/workflows.) sync.sh repairs the parent and retries. It also clears the leftovers that pulling a subrepo nested inside release/ leaves behind, which would otherwise stop the next publish. See .suede/core's README.
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.
A suede dependency may itself depend on other suede dependencies. Which kind a dependency is, is decided entirely by where it lives and what it is named — no config file, no manifest you maintain by hand:
| Kind | How it is announced | What ships |
|---|---|---|
| Release dependency | A root entry named $repo$SEP<dependency> — a folder, or a symlink to one anywhere outside release/ |
A pointer (.gitrepo), not the source |
| Development dependency | Any other .gitrepo folder outside release/ |
Nothing; the release branch never sees it |
| Vendored release dependency | It lives inside release/ |
The source itself, verbatim |
$repo is your repository's name without the owner; $SEP is the separator
between it and the dependency's name — . where imports are path literals
(TypeScript, Svelte, Go), __ where a path segment has to be a legal identifier
(Python, Rust). The prefix match includes the separator, so a sibling folder
that merely starts with your repo's name is never promoted by accident.
DEPENDENCIES-OF-DEPENDENCIES.md is the full treatment, including why the separator is part of the name match and what to do when a dependency can no longer stay pristine.
A project records its whole transitive closure as its own release
dependencies. If you install B and B needs C, you end up with root
entries for both, and B gets a symlink:
app.B/ real folder — B's release bytes
app.C/ real folder — C's release bytes
B.C -> ./app.C symlink satisfying B's edge
Three things fall out of that, and they are why the bookkeeping is worth it: a
manifest is a complete recipe (install each pointer once, flat, done);
recursion becomes a repair path rather than the happy path; and every
dependency in the closure is named in your root, so every one is yours to
repoint, fork or unify. suede check enforces exactly one structural rule —
that nothing was resolved implicitly — and stays informational about which
commit you chose.
The release/.suede/.dependencies/ folder is generated by
suede extract during the publish. Don't edit it by hand.
MIGRATION-V1-V2.md is the general procedure — it covers
every shape a repository is currently in (a dependency that predates the
vendored core, one that has it, or a plain consumer), the order to migrate them
in, and the steps for each. Copy it into the repository as MIGRATION.md and
work through it.
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 extension can be omitted from requests (e.g.
https://suede.sh/install/releaseinstead ofhttps://suede.sh/install/release.sh), andhttps://suede.sh/suedeserves the installer itself - 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.
The installer is a single dependency-free Python file. macOS ships a suitable
interpreter with the Command Line Tools (xcode-select --install); most Linux
distributions have one already. Nothing is installed from PyPI — there are no
runtime dependencies, which is what lets you read
scripts/suede.py and patch it if you ever need to.
git is required too, and does all the network access, so private
repositories, SSH keys and credential helpers work with no extra setup.
An installed dependency records the SSH URL of its remote, so git subrepo push from inside it has a route back upstream — HTTPS password authentication
no longer offers one. What you publish records the HTTPS URL instead, so
consumers and CI runners can resolve your dependencies without a key of yours.
Fetching tries SSH first and falls back to HTTPS, and suede treats the two
spellings as one repository, so a tree installed either way plans and checks
identically.
Install git-subrepo
Needed to sync dependencies (pull, push), not to install them — the
installer uses plain git only.
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.
Note
git-subrepo is enabled by sourcing its .rc from your shell startup file, so
a script that does not run through your login shell can find it missing even
though it is installed. If GIT_SUBREPO_ROOT is set, sync
and upstream source
$GIT_SUBREPO_ROOT/.rc themselves rather than failing.
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
sync - Propose your local changes back to the dependency with
upstream - 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
- Contributions from consumers arrive as pull requests against the dependency's
mainbranch - The consumed
releasebranch is never written to directly, so an unreviewed change cannot reach other consumers - Maintainers vet changes, and a publish is refused outright if a shipped pointer would be dishonest
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. - Windows: work inside WSL 2 with the repository on the Linux filesystem. The edge entries between dependencies are symlinks, and native Windows handles committed symlinks poorly. A devcontainer on Windows already runs on the WSL 2 backend.
This repo pushes to the downstream suede-dependency-template using a personal access token. PATs expire, so a workflow checks the remaining time and keeps a status banner at the top of this README up to date.
One-time setup after you fork:
-
Create the token. GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens. Scope it to only the target repo ("Only select repositories") and grant:
- Contents: Read and write
- Workflows: Read and write
- Metadata: Read-only (GitHub adds this automatically)
-
Store the token. Save it as a repository secret named
SUEDE_DEPENDENCY_TEMPLATE_PAT(Settings → Secrets and variables → Actions → Secrets). -
Record the expiry. On the token page, copy the expiration date and save it as a repository variable named
SUEDE_DEPENDENCY_TEMPLATE_PAT_EXPIRY(same screen, Variables tab). Use ISO formatYYYY-MM-DD(e.g. GitHub's "Tue, Jun 30 2026" becomes2026-06-30).
That's it. The Check deploy token expiry workflow runs on every push to
main and weekly, then commits an updated banner to the top of this README:
- 🟢 TIP — more than 30 days left
- 🟡 WARNING — 30 days or fewer
- 🔴 CAUTION — final week, or already expired
When you rotate the token, update both SUEDE_DEPENDENCY_TEMPLATE_PAT (new token) and
SUEDE_DEPENDENCY_TEMPLATE_PAT_EXPIRY (new date).
Banner placement (optional). By default the banner is prepended to the very top of the README. To pin it somewhere specific instead — say, under your title or below a badges row — commit these two markers where you want it, and the workflow will fill that spot from then on:
<!-- TOKEN-STATUS:START -->
> [!TIP]
> 🟢 The deploy token (`SUEDE_DEPENDENCY_TEMPLATE_PAT`) is good for **68 more days** (2026-11-08).
> _Last checked: 2026-08-31 (UTC)._
<!-- TOKEN-STATUS:END -->The workflow only ever rewrites the lines between those markers, so anything above or below stays exactly as you left it.

