# Package Sources
Every time you run a rototo command, or load config from an app, the very first
question is the same: *where is the package?* That answer is a **package
source** - a short string that tells rototo where to go and get your config.
The nice part is that it's one string, and it works the same everywhere. The
`app-config` you type after `rototo lint` is the same thing you put in
`ROTOTO_PACKAGE_SOURCE` for your running service, and the same thing you hand to
`Package.load(...)` in code. Learn it once and you're done.
This page walks through every shape that string can take.
## The shortest one: a folder on your machine
Most of the time, especially while you're editing, the package is just a folder
sitting on your disk. So the source is just the path to that folder:
```sh
rototo lint app-config
rototo lint ./packages/checkout
rototo lint /home/me/work/runtime-config
```
Relative or absolute, both are fine. No prefix, no ceremony - if it looks like a
path, rototo treats it as a path.
And if you don't pass anything at all, rototo is helpful about it: it looks in
the current folder, then the one above it, and so on, until it finds a
`rototo-package.toml`. So when you're already standing inside your package, you
can just say:
```sh
rototo lint
```
If you ever want to be explicit that it's a local path - say, in a script where
you'd rather not leave it to guessing - you can spell it out with `file://`:
```sh
rototo lint file:///home/me/work/runtime-config
```
It means exactly the same thing as the plain path. One small catch: the
`file://` form is *just* a folder, so it doesn't take any of the `#...` extras
described below. If you find yourself wanting those, you probably want a git
source instead.
## Loading straight from a git repo
Here's where it gets useful. Your config lives in git - that's the whole point -
so you can point rototo straight at the repo without cloning it yourself first.
You do that by sticking `git+` in front of the repo URL:
```sh
# over HTTPS
rototo lint git+https://github.com/acme/runtime-config.git
# over SSH
rototo lint git+ssh://git@github.com/acme/runtime-config.git
# even a local bare repo
rototo lint git+file:///srv/git/runtime-config.git
```
rototo fetches the repo into a temporary spot, reads the package, and cleans up
after itself. You don't manage the checkout.
Two things you'll almost always want to add: *which version* and *which folder*.
That's what the part after the `#` is for.
### The bit after the `#`: version and folder
A git source can carry two extra details, written as `#ref:subdir`:
- the **ref** - a branch, a tag, or a full commit. This is the "which version"
part.
- the **subdir** - the folder inside the repo where the package actually lives.
Repos often hold more than one thing, so this says "the package is in *here*".
Put together:
```sh
rototo lint git+https://github.com/acme/runtime-config.git#main:packages/checkout
```
Read that as: *the `runtime-config` repo, on `main`, and the package is in
`packages/checkout`.*
You don't have to supply both. The colon is the divider, so:
- `#main` - just a version. (Package is at the repo root.)
- `#main:packages/checkout` - version *and* folder.
- `#:packages/checkout` - just a folder, on whatever the repo's default is.
(Note the leading colon: nothing before it means "no specific ref".)
A quick word on which ref to pick, because it changes how things behave later:
- A **branch** like `main` keeps moving - point at it and you'll pick up newer
reviewed commits over time.
- A **tag** is a friendly name for a fixed release.
- A **full commit** is nailed down forever - perfectly reproducible, but it
never changes, so a long-running service watching it will never see anything
new.
That last point matters for [refreshing services](./adoption.md): a moving ref
is how config reaches a running fleet without redeploying; a pinned ref is how
you guarantee an exact, reproducible build.
## Loading a packaged-up archive over HTTPS
Pulling from git is great for small setups, but once you've got a real fleet,
having every instance clone from GitHub on a schedule gets fragile - now your
config depends on GitHub being up and on fetch behaving at scale. So for
production, the usual move is to pack the package into a single `.tar.gz` file,
drop it on an object store behind a CDN, and load *that*:
```sh
rototo lint https://config.acme.com/rototo/checkout/prod/current.tar.gz
```
(You build that archive with `rototo package`, which writes a deterministic
tarball you can upload.)
An archive is already a self-contained, fixed thing, so there's no "ref" to pick
- a `.tar.gz` is whatever it is. The only extra you can add is the folder
inside it, using the leading-colon form:
```sh
rototo lint https://config.acme.com/rototo/checkout/prod/current.tar.gz#:packages/checkout
```
Because an archive is just a file at a URL, the *URL* is what decides whether it
moves or stays put:
- a URL addressed by content, like `.../sha256:0f4c...b91.tar.gz`, always points
at the exact same bytes - safe to cache hard, perfect for reproducible
releases.
- a "channel" URL like `.../prod/current.tar.gz` is meant to be re-pointed at a
newer archive when you promote a release, so it should be cached only briefly.
## One thing rototo won't do: plain `http://`
You can load over `https://`, but not plain `http://`. Same goes for git:
`git+https://` and `git+ssh://` are fine, `git+http://` is not. This isn't an
oversight - config is exactly the kind of thing you don't want a network snoop
or man-in-the-middle messing with, so the unencrypted forms are turned off on
purpose. If you try one, rototo tells you to use `https://` instead.
## Private archives need a token
If an `https://` archive is private, rototo sends a **bearer token** with the
fetch. One important boundary first: this covers archive sources only. Git
sources (`git+https://`, `git+ssh://`) authenticate through git itself - your
SSH keys, credential helpers, or `gh auth` - and rototo stays out of that.
The simple case is one private archive host:
```sh
# as a flag
rototo lint https://config.acme.com/checkout/current.tar.gz --package-token "$TOKEN"
# or as an environment variable
ROTOTO_PACKAGE_TOKEN="$TOKEN" rototo lint https://config.acme.com/checkout/current.tar.gz
```
A bare token like that binds to a **single archive origin**: the first host the
load fetches an archive from. That's deliberate. A composed package can pull
archives from more than one host, and a token minted for one of them must not
be broadcast to the others. If a load with a bare token touches a second
origin, it fails and tells you to scope your tokens.
Scoping is the general form. An entry is `PREFIX=TOKEN`, where the prefix is an
`https://` URL prefix:
```sh
rototo lint ./overlay \
--package-token "https://config.acme.com/team-a=$TEAM_A_TOKEN" \
--package-token "https://archives.example.net=$PARTNER_TOKEN"
```
The environment variable takes the same entries, separated by whitespace:
```sh
ROTOTO_PACKAGE_TOKEN="https://config.acme.com/team-a=$TEAM_A_TOKEN https://archives.example.net=$PARTNER_TOKEN"
```
The rules are small and worth knowing:
- **Longest matching prefix wins.** A token for
`https://config.acme.com/team-a` beats one for `https://config.acme.com`
when the archive lives under `/team-a`. Prefixes match at path boundaries:
`/team` never covers `/teammate`.
- **No match means anonymous.** A request to a host you didn't scope a token
for carries no credential at all - a secret never goes somewhere you didn't
name.
- **The `https://` spelling is what makes an entry scoped**, never the `=`
sign. A base64 token that happens to end in `=` padding is still one bare
token.
- **A bare token must stand alone.** Mixing it with scoped entries, or passing
two bare tokens, is an error - there'd be no unambiguous answer for who gets
what.
- **Tokens never follow a cross-origin redirect.** If an archive host
redirects to a different host, the request continues without the
Authorization header.
In the SDKs, the same two shapes exist as load options: a single token string,
or a map from URL prefixes to tokens.
One habit worth keeping: when CI checks that production can really load the
released package, use the *same* authenticated source production will use. That
way you find out about a permissions problem in CI, not at 2am.
## When one package builds on another
A package can stand on top of other packages - shared defaults, a common set of
condition variables, that sort of thing. It does that by listing other sources in its
`rototo-package.toml`, and here's the part that matters for this page: those
parent sources use the **exact same grammar** as everything above. A parent can
be a local folder, a git repo with `#ref:subdir`, or an HTTPS archive.
Relative paths in that list are resolved against the package doing the
extending, so a package and the parents it leans on can travel together. The
mechanics of how packages combine live in the [package format
reference](./package-format.md); what belongs on this page is how the sources
in that list behave.
### The rules for sources in `extends`
- **Every format works.** Local paths, `file://`, `git+https://`, `git+ssh://`,
`git+file://`, and `https://` archives, including the `#ref:subdir` and
`#:subdir` fragments. If you can type it as a package argument, you can list
it in `extends`.
- **Relative paths resolve against the extending package**, not against
wherever you happened to run the command. That's what lets a repo carry
`extends = ["../shared-config"]` and load the same way from CI, your laptop,
or an app.
- **A remote package can't reach into your filesystem.** Once a package has
been staged from a git or archive source, its `extends` entries may only be
relative paths inside its own checkout or other remote sources. An absolute
path, a `file://` entry, a `git+file://` entry, or a `../` that climbs out
of the checkout fails with "escapes a staged package". A fetched package naming `/etc` or your
home directory is not a composition feature; it's a problem.
- **Remote extending remote is the cross-team shape.** A team publishes its
base package at a git ref or archive URL; your package extends it by that
source. Pin the parent to an immutable ref (a commit hash or a
content-addressed archive) when you want reproducible builds - the same
child commit then always composes against the same parent bytes.
- **Chains have limits.** Extends can nest, and parents can have parents, up
to a depth of 32. A cycle (a package that ends up extending itself through
any chain) fails the load with the cycle spelled out.
## The whole grammar on one page
If you just want the cheat sheet:
| A folder on disk | `app-config`, `./pkg`, `/abs/path` |
| Nearest package above you | *(omit the source entirely)* |
| A folder, spelled out | `file:///abs/path` |
| A git repo | `git+https://…`, `git+ssh://…`, `git+file://…` |
| A git repo, specific version | `git+https://…#main` |
| A git repo, version + folder | `git+https://…#main:packages/checkout` |
| A git repo, folder only | `git+https://…#:packages/checkout` |
| An HTTPS archive | `https://…/current.tar.gz` |
| An HTTPS archive, folder inside | `https://…/current.tar.gz#:packages/checkout` |
And the things rototo will refuse: plain `http://`, `git+http://`, and a `#...`
fragment on a `file://` source.