@ifi/oh-pi-skills 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/skills/flutter-serverpod-mvp/SKILL.md +199 -0
  3. package/skills/rust-workspace-bootstrap/SKILL.md +86 -0
  4. package/skills/rust-workspace-bootstrap/scaffold.js +222 -0
  5. package/skills/rust-workspace-bootstrap/template/.cargo/config.toml +7 -0
  6. package/skills/rust-workspace-bootstrap/template/.changeset/.gitkeep +0 -0
  7. package/skills/rust-workspace-bootstrap/template/.envrc +7 -0
  8. package/skills/rust-workspace-bootstrap/template/.github/actions/devenv/action.yml +42 -0
  9. package/skills/rust-workspace-bootstrap/template/.github/workflows/ci.yml +130 -0
  10. package/skills/rust-workspace-bootstrap/template/.github/workflows/coverage.yml +45 -0
  11. package/skills/rust-workspace-bootstrap/template/.github/workflows/docs-pages.yml +60 -0
  12. package/skills/rust-workspace-bootstrap/template/.github/workflows/release-preview.yml +52 -0
  13. package/skills/rust-workspace-bootstrap/template/.github/workflows/release.yml +87 -0
  14. package/skills/rust-workspace-bootstrap/template/.github/workflows/semver.yml +63 -0
  15. package/skills/rust-workspace-bootstrap/template/Cargo.toml +64 -0
  16. package/skills/rust-workspace-bootstrap/template/changelog.md +3 -0
  17. package/skills/rust-workspace-bootstrap/template/clippy.toml +1 -0
  18. package/skills/rust-workspace-bootstrap/template/crates/__CLI_CRATE__/Cargo.toml +20 -0
  19. package/skills/rust-workspace-bootstrap/template/crates/__CLI_CRATE__/src/main.rs +21 -0
  20. package/skills/rust-workspace-bootstrap/template/crates/__CORE_CRATE__/Cargo.toml +15 -0
  21. package/skills/rust-workspace-bootstrap/template/crates/__CORE_CRATE__/src/lib.rs +32 -0
  22. package/skills/rust-workspace-bootstrap/template/deny.toml +18 -0
  23. package/skills/rust-workspace-bootstrap/template/devenv.nix +214 -0
  24. package/skills/rust-workspace-bootstrap/template/devenv.yaml +10 -0
  25. package/skills/rust-workspace-bootstrap/template/docs/book.toml +6 -0
  26. package/skills/rust-workspace-bootstrap/template/docs/src/SUMMARY.md +3 -0
  27. package/skills/rust-workspace-bootstrap/template/docs/src/index.md +3 -0
  28. package/skills/rust-workspace-bootstrap/template/dprint.json +40 -0
  29. package/skills/rust-workspace-bootstrap/template/knope.toml +73 -0
  30. package/skills/rust-workspace-bootstrap/template/readme.md +77 -0
  31. package/skills/rust-workspace-bootstrap/template/rust-toolchain.toml +4 -0
  32. package/skills/rust-workspace-bootstrap/template/rustfmt.toml +21 -0
  33. package/skills/rust-workspace-bootstrap/template/scripts/release.sh +61 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifi/oh-pi-skills",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "On-demand skill packs for pi: web-search, debug-helper, git-workflow, and more.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: flutter-serverpod-mvp
3
+ description:
4
+ Scaffold and evolve full-stack Flutter + Serverpod MVPs using devenv, Riverpod + Hooks,
5
+ strict i18n, and GoRouter shell routing patterns inspired by OpenBudget.
6
+ ---
7
+
8
+ # Flutter + Serverpod MVP (OpenBudget-style)
9
+
10
+ Use this skill when the user wants to:
11
+
12
+ - start a **new full-stack Flutter + Serverpod project**,
13
+ - add a major feature to an existing Flutter + Serverpod monorepo,
14
+ - enforce OpenBudget-style rules for **devenv**, **Riverpod + Hooks**, **i18n**, and **routing**.
15
+
16
+ ## Core Standards (non-negotiable)
17
+
18
+ These conventions are extracted from the OpenBudget setup and should be applied by default.
19
+
20
+ 1. **Workspace-first monorepo**
21
+ - Keep app/server/shared modules in one workspace.
22
+ - Use `melos` scripts from the workspace root.
23
+
24
+ 2. **Hooks + Riverpod architecture**
25
+ - Use `HookConsumerWidget` for widgets that read providers.
26
+ - Use `HookWidget` for widgets without provider reads.
27
+ - Prefer custom hooks (`use*`) over ad-hoc widget state.
28
+ - Use `@riverpod`/`riverpod_annotation` providers, not manual legacy provider styles.
29
+
30
+ 3. **Serverpod full-stack flow**
31
+ - Keep API/domain logic in `server/` with endpoint + service pattern.
32
+ - Generate protocol/client code whenever models/endpoints change.
33
+ - App calls Serverpod via a dedicated `serverpodClientProvider`.
34
+
35
+ 4. **Strict i18n discipline**
36
+ - No hardcoded user-facing strings in UI.
37
+ - Use ARB + generated `AppLocalizations` API.
38
+ - Add and run a hardcoded text checker (OpenBudget-style `tools/check_localized_ui_text.dart`).
39
+
40
+ 5. **Structured GoRouter routing**
41
+ - Route constants in `route_names.dart`.
42
+ - Router provider in `app_router.dart` via `@riverpod`.
43
+ - Auth redirect in a pure/testable helper function.
44
+ - Use `StatefulShellRoute.indexedStack` for bottom-tab apps with separate navigator stacks.
45
+
46
+ 6. **devenv as the operational entrypoint**
47
+ - Local infra (Postgres/Redis) + scripts + process logs under devenv.
48
+ - `devenv up` should bring up backend dependencies and server.
49
+
50
+ ## Recommended Project Layout
51
+
52
+ ```text
53
+ <project>/
54
+ ├── app/ # Flutter app
55
+ ├── server/ # Serverpod backend
56
+ ├── client/ # Generated serverpod protocol client
57
+ ├── core/ # Shared Dart models/utilities (no Flutter)
58
+ ├── ui/ # Shared Flutter UI package
59
+ ├── lints/ # Centralized analyzer/lint config (optional but recommended)
60
+ ├── test_utils/ # Shared test helpers
61
+ ├── tools/ # Scripts (e.g. l10n hardcoded text check)
62
+ ├── pubspec.yaml # Workspace root + melos config
63
+ ├── devenv.nix # Development environment and scripts
64
+ ├── devenv.yaml # devenv inputs
65
+ └── .github/workflows/ci.yml
66
+ ```
67
+
68
+ ## Bootstrap Workflow for a New MVP
69
+
70
+ When asked to create a new project, execute this order:
71
+
72
+ 1. **Collect project inputs**
73
+ - Project name, organization ID, app bundle IDs, default flavor, API hostnames.
74
+
75
+ 2. **Scaffold workspace root**
76
+ - Root `pubspec.yaml` with Dart workspace members.
77
+ - `melos` scripts for analyze/test/generate/serverpod generation.
78
+
79
+ 3. **Scaffold Serverpod package**
80
+ - `server/pubspec.yaml` with `serverpod` dependencies.
81
+ - `server/config/{development,test,production}.yaml`.
82
+ - `server/config/generator.yaml` pointing to `../client`.
83
+ - Initial endpoint/service files.
84
+
85
+ 4. **Scaffold Flutter app package**
86
+ - Dependencies: `hooks_riverpod`, `flutter_hooks`, `go_router`,
87
+ `riverpod_annotation`, `serverpod_flutter`, auth package, localization deps.
88
+ - `l10n.yaml` configured with generated output directory.
89
+ - app bootstrap with `MaterialApp.router`, localization delegates, and router provider.
90
+
91
+ 5. **Wire full-stack client access**
92
+ - Add `serverpodClientProvider` with environment override support.
93
+ - Ensure test runtime handling for connectivity monitor.
94
+
95
+ 6. **Add i18n and routing foundations**
96
+ - `lib/l10n/app_en.arb` + generated localization output path.
97
+ - `route_names.dart` for all route/path constants.
98
+ - `app_router.dart` with auth redirects + shell routing where needed.
99
+
100
+ 7. **Add devenv and CI**
101
+ - `devenv.nix` with scripts for lint/test/generate/server start.
102
+ - `devenv` GitHub composite action + CI jobs for lint/test/server/integration.
103
+
104
+ 8. **Create first vertical slice**
105
+ - Auth + one core domain flow (e.g. projects/tasks/budgets).
106
+ - Endpoint → service → provider → screen.
107
+ - Unit/widget/integration tests for the slice.
108
+
109
+ ## i18n Rules (OpenBudget pattern)
110
+
111
+ - Keep ARB files under `app/lib/l10n/`.
112
+ - Keep generated localization files under `app/lib/l10n/generated/`.
113
+ - Use `AppLocalizations.of(context)` for all visible text.
114
+ - Add a hardcoded-string checker script and run it in CI (`lint:l10n`).
115
+ - Only allow explicit opt-outs with inline comment markers (for rare cases).
116
+
117
+ Minimal `l10n.yaml` baseline:
118
+
119
+ ```yaml
120
+ arb-dir: lib/l10n
121
+ template-arb-file: app_en.arb
122
+ output-localization-file: app_localizations.dart
123
+ output-dir: lib/l10n/generated
124
+ output-class: AppLocalizations
125
+ nullable-getter: false
126
+ ```
127
+
128
+ ## Routing Rules (OpenBudget pattern)
129
+
130
+ - Keep route names and paths in one constants file.
131
+ - Keep router creation inside a provider (`@riverpod GoRouter appRouter(Ref ref)`).
132
+ - Keep redirect logic in a separate helper for easy unit tests.
133
+ - For tabbed apps, use `StatefulShellRoute.indexedStack` with one navigator key per tab.
134
+ - Keep non-tab overlays outside shell branches.
135
+
136
+ ## Provider + UI Rules
137
+
138
+ - Use feature-oriented modules.
139
+ - Follow flow: **endpoint/service (server)** → **client call** → **provider** → **UI widget**.
140
+ - Keep side effects in providers/services, not in build methods.
141
+ - Handle async UI with `AsyncValue.when` or pattern matching.
142
+
143
+ ## devenv + Commands Contract
144
+
145
+ Include scripts equivalent to these responsibilities:
146
+
147
+ - `server:start`
148
+ - `runner:build`
149
+ - `runner:serverpod`
150
+ - `lint:analyze`
151
+ - `lint:l10n`
152
+ - `lint:all`
153
+ - `test:flutter`
154
+ - `test:all`
155
+
156
+ Target dev loop:
157
+
158
+ ```bash
159
+ devenv shell
160
+ dart pub get
161
+ runner:build
162
+ runner:serverpod
163
+ devenv up
164
+ ```
165
+
166
+ ## CI Contract
167
+
168
+ At minimum, CI should run:
169
+
170
+ 1. lint job (`lint:all`)
171
+ 2. test job (Flutter + Dart/server tests)
172
+ 3. server job with Postgres/Redis services
173
+ 4. integration job (if integration tests exist)
174
+
175
+ Use a reusable setup action to install Nix/devenv, cache pub deps, and install Flutter via FVM.
176
+
177
+ ## Feature Delivery Checklist (for agents)
178
+
179
+ Before marking work complete:
180
+
181
+ - [ ] Codegen run (Riverpod/build_runner + Serverpod)
182
+ - [ ] Generated files committed
183
+ - [ ] No hardcoded UI text
184
+ - [ ] New routes declared in `route_names.dart`
185
+ - [ ] Router/auth redirect tests updated
186
+ - [ ] Provider tests + widget tests added/updated
187
+ - [ ] Server endpoint/service tests added/updated
188
+ - [ ] `lint:all` and relevant tests passing
189
+
190
+ ## What to produce when user asks for a "new MVP"
191
+
192
+ Always produce:
193
+
194
+ 1. full folder skeleton,
195
+ 2. root workspace config (`pubspec.yaml` + melos scripts),
196
+ 3. app bootstrap + router + localization scaffolding,
197
+ 4. serverpod config + initial endpoint/service,
198
+ 5. devenv + CI baseline,
199
+ 6. one implemented end-to-end feature slice with tests.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: rust-workspace-bootstrap
3
+ description:
4
+ Scaffold a production-ready Rust workspace with knope changesets, devenv, and GitHub Actions CI/release workflows. Use when starting a new Rust project or monorepo.
5
+ ---
6
+
7
+ # Rust Workspace Bootstrap
8
+
9
+ Generate a Rust workspace template inspired by the release/devenv/workflow structure used in
10
+ `mdt` and `pina`.
11
+
12
+ ## What it scaffolds
13
+
14
+ - Cargo workspace with `core` + `cli` crates
15
+ - `knope.toml` with:
16
+ - `document-change`
17
+ - `release`
18
+ - `forced-release`
19
+ - `publish`
20
+ - `.changeset/` folder for change files
21
+ - `devenv.nix`, `devenv.yaml`, `.envrc`
22
+ - GitHub Actions:
23
+ - CI
24
+ - coverage
25
+ - semver checks
26
+ - release preview
27
+ - release assets
28
+ - docs-pages deployment
29
+ - Opinionated defaults for `rustfmt`, `clippy`, `deny`, `dprint`
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ # Minimal
35
+ {baseDir}/scaffold.js --name acme-tool
36
+
37
+ # Recommended (in a separate worktree)
38
+ git worktree add ../acme-tool -b feat/bootstrap-acme-tool
39
+ cd ../acme-tool
40
+ /path/to/rust-workspace-bootstrap/scaffold.js \
41
+ --name acme-tool \
42
+ --owner your-github-org \
43
+ --repo acme-tool \
44
+ --description "CLI + core Rust workspace"
45
+ ```
46
+
47
+ ## Options
48
+
49
+ - `--name <kebab-case>` (required)
50
+ - `--dir <path>` (optional, default: `./<name>`)
51
+ - `--owner <github-owner>` (optional, default: `your-github-org`)
52
+ - `--repo <github-repo>` (optional, default: `<name>`)
53
+ - `--description <text>` (optional)
54
+ - `--force` (optional, allow writing into non-empty directory)
55
+
56
+ ## Rules
57
+
58
+ - Always use `_` (underscore) separators in Rust crate names.
59
+ - Do **not** use `-` in crate package names. Example: `acme_tool_core`, not `acme-tool-core`.
60
+
61
+ ## After scaffolding
62
+
63
+ ```bash
64
+ cd <project>
65
+
66
+ direnv allow
67
+ # or
68
+ # devenv shell
69
+
70
+ install:cargo:bin
71
+ build:all
72
+ lint:all
73
+ test:all
74
+ ```
75
+
76
+ Create your first changeset:
77
+
78
+ ```bash
79
+ knope document-change
80
+ ```
81
+
82
+ Dry-run a release:
83
+
84
+ ```bash
85
+ knope release --dry-run
86
+ ```
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ function printUsage() {
9
+ console.log(`
10
+ Rust Workspace Bootstrap
11
+
12
+ Usage:
13
+ scaffold.js --name <project-name> [options]
14
+
15
+ Options:
16
+ --name <kebab-case> Required project name.
17
+ --dir <path> Target directory (default: ./<name>).
18
+ --owner <github-owner> GitHub org/user (default: your-github-org).
19
+ --repo <github-repo> GitHub repo name (default: <name>).
20
+ --description <text> Workspace description.
21
+ --force Allow writing into a non-empty target directory.
22
+ --help Show this help.
23
+ `);
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const options = {
28
+ name: "",
29
+ dir: "",
30
+ owner: "your-github-org",
31
+ repo: "",
32
+ description: "",
33
+ force: false,
34
+ help: false,
35
+ };
36
+
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const arg = argv[index];
39
+ if (arg === "--help" || arg === "-h") {
40
+ options.help = true;
41
+ continue;
42
+ }
43
+ if (arg === "--force") {
44
+ options.force = true;
45
+ continue;
46
+ }
47
+
48
+ if (arg.startsWith("--")) {
49
+ const key = arg.slice(2);
50
+ const value = argv[index + 1];
51
+ if (!value || value.startsWith("--")) {
52
+ throw new Error(`Missing value for option: ${arg}`);
53
+ }
54
+ index += 1;
55
+
56
+ switch (key) {
57
+ case "name":
58
+ options.name = value;
59
+ break;
60
+ case "dir":
61
+ options.dir = value;
62
+ break;
63
+ case "owner":
64
+ options.owner = value;
65
+ break;
66
+ case "repo":
67
+ options.repo = value;
68
+ break;
69
+ case "description":
70
+ options.description = value;
71
+ break;
72
+ default:
73
+ throw new Error(`Unknown option: ${arg}`);
74
+ }
75
+ continue;
76
+ }
77
+
78
+ if (!options.name) {
79
+ options.name = arg;
80
+ }
81
+ }
82
+
83
+ return options;
84
+ }
85
+
86
+ function toTitleCase(value) {
87
+ return value
88
+ .split("-")
89
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
90
+ .join(" ");
91
+ }
92
+
93
+ function collectFiles(rootDir, currentDir = rootDir) {
94
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
95
+ const files = [];
96
+
97
+ for (const entry of entries) {
98
+ const absolutePath = path.join(currentDir, entry.name);
99
+ if (entry.isDirectory()) {
100
+ files.push(...collectFiles(rootDir, absolutePath));
101
+ continue;
102
+ }
103
+ if (entry.isFile()) {
104
+ files.push(path.relative(rootDir, absolutePath));
105
+ }
106
+ }
107
+
108
+ return files;
109
+ }
110
+
111
+ function applyTokens(input, tokens) {
112
+ let output = input;
113
+ for (const [token, value] of Object.entries(tokens)) {
114
+ output = output.split(token).join(value);
115
+ }
116
+ return output;
117
+ }
118
+
119
+ function ensureTargetDirectory(targetDir, force) {
120
+ if (!fs.existsSync(targetDir)) {
121
+ fs.mkdirSync(targetDir, { recursive: true });
122
+ return;
123
+ }
124
+
125
+ const currentFiles = fs.readdirSync(targetDir);
126
+ if (currentFiles.length > 0 && !force) {
127
+ throw new Error(
128
+ `Target directory is not empty: ${targetDir}\nUse --force if you want to scaffold into an existing directory.`,
129
+ );
130
+ }
131
+ }
132
+
133
+ function main() {
134
+ const options = parseArgs(process.argv.slice(2));
135
+ if (options.help) {
136
+ printUsage();
137
+ return;
138
+ }
139
+
140
+ if (!options.name) {
141
+ printUsage();
142
+ throw new Error("Project name is required. Pass --name <project-name>.");
143
+ }
144
+
145
+ if (!/^[a-z][a-z0-9-]*$/.test(options.name)) {
146
+ throw new Error("Project name must be kebab-case (letters, numbers, dashes). Example: acme-tool");
147
+ }
148
+
149
+ const projectName = options.name;
150
+ const projectTitle = toTitleCase(projectName);
151
+ const cratePrefix = projectName.replace(/-/g, "_");
152
+ const coreCrate = `${cratePrefix}_core`;
153
+ const cliCrate = `${cratePrefix}_cli`;
154
+ const targetDir = path.resolve(options.dir || projectName);
155
+ const owner = options.owner;
156
+ const repo = options.repo || projectName;
157
+ const description = options.description || `${projectTitle} Rust workspace`;
158
+
159
+ const tokens = {
160
+ "__PROJECT_NAME__": projectName,
161
+ "__PROJECT_TITLE__": projectTitle,
162
+ "__CORE_CRATE__": coreCrate,
163
+ "__CLI_CRATE__": cliCrate,
164
+ "__GITHUB_OWNER__": owner,
165
+ "__GITHUB_REPO__": repo,
166
+ "__DESCRIPTION__": description,
167
+ };
168
+
169
+ const currentFile = fileURLToPath(import.meta.url);
170
+ const baseDir = path.dirname(currentFile);
171
+ const templateDir = path.join(baseDir, "template");
172
+
173
+ if (!fs.existsSync(templateDir)) {
174
+ throw new Error(`Template directory not found: ${templateDir}`);
175
+ }
176
+
177
+ ensureTargetDirectory(targetDir, options.force);
178
+
179
+ const templateFiles = collectFiles(templateDir);
180
+ const writtenFiles = [];
181
+
182
+ for (const relativeTemplatePath of templateFiles) {
183
+ const sourcePath = path.join(templateDir, relativeTemplatePath);
184
+ const targetRelativePath = applyTokens(relativeTemplatePath, tokens);
185
+ const destinationPath = path.join(targetDir, targetRelativePath);
186
+ const sourceContent = fs.readFileSync(sourcePath, "utf8");
187
+ const destinationContent = applyTokens(sourceContent, tokens);
188
+
189
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
190
+ fs.writeFileSync(destinationPath, destinationContent, "utf8");
191
+
192
+ if (destinationPath.endsWith(".sh")) {
193
+ fs.chmodSync(destinationPath, 0o755);
194
+ }
195
+
196
+ writtenFiles.push(targetRelativePath);
197
+ }
198
+
199
+ writtenFiles.sort();
200
+
201
+ console.log(`\n✅ Scaffolding complete: ${targetDir}`);
202
+ console.log(` Project: ${projectName}`);
203
+ console.log(` Core crate: ${coreCrate}`);
204
+ console.log(` CLI crate: ${cliCrate}`);
205
+ console.log(` GitHub repo: ${owner}/${repo}`);
206
+ console.log(` Files: ${writtenFiles.length}`);
207
+ console.log("\nNext steps:");
208
+ console.log(` cd ${targetDir}`);
209
+ console.log(" direnv allow # or: devenv shell");
210
+ console.log(" install:cargo:bin");
211
+ console.log(" lint:all && test:all && build:all");
212
+ console.log(" knope document-change");
213
+ console.log(" knope release --dry-run\n");
214
+ }
215
+
216
+ try {
217
+ main();
218
+ } catch (error) {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ console.error(`\n❌ ${message}\n`);
221
+ process.exit(1);
222
+ }
@@ -0,0 +1,7 @@
1
+ [alias]
2
+ deny = ["bin", "cargo-deny"]
3
+ insta = ["bin", "cargo-insta"]
4
+ llvm-cov = ["bin", "cargo-llvm-cov"]
5
+ nextest = ["bin", "cargo-nextest"]
6
+ semver-checks = ["bin", "cargo-semver-checks"]
7
+ workspaces = ["bin", "cargo-workspaces"]
@@ -0,0 +1,7 @@
1
+ export DIRENV_WARN_TIMEOUT=20s
2
+
3
+ eval "$(devenv direnvrc)"
4
+
5
+ # The use_devenv function supports passing flags to the devenv command
6
+ # For example: use devenv --impure --option services.postgres.enable:bool true
7
+ use devenv -c
@@ -0,0 +1,42 @@
1
+ name: devenv
2
+ description: Setup development environment with devenv
3
+ inputs:
4
+ github-token:
5
+ description: Provide a GitHub token
6
+ required: true
7
+ cache-version:
8
+ description: Cache version for dependency keys
9
+ required: false
10
+ default: "v1"
11
+
12
+ runs:
13
+ using: composite
14
+ steps:
15
+ - name: cache rust dependencies
16
+ uses: Swatinem/rust-cache@v2
17
+ with:
18
+ prefix-key: "${{ inputs.cache-version }}"
19
+
20
+ - name: cache cargo binaries
21
+ uses: actions/cache@v4
22
+ with:
23
+ path: ./.bin
24
+ key: ${{ runner.os }}-cargo-bin-${{ inputs.cache-version }}-${{ env.RUSTUP_TOOLCHAIN }}-${{ hashFiles('rust-toolchain.toml', 'Cargo.toml') }}
25
+ restore-keys: |
26
+ ${{ runner.os }}-cargo-bin-${{ inputs.cache-version }}-
27
+
28
+ - name: install nix
29
+ uses: cachix/install-nix-action@v31
30
+ with:
31
+ github_access_token: ${{ inputs.github-token }}
32
+
33
+ - name: setup nix environment
34
+ run: |
35
+ nix profile add --accept-flake-config nixpkgs#cachix
36
+ cachix use devenv
37
+ nix profile add nixpkgs#devenv
38
+ shell: bash
39
+
40
+ - name: install dependencies
41
+ run: install:cargo:bin
42
+ shell: devenv shell -- bash -e {0}
@@ -0,0 +1,130 @@
1
+ name: "ci"
2
+ permissions:
3
+ contents: read
4
+ on:
5
+ push:
6
+ branches:
7
+ - main
8
+ pull_request:
9
+ branches:
10
+ - main
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
15
+
16
+ jobs:
17
+ commit-lint:
18
+ timeout-minutes: 15
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: checkout repository
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: validate commit messages
27
+ run: |
28
+ PATTERN='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+'
29
+
30
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
31
+ BASE_SHA='${{ github.event.pull_request.base.sha }}'
32
+ HEAD_SHA='${{ github.event.pull_request.head.sha }}'
33
+ COMMITS=$(git log --format='%s' --no-merges "${BASE_SHA}..${HEAD_SHA}")
34
+ else
35
+ COMMITS=$(git log --format='%s' -1)
36
+ fi
37
+
38
+ FAILED=0
39
+ while IFS= read -r msg; do
40
+ [ -z "$msg" ] && continue
41
+ if ! echo "$msg" | grep -qE "$PATTERN"; then
42
+ echo "::error::Invalid commit message: '$msg'"
43
+ FAILED=1
44
+ fi
45
+ done <<< "$COMMITS"
46
+
47
+ if [ "$FAILED" -eq 1 ]; then
48
+ exit 1
49
+ fi
50
+
51
+ changeset:
52
+ timeout-minutes: 10
53
+ runs-on: ubuntu-latest
54
+ if: github.event_name == 'pull_request' || !startsWith(github.event.head_commit.message, 'chore: prepare releases')
55
+ steps:
56
+ - name: checkout repository
57
+ uses: actions/checkout@v4
58
+
59
+ - name: check for changeset
60
+ run: |
61
+ CHANGESETS=$(find .changeset -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')
62
+ if [ "$CHANGESETS" -eq 0 ]; then
63
+ echo "::error::No changeset found. Run 'knope document-change'."
64
+ exit 1
65
+ fi
66
+ echo "✅ Found $CHANGESETS changeset(s)"
67
+
68
+ lint:
69
+ timeout-minutes: 60
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - name: checkout repository
73
+ uses: actions/checkout@v4
74
+
75
+ - name: setup
76
+ uses: ./.github/actions/devenv
77
+ with:
78
+ github-token: ${{ secrets.GITHUB_TOKEN }}
79
+
80
+ - name: lint
81
+ run: lint:all
82
+ shell: devenv shell -c -- bash -e {0}
83
+
84
+ test:
85
+ timeout-minutes: 60
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - name: checkout repository
89
+ uses: actions/checkout@v4
90
+
91
+ - name: setup
92
+ uses: ./.github/actions/devenv
93
+ with:
94
+ github-token: ${{ secrets.GITHUB_TOKEN }}
95
+
96
+ - name: run tests
97
+ run: test:all
98
+ shell: devenv shell -c -- bash -e {0}
99
+
100
+ build:
101
+ timeout-minutes: 60
102
+ runs-on: ubuntu-latest
103
+ needs: [commit-lint, changeset, lint, test]
104
+ if: ${{ !failure() && !cancelled() }}
105
+ steps:
106
+ - name: checkout repository
107
+ uses: actions/checkout@v4
108
+
109
+ - name: setup
110
+ uses: ./.github/actions/devenv
111
+ with:
112
+ github-token: ${{ secrets.GITHUB_TOKEN }}
113
+
114
+ - name: build default features
115
+ run: build:default
116
+ shell: devenv shell -c -- bash -e {0}
117
+
118
+ - name: build all features
119
+ run: build:all
120
+ shell: devenv shell -c -- bash -e {0}
121
+
122
+ security:
123
+ timeout-minutes: 30
124
+ runs-on: ubuntu-latest
125
+ steps:
126
+ - name: checkout repository
127
+ uses: actions/checkout@v4
128
+
129
+ - name: run cargo-deny
130
+ uses: EmbarkStudios/cargo-deny-action@v2