From d4e8023176f706aec4f2bab1bae0e20372cc14f6 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 12 Oct 2026 12:59:29 +0200 Subject: [PATCH 1/2] ci: test colony-ui on Windows and macOS, install Rust with rustup, and publish the code signing and privacy policy Replace dtolnay/rust-toolchain with rustup in CI, the dependencies workflow and the release template. Run colony-ui's tests on Windows and macOS. Add an action pins job that fails on an unpinned action, a pin without its version comment, or one action pinned at two commits; align upload-artifact on v7.0.2 in the shared workflow so the template and the workflow agree. README: centred header with a tagline, a Status blockquote, a design rules index, Code signing policy and Privacy sections, and License. --- .github/workflows/ci.yml | 95 +++++++++++++++++++++++--- .github/workflows/dependencies.yml | 10 ++- .github/workflows/sign-and-publish.yml | 4 +- README.md | 79 +++++++++++++++++---- design/dependencies.md | 21 ++++++ design/releases.md | 5 +- templates/sign-and-publish-caller.yml | 16 +++-- 7 files changed, 195 insertions(+), 35 deletions(-) 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/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 b6b4666..05f9e51 100644 --- a/design/releases.md +++ b/design/releases.md @@ -337,7 +337,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. From f5d2222bb5a7adf4757320b6b3e8e2bb065f964c Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 12 Oct 2026 13:16:22 +0200 Subject: [PATCH 2/2] test(colony-ui): let the nesting test see the Windows cache subdirectory The test never ran on Windows, where cache_dir adds 'cache' below Colony/ by design. --- crates/colony-ui/src/paths.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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()