Imported from Team-Haruki/Haruki-Sekai-API (
AGENTS.md). Install upstream withnpx skills add Team-Haruki/Haruki-Sekai-API. Copyright stays with the author.
AGENTS.md — AI Agent Guidelines for Haruki Sekai API
Project Summary
Haruki Sekai API is a Rust service that proxies encrypted API calls to regional game servers of "Project Sekai: Colorful Stage". It manages multi-region game accounts, auto-updates master data, and serves a REST API for HarukiBot.
Repository Structure
src/
main.rs – App bootstrap (config, server, graceful shutdown)
logging.rs – Tracing setup, custom console formatter (main-only module)
lib.rs – Public modules, AppState struct, RequestCoalescer (single-flight)
config.rs – YAML config structs with serde defaults
error.rs – AppError enum (thiserror), HTTP status mapping, IntoResponse
utils.rs – retry_async(), CachedResource<T>
ingest_engine.rs – Bulk JSON→DB ingestion using schema_info.json (the in-sync path);
`MasterSchema` (table map, file→table resolution) shared with ingest/
ingest/ – Registry-driven multi-target ingest (`master_ingest` binary):
mod.rs – Ingester: targets, coalesced per-region triggers, cron reconcile
run.rs – One region run: state diff, fetch+parse each changed file once,
fan-out to per-target writer tasks, checks, commit
target.rs – Per-target SQL: state tables, advisory lock, introspection,
mapping hash, staged upsert/delete of a file, raw rows
registry_client.rs – current / blob fetch with retries and streamed SHA-256 check
http.rs – /health, publish webhook, allow-shrink
upstream.rs – RegionRouter: multi-upstream routing with priority-ordered
targets (local client + remote Haruki nodes), circuit breakers
api/
routes.rs – Axum router (health, image, protected API, internal routes)
apis.rs – Handler functions (profile, system, ranking proxies)
middleware.rs – JWT auth middleware with Redis caching
image.rs – Image/blob proxies (MySekai, housing/profile-card
thumbnails, custom music score) via RegionRouter
internal.rs – Node-to-node /internal/* API (sekai-api relay, login probe,
game byte stream, master version/bundle, update webhook,
app-identity push); gated by backend.internal_token
client/
sekai_client.rs – Core game client (login, encrypted API calls, retry)
account.rs – CP and Nuverse account types, SekaiAccount trait
session.rs – AccountSession with per-account API locking
helper.rs – CookieHelper, VersionHelper, version comparison
token_utils.rs – JWT / Nuverse token user ID extraction
nuverse_schema.rs – Schema-bundle-driven array→dict restoration for Nuverse servers
housing.rs – MySekai housing thumbnail URL normalisation shared by the
housing competition API and thumbnail image routes
crypto/
sekai_cryptor.rs – AES-128-CBC encryption with MessagePack serialization
db.rs – init_db, init_master_db, init_registry_state_db,
init_registry_blob_table, init_redis
db/entity/ – SeaORM entities: sekai_user, sekai_user_server,
registry_state, registry_publish_history, registry_blob
updater/
scheduler.rs – Cron jobs: cookie refresh, master update (local or
remote-account), master sync poll; warns that the old
app-hash polling settings are ignored
master.rs – MasterUpdater: version check, download, git push, DB ingest
sync.rs – MasterSyncer: pull master bundles from an owner node
(webhook-triggered, cron fallback); records the last git
push outcome
git.rs – GitHelper: stage, commit, push by running the `git` CLI
(wall-clock timeout, kills the process group on expiry);
fetches before every push and refuses to commit when the
remote diverged
master_stream.rs – Table-by-table streaming decode of a downloaded master
payload (rows streamed one at a time)
prune.rs – Stale master file pruning after a complete dump/bundle:
protect list, ratio + absolute cap guards, producer-side
two-consecutive-dumps rule (pending set outside the worktree)
registry/
service.rs – Registry: per-region pull via MasterSyncer, git push,
ingest, per-region manifest publication
http.rs – Registry HTTP surface (pointers, digest-addressed blobs and
manifests, /health, app identity, subscriber fan-out)
metas.rs – music_metas feed (omakase rows injected), with gzip/zstd
copies precompressed once per blob
encoding.rs – Response compression (gzip/zstd per Accept-Encoding, not
bundles) and per-coding ETags (`"<tag>-gzip"`)
state.rs – Per-region registry state (manifests, snapshots, history,
app identity, metas pointers): JSON files, or a database
when `registry.state_dsn` is set (one-time file import)
blobs.rs – Master file content store (`registry.blob_store`): the
master directories (fs) or content-addressed zstd blobs in
`registry_blobs` (pg): import, GC, directory fallback
models/ – ~136 auto-generated game data model files (never hand-edit;
regenerate them from the source data)
bin/
run_ingest.rs – Standalone CLI for master data ingestion
master_registry.rs – Master data manager (registry), runs on the same config file
master_ingest.rs – Registry-driven ingest role (`ingest` config section)
bench_profile.rs – Per-stage latency benchmark for the profile proxy path
bench_stages.rs – Offline per-stage CPU / allocation profile of the proxy
path on a captured response body (no accounts needed)
tools/
ent_generator/ – Rust tool that reads src/models/ and generates:
- schema_info_generated.json (table→column mapping)
- ent_schemas/generated/*.go (EntGo schemas with table name annotations)
nuverse_schema_generator/ – C# (Mono.Cecil) tool that reads Assembly-CSharp.dll from an
Il2Cpp DummyDll dump and emits Data/structures/nuverse_schema_bundle.json
docs/
nuverse-schema-guide.md – Nuverse schema assets: layout, field naming, update workflow
master-registry-storage-and-ingest.md – registry content in PostgreSQL (blob store) and the
registry-driven multi-target ingest design
migrations/ – Hand-written, idempotent DDL for ingest targets that run with
`create_tables: false`; apply before shipping a schema_info.json
that adds tables
Data/registry/ – Default `registry.state_dir` (runtime, git-ignored): registry
JSON state (per-region manifests); music_metas blobs
Data/structures/ – Committed Nuverse schema assets (nuverse_schema_bundle.json,
*.avsc; versioned copies under 6.0.0/ and 6.4.0/)
tests/ingest_memory.rs – Peak-heap integration test of a registry ingest (PostgreSQL-gated)
tools/generate_mysekai_followups.py – Generates MySekai follow-up models from an il2cpp
dump (see docs/mysekai-followups.md); tested by CI (Python tools)
schema_info.json – Authoritative DB schema used by ingest engine
haruki-sekai-configs.example.yaml – Configuration template
Key Concepts
Server Regions & Protocols
- CP servers (Jp, En): Use cookies, JWT credentials,
PUTlogin, direct AES-encrypted responses - Nuverse servers (Tw, Kr, Cn): Use access tokens,
POSTlogin, CDN-based master data, flat array responses that need restoration - Branch with
ServerRegion::is_cp_server()
Data Flow (Game API Call)
RegionRouterpicks a target in ascending priority order (local client atlocal_priority, remote upstreams fromupstreams), skipping targets with an open circuit breaker- Local target: select account session (round-robin), serialize request body → MessagePack → AES-128-CBC encrypt, send HTTP with session token + headers
- Remote target: forward via the peer's
POST /internal/sekai-api(the peer executes on its local client only, so forwarding cannot loop) - Receive encrypted response → AES decrypt → MessagePack → JSON
- Return ordered JSON to API caller
Multi-Node Topology
backend.internal_tokengates all/internal/*node-to-node endpoints (Bearer auth; endpoints answer 404 when the token is unset)- Game API failover: per-region
upstreamslist of remote Haruki nodes; a region with upstreams butenabled: false(no local accounts) is served remote-only - Master sync: each region has one owner node running the master updater; peer
nodes configured with
master_sync.source_urlpull the owner's master bundle (MasterSyncer), triggered by the owner's/internal/master-updatedwebhook withpoll_cronas fallback. Unpacking skips files whose bytes are unchanged (their mtime stays, so git and digest caches stay valid); theproducedset still lists every bundle file, so pruning is unaffected.master_sync.compress_bundle: falseasks the owner for an uncompressed bundle (same host or network) - Remote-account master production: a node with
master_remote_source.urlruns the region's master pipeline locally but borrows a peer's accounts for the login probe and (CP) the encrypted master-split fetch, relayed as untouched bytes via/internal/game-stream - App identity is not polled on a node: it arrives via
POST /internal/app-identity(src/api/internal.rs) - appVersion is stored and sent exactly as pushed (the package manifest's
versionName, which the server accepts, patch releases included).servers.<region>.pin_app_version_patch_to_zerois an opt-in escape hatch, off by default for every region, that makes the region's appVersionmajor.minor.0everywhere it is stored or sent: the version file (persist_app_identity,persist_version_file), the login headers (SekaiClient::normalize_app_version), synced owner identities and the registry'sGET/PUT /v1/app/{region}. All go throughhelper::effective_app_version(pin, version); never zero a patch elsewhere
Master Data Pipeline
MasterUpdaterchecks game server for new data version- Downloads and decrypts master data (CP: split API; Nuverse: CDN + structure file)
- Saves JSON files directly into the region's
servers.<region>.master_dir - Optionally pushes to git repository
- Optionally ingests into PostgreSQL via
IngestionEngine IngestionEnginemaps JSON filenames → table names usingschema_info.json
- On an asset version change the updater first calls every
asset_updater_serversentry whoseregionslist is empty or contains the region (awaited before the master download). A 409 containing "is disabled" (node does not own the region) is not retried; other 409s mean a job is running and retryASSET_UPDATER_MAX_CONFLICT_RETRIESx at 60 s. The payload carriesapp_version, the appVersion the check logged in with (the account node's for a remote login), so an updater 7.3.0+ adopts a new Nuverse app version for the job it starts (after its own CDN probe); older updaters ignore the field - Downloaded payloads are decoded table by table with rows streamed (
master_stream.rs), so the producer's peak memory is bounded by one row rather than the whole payload. Keep new master consumers on that path — neverunpack_ordereda whole master - Ingestion streams row batches bounded by row count and size (
ROWS_PER_BATCH,BATCH_BYTESiningest_engine.rs: a few master files have ~1k rows of 25-50 KB each);master_database.ingest_concurrency/ingest.parse_concurrencybound its memory.tests/ingest_memory.rsasserts the peak heap of a large-file ingest (PostgreSQL-gated) - After a complete dump (every split decoded) or a complete bundle unpack,
*.jsonfiles the dump did not produce are deleted before ingest and git push (updater/prune.rs); a failed or partial run never prunes. The producer deletes only after two consecutive complete dumps miss a table; both sides honourBUILTIN_PROTECTED_TABLES+prune_protect,prune_min_ratioandprune_max_files. Keep the protect list in sync with consumers' required tables. Ingest leaves the DB rows of a pruned table in place
Master Registry
- The
master_registrybinary is the authoritative master data source other projects consume: it pulls each region from its owner viaMasterSyncer, owns git push and ingest, and publishes per-region manifests (contentHash+gitCommit). State lives inData/registry/JSON files, or in theregistry_state/registry_publish_historytables whenregistry.state_dsnis set (files imported once into empty tables, kept). Run exactly one registry per state database (import/publish are not multi-writer safe; current manifests, app identity and metas pointers are cached in memory and refreshed only by that instance's own writes). Tables are createdIF NOT EXISTS; there is no migration path, so a column change needs hand-written DDL. Rolling back to files: stop the registry, delete<state_dir>/manifests/*/current.json(startuppublish_missingskips regions that already have a current, which would serve the pre-DB manifest), clearstate_dsn, restart - Master file content is served from the master directories (
registry.blob_store: fs, default) or fromregistry_blobsin the state database (pg, requiresstate_dsn): zstd blobs keyed by SHA-256, stored before the manifest that lists them commits (the publish transaction checks), served decompressed with the same bytes and headers (Last-Modified= first stored).blob/then also serves retained snapshots' files;files/andbundlefollowcurrent. Existing manifests are imported in the background at startup (disk, elsegit cat-fileat the manifest'sgitCommit), reads fall back to disk while a blob is missing or the database is slow/down (2 s budget per read, then a 5 s circuit breaker), and GC removes blobs no current/retained snapshot lists afterblob_gc_grace_secs(min 300; GC aborts if any snapshot is unreadable). The state pool is 8 connections withpg, 4 otherwise. Seedocs/master-registry-storage-and-ingest.md - It also maintains the music_metas feed (
metas.rs, omakase rows injected), serves the app identity (GET /v1/app/{region};PUTstores an override and pushes it toregistry.account_nodes), and fansmaster-updatednotices out toregistry.subscribers(body{server, dataVersion, contentHash, gitCommit, changedFiles, removedFiles}; the last four were added compatibly, receivers must still re-readcurrent) /healthreportsstatus: degradedwhen the last git push failed- CDN contract: pointers (
current,files/{name},music_metas.json,app) areno-cache+ ETag; digest-addressedblob/{sha256}andmanifests/{hash}are immutable — never serve changing bytes under a digest URL; their 404s/503s areno-store - Compression: every registry response except the bundle (
application/x-tar) is compressed perAccept-Encoding(gzip, zstd;registry::encoding), music_metas from<sha256>.json.gz/.json.zstwritten next to each blob (gzip 9, zstd 19; pruned with it) so the 2 MB feed costs no CPU per request. A coded body's strong ETag carries the coding ("<tag>-gzip", the form Caddy'sencodeuses),If-None-Matchmatches any coding of the same content, and a 304 names the coding a 200 would have used. Handlers only see and emit base tags.encoding::negotiatemirrors tower-http's choice (a test compares them); bodies streamed through the layer must be fused streams
Registry-Driven Ingest (master_ingest)
- Opt-in role: runs only as its own process with an
ingestconfig section; the in-sync path (master_databaseon the syncer/registry/updater) is unchanged and stays the default. Never point both at the same database - Reads only the registry (
/healthmust reportblobStore: pg, thencurrentandblob/{sha256}); the webhook (POST /internal/master-updated, beareringest.webhook_token) and the cron only trigger a reconcile of the registry'scurrent - Per target (
ingest.targets[], state keyed by targetname):master_ingest_state(per-file sha256 + mapping hash, rows, missing_since),master_ingest_version,master_raw(only withraw: true, opt-in),master_ingest_allow_shrink. A file is rewritten when its sha256 or mapping hash (schema entry, DB columns/types, key,raw,target::ENGINE_VERSION, the table'singest_engine::table_transform_tag) changed — bumpENGINE_VERSIONwhen the generic value conversion or write SQL changes (rewrites every table); change a table's transform tag when only that table's transform changes (rewrites that table only) - Per-table transforms (
json_to_sea_value_for_column, both ingest paths):cards.assetbundle_nameprefers ares<n>_no<n>archiveDisplayType;cards.card_parametersCP rows (JP/EN) are stored in the Nuverse shape{"param1": [power by level], ...}when every type's levels are exactly 1..n, otherwise unchanged;master_rawstays the file's rows - Each file is staged into a temp table, then upserted
ON CONFLICT (key) … WHERE … IS DISTINCT FROMand stale keys deleted (keyless tables / no matching unique index: delete + insert). Steady runs commit a region in one transaction per target; runs abovestage_tables/stage_bytescommit per file (statusstaging). Checks: min_ratio (override: allow-shrink), required tables, never backwards; removed files are cleared after two consecutive missing versions, never protected ones - DB tests:
HARUKI_TEST_INGEST_DSN(a user that may CREATE DATABASE), optionalHARUKI_TEST_INGEST_MASTER_DIRfor a full-directory equivalence run
Schema System
schema_info.jsondefines table names, column types, and unique keys- Generated by
tools/ent_generatorfrom Rust model files - Table names use lowercased type aliases (e.g.,
MusicArtist→musicartists) - Column
idin JSON is mapped togame_idin DB (avoids PK collision) - Every table has a
server_regioncolumn for multi-region data
Coding Standards
Language & Framework
- Rust 2021 edition, async with Tokio
- Every binary sets
mimallocas its global allocator (musl's malloc doubles the cost of the allocation-heavy msgpack -> JSON decode); keep it when adding abin. The image setsMIMALLOC_ARENA_EAGER_COMMIT=0(idle RSS +2 MB instead of +12 MB) - Axum for HTTP, SeaORM for database, tracing for logging
- sonic-rs for fast JSON, serde_json when key order matters
Error Handling
- Define errors as
AppErrorvariants insrc/error.rs - Implement
From<ExternalError>for automatic?conversion AppErrorimplementsIntoResponsewith JSON error body:{"result":"failed","status":N,"message":"..."}- Use
anyhow::Resultonly inmain()and CLI binaries, not in library code
Naming & Style
- Modules/files:
snake_case(e.g.,sekai_client.rs) - Structs/enums:
PascalCase - Functions:
snake_case - Game data models: all fields
Option<T>,#[serde(rename_all = "camelCase")] - Enum variants for game types:
#[serde(rename_all = "snake_case")] - Config enums:
#[serde(rename_all = "lowercase")] - Comments: only when clarifying non-obvious logic; do not over-comment
Concurrency
Arc<RwLock<>>(parking_lot) for shared mutable statetokio::sync::Mutexfor async critical sectionsAtomicBool/AtomicUsizefor lock-free flags and counters- Parallel initialization via
tokio::spawn+futures::future::join_all
Logging
- Use
tracing::{info, warn, error, debug}— neverprintln!in library code - Region-specific messages: prefix with
"{} message", region.as_str().to_uppercase() - Log levels:
errorfor unrecoverable failures,warnfor handled failures,infofor state changes,debugfor diagnostics
Testing
- Most tests live in
#[cfg(test)] mod testsblocks within source files; the ingest role's tests are insrc/ingest/tests.rsandtests/ingest_memory.rsis an integration test - Async tests use
#[tokio::test] - Tests requiring external services (DB, Redis, game servers) are marked
#[ignore]. PostgreSQL ones readHARUKI_TEST_INGEST_DSN/HARUKI_TEST_REGISTRY_DSN(HARUKI_TEST_INGEST_MASTER_DIRoptional). A bare--ignoredalso runsupdater::master_stream::tests::peak_memory_probe(~500 MB msgpack) andingest_engine::test_direct_ingestion(hard-coded local DSN), so CI filters to--ignored --test-threads=1 ingest::tests postgres - Return type:
anyhow::Result<()>for tests with fallible operations
Building
# Development build
cargo build
# Release build (thin LTO, codegen-units = 1, panic = abort, stripped)
cargo build --release
# Run server
cargo run
# Run master data ingestion
cargo run --bin run_ingest
# Run the master data manager (registry) on the same config file
cargo run --bin master_registry
# Run the registry-driven ingest role (needs an `ingest` section)
cargo run --bin master_ingest
# Tests, a single test, and the PostgreSQL-gated ones as CI runs them
cargo test
cargo test <test_name>
HARUKI_TEST_INGEST_DSN=... HARUKI_TEST_REGISTRY_DSN=... \
cargo test --lib -- --ignored --test-threads=1 ingest::tests postgres
HARUKI_TEST_INGEST_DSN=... cargo test --test ingest_memory -- --ignored
# Lint and format
cargo clippy
cargo fmt
# Per-stage CPU / allocation profile of the game-API proxy path on a captured
# response body (JSON fixture): AES decrypt, msgpack decode, Nuverse restore,
# serialization, cache entry, gzip/zstd, the auth JWT, and the compression layer
STAGES_FIXTURE=ranking.json STAGES_PATH='/user/{userId}/event/1/ranking' \
STAGES_BUNDLE=Data/structures/nuverse_schema_bundle.json cargo run --release --bin bench_stages
# Run ent_generator (from tools/ent_generator/)
cd tools/ent_generator && cargo run
# Docker build
docker build --build-arg VERSION=v1.0.0 -t haruki-sekai-api .
The Dockerfile is a cargo-chef multi-stage build (rust:1.99-alpine → alpine:3.24). The
image contains haruki-sekai-api (the default command), master_registry, master_ingest,
run_ingest, schema_info.json and Data/structures, and exposes 9999 (API,
backend.port), 9998 (registry.port) and 9997 (ingest.listen, which defaults to
127.0.0.1:9997). git is installed because GitHelper shells out to it.
The runtime stage pins exact Alpine package versions (ca-certificates, tzdata, git, gnupg,
openssh-keygen). Alpine drops superseded package versions from its index, which happens to tzdata several
times a year. When that happens, an uncached build fails at apk add, even though CI can stay green while the
layer comes from the registry cache. When the base image or a package moves, refresh the pins to the versions in
the current v3.24 index.
Common Tasks for Agents
Adding a New Game Data Model
- Create
src/models/{name}.rswithpub type X = Vec<XElement>;and the struct definition - Add
pub mod {name};tosrc/models.rs - Run
cd tools/ent_generator && cargo runto regenerateschema_info_generated.jsonand Go schemas - Copy
schema_info_generated.jsontoschema_info.jsonif correct - Copy generated Go schemas to the EntGo project and run migrations
Adding a New API Endpoint
- Add handler function in
src/api/apis.rs - Register route in
src/api/routes.rs(protected routes go under the auth middleware layer) - If the endpoint proxies game API calls, use
proxy_game_api()or model after existing handlers
Adding a New Error Variant
- Add variant to
AppErrorenum insrc/error.rs - Add
#[error("...")]message - Add HTTP status mapping in
status_code()method - Add
Fromimplementation if converting from an external error type
Modifying the Ingest Engine
- Table-to-file mapping:
resolve_table_name()normalizes filenames (lowercase, strip underscores, try plural forms) - Column mapping: JSON keys are normalized (lowercase + strip underscores),
id→game_id - Unmapped JSON keys are dropped (only keys matching a schema column are inserted)
- In-sync path: one transaction per file, DELETE existing region data, then batch INSERT
- Registry-driven path (
src/ingest/): see "Registry-Driven Ingest" above; unmapped keys are counted inmaster_ingest_version.unknown_keysand kept inmaster_rawwhenraw: true
Modifying Config
The config file path comes from the CONFIG_PATH env var (default
haruki-sekai-configs.yaml in the working directory; the file is git-ignored, copy
haruki-sekai-configs.example.yaml). Per-region server entries hold AES keys (hex),
account directories, master data paths and cron schedules. Two separate databases are
configured: database (users; sekai_users / sekai_user_servers are created with
create_table_from_entity().if_not_exists()) and master_database (master data tables
from schema_info.json). apphash_sources and servers.<region>.enable_app_hash_updater /
app_hash_updater_cron are deprecated and ignored (a startup warning names them).
- Add field to relevant struct in
src/config.rswith#[serde(default = "...")] - Add default function if needed
- Update
haruki-sekai-configs.example.yaml
Git commits
All commit subjects must follow:
[Type] Short description starting with capital letter
Allowed types:
| Type | Usage |
|---|---|
[Feat] |
New feature or capability |
[Fix] |
Bug fix |
[Chore] |
Maintenance, refactoring, dependency or build changes |
[Docs] |
Documentation-only changes |
Rules:
- Description starts with a capital letter.
- Use imperative mood:
Add ..., notAdded .... - No trailing period.
- Keep the subject at or below roughly 70 characters.
- Agent attribution uses the standard Git
Co-authored-by:trailer in the commit body, not a free-formAgent:line. This makes GitHub render the co-author avatar on the commit page. The trailer must be on its own line, separated from the subject by a blank line, in the formCo-authored-by: <Display Name> <email>. Suggested values per agent:- Claude:
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>(substitute the actual model, e.g.Claude Opus 4.7,Claude Sonnet 4.6,Claude Haiku 4.5) - Codex:
Co-authored-by: Codex <noreply@openai.com> - Copilot:
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Claude:
Examples from this repo's history:
[Feat] Add custom music score proxy routes
[Fix] Replace manual padding repeat
[Chore] Update dependencies
[Chore] Bump actions/download-artifact from 4 to 8
GitHub Actions workflows
CI and releases reuse the shared workflows in
seiunx-dev/ci-templates (@v1). The files in
.github/workflows are thin callers; see docs/ci-workflows.md for the job-by-job layout.
ci.yml(CI) runs onmainpushes, pull requests targetingmain, and manual dispatch: Rust lint (fmt + Clippy-D warningsfor the crate andtools/ent_generator), tests run once under cargo-llvm-cov with a Postgres service (plus the PG-gated--ignored ingest::tests postgrestests), an MSRV check againstrust-versioninCargo.toml(1.94), Python tool tests, Sonar (scans the uploaded coverage, no second test run), Docker, and workflow lint (actionlint).CI OKis the only required status check; it fails when any CI job fails or is cancelled.- Docker (inside
ci.yml): PRs build only, and only when Docker inputs change. Onmainthe image job does not wait for the tests: it runs in parallel and pushes the immutableghcr.io/team-haruki/haruki-sekai-api:sha-<full sha>and:sha-<7 chars>as soon as the build finishes; theDocker tagsjob (templatedocker-retag.yml, afterCI OK) then moves:mainto that digest without rebuilding.:maintherefore only follows commits whoseCI OKpassed and lags the:sha-*tags until then; deploy:sha-<7 chars>when the image is needed earlier. release.yml(Release) runs onv*tags and manual dispatch. A manual dispatch is a dry run: it builds the archives and publishes nothing.- Release flow: bump
versioninCargo.tomlin a PR → merge → wait forCI OKon main → push tagv<version>→ the gate checks tag ==v+ Cargo version and waits forCI OKon the tagged commit → linux-x64 / macos-arm64 / windows-x64 archives are built on the tag (asset namesharuki-sekai-api-<label>.tar.gz|zip, plusSHA256SUMS-<tag>.txt) → the main:sha-<sha>image is re-tagged as:X.Y.Z,:X.Y,:latestwithout a rebuild → the GitHub Release is published with auto-generated notes, which are then rewritten to the release notes standard (see Release notes). Never rewrite the version from the tag. - Caches: Rust caches and the Docker registry cache (
:buildcache) are written only frommain; PRs and tags only read them. Release builds are cold builds on the tag. - Concurrency: PR runs are grouped per PR and cancel older runs; every other event gets its own group per commit, so main runs are never cancelled.
Workflow maintenance rules:
- Reuse the shared templates first. Add a custom job or step only when a template genuinely cannot meet this project's needs; keep it in the thin caller (
ci.yml/release.yml) with a comment explaining why the template was not enough. - Fix template bugs and add missing template features upstream in
seiunx-dev/ci-templatesinstead of working around them here; callers stay on@v1. - Keep
permissionsminimal:contents: readby default,packages: writeonly on the Docker jobs,contents: writeonly on the GitHub Release job. - Pin third-party actions in custom steps to a commit SHA with a version comment.
- Do not reintroduce
docker.yml,sonar.yml, per-workflow prebuild/reuse logic, or main-push release builds. - Do not add
needs:on the test jobs to the Docker job, and do not suppressgithubactions:S7637(full-SHA pins) insonar-project.properties: the template'ssonar.ymlalready ignores it for the@v1references.
Release notes
Release notes follow the org standard in
seiunx-dev/ci-templates RELEASE_NOTES.md and are written in English.
- Title every release with the tag only, for example
v6.28.2. - Publish a tag as a pre-release only when it has an
-alpha,-betaor-rcsuffix; every other tag is a regular release, and every tag gets a release. - Omit empty sections, and end each item with its PR number
(#123)(short commit SHA when there is no PR). - After the
Releaseworkflow publishes a release with auto-generated notes, rewrite them to the standard (gh release edit <tag> --notes-file <file>). - Never mention internal infrastructure (node or server names, private or tailnet IPs, internal hostnames, server paths) in release notes.
