Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 86 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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"
10 changes: 7 additions & 3 deletions .github/workflows/dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/sign-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down
79 changes: 66 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<div align="center">

# Project Colony Resources

**Shared design tokens, rules, templates and the colony-ui crate every Project Colony program builds on.**

</div>

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.
Expand All @@ -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

```
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
`<platform root>/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).
Expand Down
9 changes: 8 additions & 1 deletion crates/colony-ui/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions design/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion design/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 9 additions & 7 deletions templates/sign-and-publish-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading