Keep the files many PRs add to conflict-free (#1564)
Message catalogs sorted one message per line (cargo xtask lists [--fix], checked in cargo xtask ci and the Lists workflow); the language list one per line, English first then by id; THIRD-PARTY-LICENSES.txt and docs/features.md rewritten on main by the Generated files workflow instead of in PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
342305705d
commit
f08ed74617
18 files changed
+6001
-5817
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# Files generated from the code are rewritten here, on main, after each merge, so pull requests
|
||||
# never edit them (two pull requests that each regenerated one would always conflict):
|
||||
# THIRD-PARTY-LICENSES.txt (`cargo xtask licences`) and docs/features.md (`cargo xtask features`).
|
||||
name: Generated files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
# One run at a time; a burst of merges leaves one run waiting, which sees all of them.
|
||||
concurrency:
|
||||
group: generated-files
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
regenerate:
|
||||
name: Regenerate and commit
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
# Always main itself, never pull request code, since this job can push.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: main
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: generated-files
|
||||
- name: THIRD-PARTY-LICENSES.txt
|
||||
run: cargo xtask licences
|
||||
- name: docs/features.md
|
||||
run: cargo xtask features
|
||||
- name: Commit if changed
|
||||
run: |
|
||||
if git diff --quiet -- THIRD-PARTY-LICENSES.txt docs/features.md; then echo "unchanged"; exit 0; fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add THIRD-PARTY-LICENSES.txt docs/features.md
|
||||
git commit -m "Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]"
|
||||
for _ in 1 2 3; do
|
||||
git pull --rebase origin main && git push origin HEAD:main && exit 0
|
||||
sleep 10
|
||||
done
|
||||
exit 1
|
||||
@@ -18,3 +18,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: bash scripts/test-format-pending.sh
|
||||
|
||||
sorted:
|
||||
name: Lists are sorted one item per line
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: lists
|
||||
- run: cargo xtask lists
|
||||
@@ -160,5 +160,11 @@ before the draft is verified.
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a
|
||||
milestone task lands. `cargo xtask features` regenerates `docs/features.md` (SolidCraft's
|
||||
commands by area, from the registry) when commands are added or renamed.
|
||||
milestone task lands. `docs/features.md` (SolidCraft's commands by area, from the registry) and
|
||||
`THIRD-PARTY-LICENSES.txt` are regenerated on `main` after each merge by the Generated files
|
||||
workflow; never regenerate them in a PR.
|
||||
|
||||
## Lists many PRs add to
|
||||
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
|
||||
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
|
||||
then by id, generated files left to `main`, and a design-format change as one new file.
|
||||
@@ -133,5 +133,11 @@ before the draft is verified.
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a
|
||||
milestone task lands. `cargo xtask features` regenerates `docs/features.md` (SolidCraft's
|
||||
commands by area, from the registry) when commands are added or renamed.
|
||||
milestone task lands. `docs/features.md` (SolidCraft's commands by area, from the registry) and
|
||||
`THIRD-PARTY-LICENSES.txt` are regenerated on `main` after each merge by the Generated files
|
||||
workflow; never regenerate them in a PR.
|
||||
|
||||
## Lists many PRs add to
|
||||
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
|
||||
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
|
||||
then by id, generated files left to `main`, and a design-format change as one new file.
|
||||
+20
-2
@@ -98,8 +98,8 @@ The [README](README.md#architecture) has the layer table. The short version:
|
||||
MIT OR Apache-2.0, and its solver and blends are written from the maths. (Running an
|
||||
open-source application to get reference values for a test is fine; reading its source is
|
||||
not.) Permissive
|
||||
dependencies are fine. Note which one you're adding and why in the PR, and run
|
||||
`cargo xtask licences` so THIRD-PARTY-LICENSES.txt lists it (CI checks that it is current).
|
||||
dependencies are fine. Note which one you're adding and why in the PR. THIRD-PARTY-LICENSES.txt
|
||||
lists it once the change lands: the Generated files workflow rewrites it on `main`.
|
||||
Nightly/manual CI also scans advisories, bans and sources with cargo-deny; see
|
||||
[the local command and coverage limits](docs/book/src/dev/testing.md#dependency-advisories).
|
||||
3. **Every asset is original or openly licensed and has a row in
|
||||
@@ -224,6 +224,24 @@ a conflict. `crates/doc/src/format/pending/README.md` has an example.
|
||||
files into `crates/doc/src/format/v<N>/` and fixes that number for good (see
|
||||
[docs/releasing.md](docs/releasing.md)). Never add a file to a `v<N>/` folder afterwards.
|
||||
|
||||
## Lists many changes add to
|
||||
|
||||
Some files gain an item in many pull requests at once. They are kept so that two changes that
|
||||
each add an item only meet when their items are neighbours, which spares everyone a conflict
|
||||
and a fresh CI run:
|
||||
|
||||
- **Message catalogs** (`assets/locales/*.json`): one message per line, sorted by id. Add your
|
||||
message anywhere and run `cargo xtask lists --fix`; `cargo xtask lists` (in `cargo xtask ci`
|
||||
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
|
||||
- **`mod` lines**: one per line, in the sorted group they belong to (`cargo fmt` keeps each group
|
||||
sorted). Add a new module into the existing group, not as a new group at the end.
|
||||
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
|
||||
checks it).
|
||||
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
|
||||
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
|
||||
features`). Don't regenerate or edit them in a pull request.
|
||||
- **Design format**: one file per change, never an edit to a shared number (above).
|
||||
|
||||
## Sending a change
|
||||
|
||||
1. Work is tracked in [GitHub issues](https://github.com/bherbruck/solidcraft/issues), labelled
|
||||
|
||||
+870
-871
File diff suppressed because it is too large.
Load diff
+648
-649
File diff suppressed because it is too large.
Load diff
+562
-564
File diff suppressed because it is too large.
Load diff
+648
-649
File diff suppressed because it is too large.
Load diff
+870
-871
File diff suppressed because it is too large.
Load diff
+495
-495
File diff suppressed because it is too large.
Load diff
+611
-611
File diff suppressed because it is too large.
Load diff
+438
-438
File diff suppressed because it is too large.
Load diff
+654
-655
File diff suppressed because it is too large.
Load diff
@@ -32,15 +32,28 @@ impl Catalog {
|
||||
}
|
||||
|
||||
static ENGLISH: Catalog = Catalog::new("en", "English", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/en.json")));
|
||||
static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ja.json")));
|
||||
static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/de.json")));
|
||||
static FRENCH: Catalog = Catalog::new("fr", "Français", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/fr.json")));
|
||||
static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ja.json")));
|
||||
static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ko.json")));
|
||||
static RUSSIAN: Catalog = Catalog::new("ru", "Русский", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ru.json")));
|
||||
static UKRAINIAN: Catalog = Catalog::new("uk", "Українська", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/uk.json")));
|
||||
static TURKISH: Catalog = Catalog::new("tr", "Türkçe", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/tr.json")));
|
||||
static UKRAINIAN: Catalog = Catalog::new("uk", "Українська", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/uk.json")));
|
||||
static CHINESE: Catalog = Catalog::new("zh", "中文", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/zh.json")));
|
||||
pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &FRENCH, &KOREAN, &RUSSIAN, &UKRAINIAN, &TURKISH, &CHINESE];
|
||||
/// English first (the fallback), then by language id, one per line so that adding a language
|
||||
/// doesn't edit a line another addition edits.
|
||||
#[rustfmt::skip]
|
||||
pub const LOCALES: &[&Catalog] = &[
|
||||
&ENGLISH,
|
||||
&GERMAN,
|
||||
&FRENCH,
|
||||
&JAPANESE,
|
||||
&KOREAN,
|
||||
&RUSSIAN,
|
||||
&TURKISH,
|
||||
&UKRAINIAN,
|
||||
&CHINESE,
|
||||
];
|
||||
|
||||
pub fn is_supported(id: &str) -> bool {
|
||||
LOCALES.iter().any(|catalog| catalog.id == id)
|
||||
@@ -188,6 +201,15 @@ pub fn tr_name(text: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The languages are listed English first, then by id, so adding one doesn't edit the
|
||||
/// line another addition edits.
|
||||
#[test]
|
||||
fn locales_are_listed_english_first_then_by_id() {
|
||||
let ids: Vec<&str> = LOCALES.iter().map(|c| c.id).collect();
|
||||
assert_eq!(ids.first(), Some(&"en"));
|
||||
assert!(ids[1..].is_sorted(), "{ids:?}");
|
||||
}
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
fn placeholders(text: &str) -> BTreeSet<&str> {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
//! into the shipped programs (solidcraft, solidcraft-cli, solidcraft-web), read from
|
||||
//! `cargo metadata` and the crates' own licence files. The programs embed it (Help ▸ About ▸
|
||||
//! Licences, `solidcraft-cli licences`), so a release download needs no licence files beside it.
|
||||
//! `--check` (part of `cargo xtask ci`) fails when the committed file is out of date, e.g. after a
|
||||
//! dependency changed.
|
||||
//! `--check` (part of `cargo xtask ci`) generates it without writing it: it fails when the
|
||||
//! dependency graph can't be read, and only reports a file that is out of date, since pull
|
||||
//! requests leave it to the Generated files workflow (`.github/workflows/generated.yml`), which
|
||||
//! rewrites it on main after each merge.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::path::Path;
|
||||
@@ -21,7 +23,10 @@ pub fn run(root: &Path, check: bool) -> Result<(), String> {
|
||||
if check {
|
||||
let old = std::fs::read_to_string(&path).unwrap_or_default();
|
||||
if old.replace("\r\n", "\n") != text {
|
||||
return Err(format!("{FILE} is out of date (dependencies changed): run `cargo xtask licences` and commit it"));
|
||||
// Not an error: pull requests don't edit the file (two that did would always conflict);
|
||||
// the Generated files workflow rewrites it on main after the merge.
|
||||
println!("licences: {FILE} will change when this lands (the Generated files workflow rewrites it on main)");
|
||||
return Ok(());
|
||||
}
|
||||
println!("licences: {FILE} is up to date");
|
||||
return Ok(());
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! `cargo xtask lists [--fix]`: files that many pull requests add to are kept in a canonical,
|
||||
//! sorted form, so two changes that each add an item meet only when their items are neighbours
|
||||
//! (CONTRIBUTING.md, "Lists many changes add to"). Checked by `cargo xtask ci` and the Lists
|
||||
//! workflow; `--fix` rewrites them.
|
||||
//!
|
||||
//! - The message catalogs (`assets/locales/*.json`): one message per line, sorted by id, no
|
||||
//! duplicate ids, as `serde_json` pretty-prints them.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
const LOCALES: &str = "assets/locales";
|
||||
|
||||
pub fn run(root: &Path, fix: bool) -> Result<(), String> {
|
||||
let dir = root.join(LOCALES);
|
||||
let entries = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let mut files: Vec<_> = entries.filter_map(Result::ok).map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "json")).collect();
|
||||
files.sort();
|
||||
let mut stale = Vec::new();
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let canonical = canonical(&text).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if canonical == text.replace("\r\n", "\n") {
|
||||
continue;
|
||||
}
|
||||
if fix {
|
||||
std::fs::write(&path, &canonical).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
println!("lists: sorted {}", path.display());
|
||||
} else {
|
||||
stale.push(path.strip_prefix(root).unwrap_or(&path).display().to_string());
|
||||
}
|
||||
}
|
||||
if !stale.is_empty() {
|
||||
return Err(format!(
|
||||
"not sorted one message per line (or an id appears twice): {}\nrun `cargo xtask lists --fix` and commit the result",
|
||||
stale.join(", ")
|
||||
));
|
||||
}
|
||||
println!("lists: message catalogs are sorted");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A catalog as it should be written: its messages sorted by id, one per line. A repeated id
|
||||
/// keeps its last message, the one the app reads.
|
||||
pub fn canonical(text: &str) -> Result<String, String> {
|
||||
let map: Map<String, Value> = match serde_json::from_str(text).map_err(|e| e.to_string())? {
|
||||
Value::Object(map) => map,
|
||||
_ => return Err("a catalog must be a JSON object".into()),
|
||||
};
|
||||
if let Some((id, _)) = map.iter().find(|(_, v)| !v.is_string()) {
|
||||
return Err(format!("message `{id}` is not a string"));
|
||||
}
|
||||
Ok(serde_json::to_string_pretty(&Value::Object(map)).map_err(|e| e.to_string())? + "\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::canonical;
|
||||
|
||||
#[test]
|
||||
fn catalogs_are_sorted_one_per_line_and_a_repeated_id_keeps_its_last_message() {
|
||||
let text = "{\n \"b\": \"B\",\n \"a\": \"A\",\n \"b\": \"B2\"\n}";
|
||||
assert_eq!(canonical(text).unwrap(), "{\n \"a\": \"A\",\n \"b\": \"B2\"\n}\n");
|
||||
assert!(canonical("[]").is_err());
|
||||
assert!(canonical("{\"a\": 1}").is_err());
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -10,6 +10,7 @@ mod features;
|
||||
mod i18n;
|
||||
mod layers;
|
||||
mod licences;
|
||||
mod lists;
|
||||
mod test;
|
||||
mod web_offline;
|
||||
mod web_offline_test;
|
||||
@@ -46,7 +47,10 @@ commands:
|
||||
into target/book
|
||||
licences [--check]
|
||||
write THIRD-PARTY-LICENSES.txt (the licences of every shipped crate, embedded
|
||||
in the programs); --check fails when it is out of date
|
||||
in the programs); --check only reports when it is out of date: pull requests
|
||||
leave it to the Generated files workflow on main
|
||||
lists [--fix] check that files many changes add to are sorted one item per line (the
|
||||
message catalogs in assets/locales); --fix sorts them
|
||||
test [CARGO ARGS] [-- TEST ARGS]
|
||||
build the tests with the ci profile, then run every test binary at once
|
||||
(and the doc tests alongside)
|
||||
@@ -57,7 +61,7 @@ commands:
|
||||
web-offline-test [--site DIR]
|
||||
check in headless Chrome that the web app works offline and updates to a new
|
||||
build (a stand-in app, or the web build in DIR such as dist/web)
|
||||
ci fmt --check, assets, licences --check, layers, clippy -D warnings, then the tests
|
||||
ci fmt --check, assets, licences --check, lists, layers, clippy -D warnings, then the tests
|
||||
with the wasm check alongside, then web-offline-test when Chrome is installed
|
||||
(stops at the first failing step)
|
||||
";
|
||||
@@ -78,6 +82,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
Some("book") => book::run(&root(), !rest.contains(&"--no-build")),
|
||||
Some("licences") => licences::run(&root(), rest.contains(&"--check")),
|
||||
Some("lists") => lists::run(&root(), rest.contains(&"--fix")),
|
||||
Some("test") => {
|
||||
let (cargo_args, test_args) = rest.iter().position(|a| *a == "--").map_or((&rest[..], &[][..]), |i| (&rest[..i], &rest[i + 1..]));
|
||||
test::run(cargo_args, test_args, vec![test::doc_tests(cargo_args, test_args)])
|
||||
@@ -358,6 +363,7 @@ fn cmd_ci() -> Result<(), String> {
|
||||
run(c, "cargo fmt --check")?;
|
||||
assets::run(&root())?;
|
||||
licences::run(&root(), true)?;
|
||||
lists::run(&root(), false)?;
|
||||
cmd_layers()?;
|
||||
let mut c = cargo();
|
||||
c.args(["clippy", "--workspace", "--all-targets", "--profile", "ci", "--", "-D", "warnings"]);
|
||||
|
||||
Reference in new issue
Block a user