diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7bceec8..53c5879 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,11 +17,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-
- - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master, 2026-10-09
with:
- toolchain: stable
- components: clippy, rustfmt
+ persist-credentials: false
+
+ # rustup is on every GitHub-hosted runner image: no third-party action to
+ # pin, and nothing for Dependabot to lose track of.
+ - name: Install Rust
+ run: |
+ rustup toolchain install stable --profile minimal -c rustfmt,clippy --no-self-update
+ rustup default stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
@@ -63,13 +67,15 @@ jobs:
# colony-ui. Without this job nothing ever builds on it, and it drifts.
#
# RUSTUP_TOOLCHAIN is what pins the cargo invocations: it outranks both the
- # rustup default the toolchain action sets and any rust-toolchain.toml. The
- # rustc check makes a pin that silently stopped working fail loudly.
+ # runner's rustup default and any rust-toolchain.toml. The rustc check makes
+ # a pin that silently stopped working fail loudly.
msrv:
name: MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Read the declared MSRV
id: msrv
@@ -79,9 +85,8 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "RUSTUP_TOOLCHAIN=$version" >> "$GITHUB_ENV"
- - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master, 2026-10-09
- with:
- toolchain: ${{ steps.msrv.outputs.version }}
+ - name: Install Rust ${{ steps.msrv.outputs.version }}
+ run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --no-self-update
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
@@ -92,3 +97,75 @@ jobs:
rustc --version | grep -q "^rustc ${RUSTUP_TOOLCHAIN}[. ]" \
|| { echo "::error::expected rustc ${RUSTUP_TOOLCHAIN}, got $(rustc --version)"; exit 1; }
cargo check --locked --all-targets
+
+ # The job above runs every test on Linux. colony-ui's paths module takes other
+ # branches on Windows and macOS (the Windows cache directory must not be the
+ # config directory), and those tests only run on those platforms.
+ colony-ui:
+ name: colony-ui tests (${{ matrix.os }})
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Install Rust
+ shell: bash
+ run: |
+ rustup toolchain install stable --profile minimal --no-self-update
+ rustup default stable
+
+ - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
+
+ - name: Tests
+ run: cargo test -p colony-ui --locked
+
+ # Dependabot moves the pins in .github/workflows/ but never reads templates/,
+ # and programs copy their release workflow from templates/. Every `uses:` in
+ # both must be a full commit SHA with its version in a trailing comment, and
+ # an action pinned in two places must be pinned at the same commit, so a
+ # Dependabot bump fails here until the template is bumped in the same PR.
+ # The template's call to the shared workflow names "organisation pin"
+ # instead of a version: it moves only as design/releases.md describes.
+ pins:
+ name: action pins
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Every action is pinned, at one commit everywhere
+ run: |
+ grep -nE '^[[:space:]]*(-[[:space:]]+)?uses:' .github/workflows/*.yml templates/*.yml \
+ | awk '
+ {
+ split($1, loc, ":"); file = loc[1]; line = loc[2]
+ match($0, /uses:[[:space:]]*[^[:space:]#]+/)
+ ref = substr($0, RSTART, RLENGTH); sub(/uses:[[:space:]]*/, "", ref)
+ comment = $0; sub(/^[^#]*/, "", comment)
+ }
+ ref ~ /^\.\// { next }
+ {
+ at = index(ref, "@"); action = substr(ref, 1, at - 1); sha = substr(ref, at + 1)
+ }
+ at == 0 || length(sha) != 40 || sha ~ /[^0-9a-f]/ {
+ printf "::error file=%s,line=%s::%s is not pinned to a full commit SHA\n", file, line, ref
+ bad = 1; next
+ }
+ comment !~ /^#[[:space:]]*v[0-9]/ && action !~ /^Project-Colony\/Project-Colony-Resources\// {
+ printf "::error file=%s,line=%s::%s needs its version in a trailing comment (# vX.Y.Z)\n", file, line, action
+ bad = 1
+ }
+ action in first && pin[action] != sha {
+ printf "::error file=%s,line=%s::%s is pinned at %s here but at %s in %s\n", file, line, action, sha, pin[action], first[action]
+ bad = 1; next
+ }
+ !(action in first) { pin[action] = sha; first[action] = file ":" line }
+ END { exit bad }
+ '
+ echo "every action is pinned to one commit"
diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml
index 86521e9..2735750 100644
--- a/.github/workflows/dependencies.yml
+++ b/.github/workflows/dependencies.yml
@@ -20,10 +20,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-
- - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master, 2026-10-09
with:
- toolchain: stable
+ persist-credentials: false
+
+ # rustup is on the runner image already; no third-party toolchain action.
+ - name: Install Rust
+ run: |
+ rustup toolchain install stable --profile minimal --no-self-update
+ rustup default stable
- name: Lockfile is current
run: |
diff --git a/.github/workflows/sign-and-publish.yml b/.github/workflows/sign-and-publish.yml
index 2e05a46..337b54b 100644
--- a/.github/workflows/sign-and-publish.yml
+++ b/.github/workflows/sign-and-publish.yml
@@ -209,7 +209,7 @@ jobs:
- name: Upload the unsigned file for SignPath
id: unsigned
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-unsigned-${{ matrix.asset }}
path: unsigned/${{ matrix.asset }}
@@ -254,7 +254,7 @@ jobs:
exit 1
fi
- - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signpath-signed-${{ matrix.asset }}
path: signed/${{ matrix.asset }}
diff --git a/README.md b/README.md
index 19b8ce4..1b8d116 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,11 @@
+
+
# Project Colony Resources
+**Shared design tokens, rules, templates and the colony-ui crate every Project Colony program builds on.**
+
+
+
The single place where Project Colony's shared design decisions are written down:
the theme palettes, the accent colours, the UI conventions every Colony program
is expected to follow.
@@ -11,6 +17,18 @@ recover the palettes, and its companion request for `src/i18n.rs` had already
gone stale, because Colony split that file into `src/i18n/{fr,en}.rs`. That is
the class of breakage this repo exists to end.
+> **Status:** `colony-ui` is published on crates.io, and Colony uses its
+> palettes and its `paths` module: Colony's hand-maintained palette constants
+> are gone. The preferences kit new in 0.1.6 (`StandardPreferences`, the fonts,
+> the navigation and Preferences page widgets) is adopted by no program yet;
+> Colony still draws its own page, so the kit is proven by unit tests and a
+> compiled example, not by a program in daily use. CI runs the `paths` tests on
+> Linux, Windows and macOS. `colony-ui` releases now go through release-please
+> and crates.io Trusted Publishing; that path has not published yet, because
+> 0.1.7 waits on the Trusted Publisher registration on crates.io. The SignPath
+> path of the shared signing workflow is wired but unused: the organisation
+> secret and variable it needs do not exist yet.
+
## How it works
```
@@ -52,6 +70,24 @@ Colony shipped before the import; see [Guarantees](#guarantees).
| `crates/colony-ui/` | the crate programs depend on: theme, fonts, labels, preferences, widgets |
| `tools/colony-tokens/` | the generator and its tests |
+## Design rules
+
+`design/` is this repository's product rather than its documentation, so it
+sits at the top level instead of under `docs/`. One page per question:
+
+| Page | What it answers |
+|---|---|
+| [repository-layout.md](design/repository-layout.md) | how a repository and its crates are arranged and named |
+| [filesystem.md](design/filesystem.md) | where a program keeps its config, data and cache, on all three platforms |
+| [navigation.md](design/navigation.md) | how a program is laid out, and how the user reaches its settings |
+| [settings-page.md](design/settings-page.md) | what every Preferences page contains, and in what order |
+| [theming.md](design/theming.md) | how a palette is built, and how to add a theme family |
+| [typography.md](design/typography.md) | which fonts and text sizes a program uses, and where its icons come from |
+| [i18n.md](design/i18n.md) | which languages a program ships, and how its strings are named and written |
+| [releases.md](design/releases.md) | how a program is versioned, built, signed and picked up by the launcher |
+| [dependencies.md](design/dependencies.md) | how crates, the toolchain and action pins stay on their latest release |
+| [documentation.md](design/documentation.md) | how the README and `docs/` are laid out |
+
## Consuming this repo
### Rust / iced programs
@@ -110,6 +146,9 @@ way: the identity button, a sidebar, and the whole Preferences page.
arm, no picker entry, no locale edit. Add the TOML, regenerate, bump the
dependency.
+A program that still ships its own copy of the palettes migrates by depending
+on `colony-ui` and deleting that copy.
+
The shared widgets take a [`Typography`] rather than reading the host's state;
`Typography::current()` carries the product of the user's two text-size
preferences and the font the dyslexia toggle selects.
@@ -221,18 +260,6 @@ you can predict removes that cost outright.
`templates/program/` holds a README and a `docs/README.md` skeleton to start
from.
-## Status
-
-`colony-ui` is published on crates.io, and Colony itself has migrated to it:
-its hand-maintained palette constants are gone, replaced by this crate. A
-program that still ships its own copy of the palettes migrates by depending on
-`colony-ui` and deleting that copy.
-
-The preferences kit (`StandardPreferences`, the fonts, the navigation and
-Preferences page widgets) is new in 0.1.6 and not adopted by any program yet:
-Colony, the reference it was extracted from, still draws its own page. It is
-covered by unit tests and a compiled example, not yet by a program in daily use.
-
## Releasing colony-ui
Nothing about a `colony-ui` release is done by hand. Conventional commits that
@@ -251,7 +278,33 @@ If publishing fails after the tag exists, fix the cause, then publish that tag:
gh workflow run release.yml --ref colony-ui-vX.Y.Z -f tag=colony-ui-vX.Y.Z
```
-## Licence
+## Code signing policy
+
+This repository ships no binaries, so there is nothing here to sign. `colony-ui`
+is published to crates.io only from CI, by
+[`.github/workflows/release.yml`](.github/workflows/release.yml) on a
+release-please tag, with a short-lived token that crates.io mints for that
+workflow through Trusted Publishing; no registry token is stored anywhere.
+
+The programs built on these resources are signed by the shared
+[sign-and-publish workflow](.github/workflows/sign-and-publish.yml) with the
+organisation's ed25519 release key, in a job that builds nothing. How that
+works: [design/releases.md](design/releases.md#5-signing). How a program
+documents it and how to verify a download:
+[templates/program/README.md](templates/program/README.md#code-signing-policy).
+
+## Privacy
+
+`colony-ui` and `colony-tokens` collect nothing, store nothing and send
+nothing, and neither makes a network call. The only thing `colony-ui` does to
+the disk is create the directories `paths::config_dir`, `data_dir`,
+`cache_dir`, `apps_dir` and `app_dir` return, under
+`/Colony/`; what goes inside them is up to the program.
+`colony-tokens` reads `tokens/`, and writes `generated/` and
+`crates/colony-ui/src/generated/` in this repository's checkout, deleting the
+stale files it finds there.
+
+## License
GPL-3.0-or-later, matching the rest of the Project Colony organisation. See
[LICENSE](LICENSE).
diff --git a/crates/colony-ui/src/paths.rs b/crates/colony-ui/src/paths.rs
index aec33af..9443d06 100644
--- a/crates/colony-ui/src/paths.rs
+++ b/crates/colony-ui/src/paths.rs
@@ -215,10 +215,17 @@ mod tests {
#[test]
fn every_root_nests_under_the_vendor_then_the_program() {
+ let mut cache = locate::cache_dir("Digger").unwrap();
+ if cfg!(windows) {
+ // Windows adds `cache` below the program directory, checked by
+ // windows_isolates_the_cache_under_the_shared_root.
+ assert!(cache.ends_with(CACHE), "{cache:?}");
+ cache.pop();
+ }
for path in [
locate::config_dir("Digger").unwrap(),
locate::data_dir("Digger").unwrap(),
- locate::cache_dir("Digger").unwrap(),
+ cache,
] {
let tail: Vec<_> = path
.components()
diff --git a/design/dependencies.md b/design/dependencies.md
index 453f51f..82ac780 100644
--- a/design/dependencies.md
+++ b/design/dependencies.md
@@ -69,6 +69,21 @@ build the program, so it belongs in CI — an untested floor is a guess.
Move it forward deliberately, when a language feature is worth it, not by
accident because a dependency raised its own floor.
+In CI, install Rust with `rustup`, which every GitHub-hosted runner image
+already carries:
+
+```yaml
+- name: Install Rust
+ shell: bash
+ run: |
+ rustup toolchain install stable --profile minimal --no-self-update
+ rustup default stable
+```
+
+A third-party toolchain action is one more pin to keep current, and one that
+publishes no versions to bump has nothing Dependabot can move. `shell: bash`
+keeps the same step working on the Windows and macOS legs.
+
## GitHub Actions
Colony pins actions by commit SHA with the version in a trailing comment, and
@@ -82,6 +97,12 @@ the `github-actions` entry from `templates/dependabot.yml`, so the action SHAs
keep moving after you adopt them. Dependabot does not read `templates/`, so
the pins there are bumped by hand, to the current release of each action.
+This repository's CI keeps those hand bumps honest: its `action pins` job
+fails when any `uses:` in `.github/workflows/` or `templates/` is not a full
+commit SHA with its version in a trailing comment, or when one action is pinned
+at two different commits. A Dependabot PR that bumps an action the template
+also uses goes red until the template is bumped in the same PR.
+
## Where the ecosystem stands
`colony-ui` is the shared floor. It declares iced once, in
diff --git a/design/releases.md b/design/releases.md
index 82cfdf5..2d66192 100644
--- a/design/releases.md
+++ b/design/releases.md
@@ -347,7 +347,10 @@ sign-and-publish:
`619460ff4dc0049f129955f0988d368427b9eedb`, which is the one the template names.
The pin moves only when `sign-and-publish.yml` itself changes, and then the
template and every caller move to the new commit together. A commit to this
-repository that does not touch the workflow is no reason to move it. That is
+repository that does not touch the workflow is no reason to move it, and
+neither is a routine action bump inside it on its own: that bump reaches the
+callers with the next change that moves the pin, unless it fixes a security
+advisory, which moves the pin by itself. That is
why `templates/dependabot.yml` tells Dependabot to ignore this repository: it
would otherwise propose the newest commit of `main` to each caller on its own.
diff --git a/templates/sign-and-publish-caller.yml b/templates/sign-and-publish-caller.yml
index df647cf..46d1395 100644
--- a/templates/sign-and-publish-caller.yml
+++ b/templates/sign-and-publish-caller.yml
@@ -120,13 +120,15 @@ jobs:
ref: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
persist-credentials: false
- # Pinned to master with `toolchain: stable` rather than to the `stable`
- # branch: that branch is rebased onto every master commit, so a SHA
- # pinned on it stops being reachable from any branch.
- - uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master, 2026-10-09
- with:
- toolchain: stable
- targets: ${{ matrix.target }}
+ # rustup is on every GitHub-hosted runner image, so there is no
+ # third-party toolchain action to pin. Adding the target is a no-op on
+ # the legs that build for their own host; the Intel macOS leg needs it.
+ - name: Install Rust
+ shell: bash
+ run: |
+ rustup toolchain install stable --profile minimal --no-self-update
+ rustup default stable
+ rustup target add ${{ matrix.target }}
# No build cache here: SignPath forbids release builds that reuse
# outputs of earlier, unverified builds.