Imported from leynier/alera (
.github/AGENTS.md). Install upstream withnpx skills add leynier/alera --skill .github. Copyright stays with the author.
AGENTS
Scope
This file applies to GitHub metadata and GitHub Actions workflows.
Workflow Policy
- Release jobs must be reproducible from a clean checkout.
- Build jobs must run on native runners for their platform.
- The shared job prologue lives in the composite action
.github/actions/setup-flutter-workspace. Jobs must consume it rather than repeating the steps inline, so the sequence stays identical acrosspr.yml,desktop-build.yml,release-cut.yml, andwarm-cache.yml.actions/checkoutstays in the workflow, because a local composite action cannot be resolved before the repository is on disk. - Any job that builds the desktop app or runs the Linux desktop integration tests must set up the pinned Rust toolchain (matching
rust/rust-toolchain.toml) and the Rust build cache before the Flutter build, because the native build hooks compile the Rustaleraterminal-host sidecar viacargo build --locked. Passrust: 'true'to the composite action, which orders the steps correctly. - Static analysis and root Flutter test jobs do not build the Linux desktop app and must pass
linux-toolchain: 'false'to the composite action. Test jobs must still keep the native asset setup and preflight enabled. - Flutter jobs that can run native asset hooks must set up Zig 0.16.0, restore the native asset cache, apply the temporary native asset CI workarounds, and run the native asset preflight before longer test/build commands. This keeps clean runners resilient when
ghostty_vte, PDFium, orportable_ptyprebuilt downloads are missing or transiently unavailable. This is the composite action'snative-assets/preflightdefault. - Workflows must check out with
submodules: falseand initialize only the submodules the job builds against, via.github/actions/init-required-submodules, before dependency resolution. The eightreference_projects/*submodules are contributor reading material excluded from analysis, and cloning them costs 649 MB per job. The initialization must stay--recursive, becausethird_party/dart_terminalnests theghosttysubmodule and the native asset cache key hashes a file inside it: without--recursive,hashFiles()does not fail, it silently hashes a different set. - The native asset cache key must stay keyed only on
runner.os,runner.arch, and the hashed inputs. Namespacing it per job or per platform produces several copies of byte-identical content that compete for the repository cache budget. - The Flutter version must stay pinned in the composite action rather than floating on
channel: stable, so a Flutter release does not invalidate the SDK cache for every job on every platform at once, including the warm cache onmain. - Mobile Android CI copies
.github/gradle/gradle_plugin_repos.init.gradleinto~/.gradle/init.dbeforeflutter build apk. Flutter's includedflutter_tools/gradlebuild has nopluginManagementblock, so a cold runner only searches the plugin portal for that classpath; the portal 303s Kotlin artifacts to Maven Central and then fails to parsekotlin-gradle-plugins-bom. Do not patch$FLUTTER_ROOTfor this: the Flutter SDK cache would pick up the mutation. - Rust compiler outputs use
sccache, installed by.github/actions/setup-rust-sccache. Trusted warm-cache, release, and pull-request rust jobs may connect it to a dedicated Cloudflare R2 bucket through theSCCACHE_R2_ACCOUNT_IDandSCCACHE_R2_BUCKETrepository variables and theSCCACHE_R2_ACCESS_KEY_IDandSCCACHE_R2_SECRET_ACCESS_KEYrepository secrets. Those credentials must have access only to the compiler-cache bucket and must never reuse thealera-updatesbucket or its production credentials. Fork pull requests, incomplete configurations, and R2 authentication failures fall back to the runner-local cache so cache availability cannot block a build. - Cargokit sets
CARGOKIT_TEMP_DIRunder the Flutter build directory, so the fully linkedalera_nativeandcode_forgeoutputs underbuild/**/cargokit_buildkeep a separateactions/cacheentry. Onlywarm-cache.ymlmay save that entry, so pull requests and release jobs restore a shared copy instead of writing private copies into their own ref scopes. Windows CI must install the pinned LunarG Vulkan SDK (1.4.350.0) from the official installer and verify its SHA-256. Do not usewinget: GitHub-hosted runners fail withAPPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING(-1978335217). Windows Whisper/Vulkan jobs map scratch toR:\cwith native cargo--target-diratR:\c\n(notR:\c\alera_nativeorR:\alera-cargokit: those prefixes make vulkan-shaders-gen's nested TryCompile object exceedMAX_PATHandcl.exereportsC1083with an empty generated-file name), setCMAKE_GENERATOR=Ninjaso nested vulkan-shaders-gen cmake inherits Ninja, set_CL_=/Z7 /FSbecausecl.exeignoresCFLAGS, and setGGML_CCACHE=OFFso ggml does not wrapcl.exewith the rustc sccache (that pairing drops.objfiles under Ninja). warm-cache.ymlexists to populate themainCargokit cache scope and the shared R2 compiler cache. It must keep using the same composite actions as the pull request and release jobs, so configuration cannot drift. It must keep running the real release build, since a prologue-only warm job would leave Cargokit cold, and it must keep the Linuxwarm-rustjob, since that release build's rustc command line does not match PR clippy/test.- Every workflow must declare an explicit
permissionsblock. The repository default grants the wholeGITHUB_TOKENscope set to jobs that only read the checkout, so a compromised action or dependency inherits write access it never needed. Workflows that build, analyze, or test declarecontents: readat the workflow level; a job that genuinely writes something adds the single scope it needs at the job level, ascloud-deploy.ymlandrelease-cut.ymldo. CodeQL'sactions/missing-workflow-permissionsrule reports a workflow that omits it. cache-cleanup.ymldeletes therefs/pull/N/mergeActions caches when a pull request closes. It must keepactions: writeas its only write permission and must not check out or execute pull request code.- The Flutter test job is sharded by test file through
tool/ci/select_test_shard.dart, never throughflutter test --total-shards/--shard-index. That flag slices the tests inside each suite after the suite has been loaded, so every shard would still compile every test file and the split would buy nothing. Shards are packed by file size rather than round-robin so large widget suites cannot land on one runner. TheTEST_SHARDSworkflow env, the number of shard entries in the matrix, and the--expect-inputsargument of the coverage gate must stay in agreement, otherwise a lost shard silently shrinks the totals. pr.ymlstartsrust testimmediately, then achangesjob that runs.github/actions/select-ci-jobs(tool/ci/select_ci_jobs.dart) against the pull request file list. That composite must passproblem-matcher: falsetodart-lang/setup-dart; the default matcher resolvesdart-analyzer.jsonagainst the composite directory and 404s. Docs-only, landing-only, and cloud-only changes skip Flutter tests, generation, packages, mobile, and Rust cargo; unknown paths fail open and enable every job.workflow_dispatchalways enables every job.rust testreuses that selector as its first step and skips cargo when Rust is not required, so the create→done critical path does not wait forchangesor the Flutter fan-out.pr-readyis the required check: it treats skipped optional jobs as success, butchanges,static, andrust testmust succeed. Do not putpaths:on the workflow itself, because a skippedpr-readywould block merge. Do not collect a second create→done sample by re-running a successful PR Checks run: GitHub keeps the originalcreated_at, so attempt 2 inflates the wall. Dispatch a second run instead.- The pull request rust checks use
.github/actions/setup-rust-checksas two jobs inpr.yml(rust testandrust clippy). clippy-driver and rustc do not share rustc artifacts, so a sequential clippy-then-test job compiled the workspace twice and was the critical path of PR Checks;cargo test --workspacespends almost all of that step compiling.rust testhas noneeds:so it starts at workflow create;rust clippystill waits forchangesso it does not take the first free runner from that compile. The composite owns the cargo flags,CMAKE_GENERATOR,RUSTFLAGS, andCARGO_PROFILE_DEV_DEBUGsowarm-cache.yml's Linuxwarm-rustjob can populate matching R2 keys. The Flutter release build in the warm matrix does not: release and clippy-driver command lines miss. Do not add a third rust leg; the extra runner setup would cost more than it buys back. Do not putneeds: changesback onrust test: the post-filter queue was the create→done gap on #656 / #676.tool/ci/run_rust_workspace_tests.shkeeps--workspaceon every cargo test so feature unification stays identical, then runsorchestration_review_regressionswith--test-threads=1because that binary drives real PTYs and flakes under cargo's default parallel test-target scheduling. Do not follow--no-runwith--excludeor-p alera-cli: those package filters relink shared crates (~3 minutes each on PR Checks). Previous-host conformance downloads the pinnedv0.49.0published runtime tarball (SHA-256 pinned intool/ci/host_compatibility.sh) instead of rebuilding that tag from source, which was ~5.5 minutes on the rust-test critical path. Assert the crate version and commit that tarball actually stamps (0.1.0/17a183f5), not the product tag. - The desktop E2E job runs one
flutter testinvocation per file inintegration_test/, never the directory. A directory run launches the app once and then fails to start it again for the next suite ("Unable to start the app on the device"), so the second and later files can never pass. - The Linux startup performance measurement lives in
startup-performance.ymlonmain, not in the pull request workflows. It launches the real app three times under xvfb, which made it the critical path ofpr.ymlwhile gating nothing, since it is invoked without--enforce-budgetand its step iscontinue-on-error. Software rendering under xvfb on a shared runner also makes the raster metrics too noisy to read per pull request. If it is ever meant to gate, it needs--enforce-budgetand a budget calibrated for xvfb first. - Pull request workflows must not rerun solely because a draft pull request becomes ready for review; the existing checks for the same commit remain authoritative.
- Pull request workflows must declare a
concurrencygroup keyed by${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}withcancel-in-progress: true, so a new commit cancels the superseded run instead of leaving it to saturate the shared runner pool.release-cut.ymlis deliberately excluded: it keeps its ownrelease-cutgroup withcancel-in-progress: false, because cancelling mid-publish would expose a partial release. - Do not expose partial releases to users.
cloud-deploy.ymlis the only normal production path forcloud/,edge/, andinfra/production/. Its third-party actions must stay pinned to immutable commit SHAs, its Google authentication must remain keyless through WIF, and only itscloud-productiondeployment job may read the Cloudflare tokens. Production plans containing deletes or replacements must fail before apply, and post-apply verification failures must attempt the documented Cloud Run and Worker rollback.release-cut.ymlis the single manual release entry point. It must plan desktop and mobile independently, skip unchanged products, and preserve their separate version and tag sequences.- Desktop release builds are split into native application builds, one cross-architecture runtime build per platform, and a deterministic runtime packaging job. The native application jobs reuse the unsigned sidecar and helper bundle already produced inside the app before platform signing. The packaging job must receive exactly one helper bundle per platform and both x64 and arm64 binaries, preserve executable modes through tarred intermediate artifacts, and finish before
publishcan run. - Release automation must publish drafts first, verify required assets and update manifests, then publish public releases.
- Release notes must be product-scoped via
tool/release/generate_release_notes.dart(desktop excludesmobile/andlanding/; mobile includes onlymobile/), and only desktop stable releases may carry the Latest badge. - Release workflows must not push release commits or tags until platform artifacts and update manifests have been generated and verified.
- Update indexes must be deployed only after the corresponding GitHub Release is public.
- Stable and release-candidate update indexes live under separate channel paths. Publishing one channel must not overwrite or delete the other channel.
- Stable release jobs must not enable automatic installation until the signed descriptor trust path is configured for the relevant platform.
- Release jobs must use schema v3 indexes, sign every platform
release.json, and verify descriptor signatures plus artifact hashes before upload. - Linux release jobs must publish signed APT and RPM repository metadata when Linux packages are included.
- The desktop package managers (
publish_packages,publish_chocolatey) run only on stable cuts and only after the GitHub Release is public, because every manifest they push points at that release's assets. A failure there must not roll the release back: the release is already correct and the manifest can be pushed by hand. Release candidates never reach them, for the same reason they never reach the stable Linux repositories. Both jobs must skip with a warning when their secret is absent rather than fail, mirroring how the Apple and Windows signing steps treat theirs. Each destination repository is written with its own SSH deploy key (ALERA_HOMEBREW_TAP_DEPLOY_KEY,ALERA_SCOOP_BUCKET_DEPLOY_KEY) rather than one account-wide token, so a key that leaks or needs rotating reaches nothing else, and the step pins GitHub's host keys fromapi.github.com/metainstead of accepting them on first use: a runner that trusted a forged host key would push the manifest elsewhere and still report success. - Signing secrets must be scoped only to release jobs that need them.
Issue And PR Policy
- Issue templates should collect platform details when behavior may differ across macOS, Windows, and Linux.
- Pull request templates should require validation notes, platform notes, and security/update risk notes when relevant.
- Workflow-created labels must be created idempotently when they may not exist in a fresh public repository.