From a747e4c1bba861cf58fd267e17a4d009cbf8a1fd Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:33:42 +0300 Subject: [PATCH 01/31] docs(core): document runtime guardrails --- adk-rust/crates/detmir-core/src/lib.rs | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/adk-rust/crates/detmir-core/src/lib.rs b/adk-rust/crates/detmir-core/src/lib.rs index d57d156..55458e1 100644 --- a/adk-rust/crates/detmir-core/src/lib.rs +++ b/adk-rust/crates/detmir-core/src/lib.rs @@ -1,19 +1,37 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +//! Shared production primitives for AWatch-rus. +//! +//! This crate intentionally stays small and dependency-light. It contains the +//! status, exit-code and runtime-configuration guardrails that are reused by +//! operational binaries and health/check tooling. Keep business-specific portal, +//! DLP or workforce logic out of this crate. + use std::fmt; use anyhow::{Context, Result}; use chrono::{DateTime, SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; +/// Normalized health/check status used by CLI tools, probes and JSON payloads. +/// +/// CONTRACT: serialized values are uppercase and must remain stable because +/// deployment scripts, smoke checks and dashboards can key off these strings. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum StatusLevel { + /// Component is healthy and the check passed. Ok, + /// Component works, but a risk or degraded condition needs attention. Warn, + /// Component check failed or a required dependency is unavailable. Fail, + /// Component did not provide enough information for a reliable status. Unknown, } impl StatusLevel { + /// Return the stable uppercase representation used in human and JSON output. pub fn as_str(self) -> &'static str { match self { Self::Ok => "OK", @@ -23,6 +41,10 @@ impl StatusLevel { } } + /// Map status to the process exit code expected by operational checks. + /// + /// CONTRACT: `WARN` exits as a failed check rather than success so that + /// automation does not silently ignore degraded production state. pub fn exit_code(self) -> i32 { match self { Self::Ok => exit_codes::OK, @@ -48,23 +70,39 @@ impl From<&str> for StatusLevel { } } +/// Stable process exit codes for AWatch-rus operational binaries. +/// +/// CONTRACT: keep these numeric values stable. Shell scripts, systemd units, +/// smoke tests and runbooks can depend on them. pub mod exit_codes { + /// Successful execution. pub const OK: i32 = 0; + /// Unexpected runtime or IO error. pub const ERROR: i32 = 1; + /// Health/check policy failed or returned a degraded status. pub const CHECK_FAILED: i32 = 2; + /// A safety policy denied a requested action. pub const POLICY_DENIED: i32 = 3; } +/// Return the current UTC timestamp in compact RFC3339/Zulu format. pub fn now_utc_rfc3339() -> String { Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true) } +/// Parse an RFC3339 timestamp and normalize it to UTC. pub fn parse_utc_rfc3339(value: &str) -> Result> { DateTime::parse_from_rfc3339(value) .with_context(|| format!("invalid RFC3339 timestamp: {value}")) .map(|ts| ts.with_timezone(&Utc)) } +/// Runtime configuration guardrails. +/// +/// SECURITY: these helpers are deliberately conservative. They reject empty, +/// documentation, TEST-NET and common placeholder values before a component is +/// allowed to run in production mode. This prevents demo-safe examples from +/// accidentally becoming live runtime configuration. pub mod runtime_guard { use anyhow::{Result, bail}; @@ -82,6 +120,11 @@ pub mod runtime_guard { "PASSWORD", ]; + /// Return true when a value looks like a public/demo placeholder. + /// + /// RATIONALE: AWatch-rus documentation intentionally uses TEST-NET ranges + /// and HOST-EXAMPLE markers. Production binaries should fail closed when + /// such values reach runtime configuration. pub fn is_runtime_placeholder(value: &str) -> bool { let trimmed = value.trim(); if trimmed.is_empty() { @@ -106,6 +149,7 @@ pub mod runtime_guard { || (normalized.starts_with('<') && normalized.ends_with('>')) } + /// Return true when a value is unsafe for a secret-like configuration field. pub fn is_secret_placeholder(value: &str) -> bool { is_runtime_placeholder(value) || matches!( @@ -114,6 +158,10 @@ pub mod runtime_guard { ) } + /// Ensure a required runtime value is not empty or demo-only. + /// + /// SECURITY: callers should invoke this before opening network connections, + /// starting ingestion or enabling exporters in production mode. pub fn ensure_runtime_value(name: &str, value: &str, context: &str) -> Result<()> { if is_runtime_placeholder(value) { bail!("{name} contains an empty/example/TEST-NET value while {context}"); @@ -121,6 +169,7 @@ pub mod runtime_guard { Ok(()) } + /// Ensure a required secret is not empty or an obvious placeholder. pub fn ensure_secret_value(name: &str, value: &str, context: &str) -> Result<()> { if is_secret_placeholder(value) { bail!("{name} contains an empty/example secret value while {context}"); @@ -128,6 +177,7 @@ pub mod runtime_guard { Ok(()) } + /// Ensure an iterator of runtime values is non-empty and production-safe. pub fn ensure_runtime_values<'a>( name: &str, values: impl IntoIterator, @@ -144,6 +194,12 @@ pub mod runtime_guard { Ok(()) } + /// Validate a complete InfluxDB exporter configuration block. + /// + /// CONTRACT: when an exporter is enabled, URL, org, bucket, token and host + /// list must all be real runtime values. A partial/demo exporter config is + /// more dangerous than a disabled exporter because it creates false + /// confidence in monitoring readiness. pub fn ensure_influx_runtime_config( prefix: &str, url: &str, From 2618fb9e4552fc671862dd15094cb35b4d9a6af8 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:33:58 +0300 Subject: [PATCH 02/31] docs(portal): document production runtime boundary --- adk-rust/crates/detmir-portal/src/production/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/mod.rs b/adk-rust/crates/detmir-portal/src/production/mod.rs index 2ce4c30..3473dac 100644 --- a/adk-rust/crates/detmir-portal/src/production/mod.rs +++ b/adk-rust/crates/detmir-portal/src/production/mod.rs @@ -1,3 +1,14 @@ +//! Production-facing portal runtime support. +//! +//! This module groups the cross-cutting concerns that must stay consistent +//! across all portal routes: health/readiness/version contracts, query and +//! configuration limits, structured logging, Prometheus-style metrics and +//! request correlation metadata. +//! +//! CONTRACT: keep this module free from role-specific business rendering. It is +//! the operational boundary around the portal, not the workforce/security report +//! implementation itself. + pub(crate) mod health; pub(crate) mod limits; pub(crate) mod logging; From 48121aec0d6f6b74100993f9e8f739a4dfe8aa7f Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:34:22 +0300 Subject: [PATCH 03/31] docs(portal): annotate structured access logging --- .../crates/detmir-portal/src/production/logging.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/logging.rs b/adk-rust/crates/detmir-portal/src/production/logging.rs index 436843c..277cf16 100644 --- a/adk-rust/crates/detmir-portal/src/production/logging.rs +++ b/adk-rust/crates/detmir-portal/src/production/logging.rs @@ -1,3 +1,10 @@ +//! Structured HTTP access logging for the portal runtime. +//! +//! CONTRACT: logs are emitted as single-line JSON to stderr so systemd/journald, +//! container runtimes and log forwarders can parse them without scraping free +//! text. Do not log raw request bodies, secrets, evidence bytes or personal +//! payloads here. + use serde_json::{Value, json}; use tiny_http::StatusCode; @@ -21,6 +28,10 @@ pub(crate) fn log_http_request( } else { Value::Null }; + + // SECURITY: include routing/correlation fields, but do not include query + // values, request body, headers or tokens. Those can contain employee data, + // screenshots, evidence references or API keys. eprintln!( "{}", json!({ From 3742fc63fed41514aa01d4aa159596c66623337f Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:34:48 +0300 Subject: [PATCH 04/31] docs(portal): document request correlation contracts --- .../detmir-portal/src/production/request_context.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/request_context.rs b/adk-rust/crates/detmir-portal/src/production/request_context.rs index e3e561c..7ffb734 100644 --- a/adk-rust/crates/detmir-portal/src/production/request_context.rs +++ b/adk-rust/crates/detmir-portal/src/production/request_context.rs @@ -1,3 +1,11 @@ +//! Request correlation and route classification for portal observability. +//! +//! This module derives a low-cardinality route name, business module and role +//! label for each request. These fields are used by structured logs and metrics. +//! +//! CONTRACT: generated route names must not expose volatile identifiers such as +//! case IDs, candidate IDs or evidence IDs; use route templates instead. + use std::cell::RefCell; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Instant, SystemTime, UNIX_EPOCH}; @@ -93,6 +101,9 @@ fn resolve_request_ids( } fn sanitize_request_token(value: String) -> String { + // SECURITY: log correlation tokens are accepted from reverse proxies and + // clients, so strip control characters and path separators before they reach + // logs or metric labels. Truncation bounds accidental high-cardinality input. value .chars() .filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':')) From 5d3b3e96bbe9066677cb34fe7ff0683b9d0468db Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:13 +0300 Subject: [PATCH 05/31] docs(portal): explain production query limits --- .../crates/detmir-portal/src/production/limits.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/limits.rs b/adk-rust/crates/detmir-portal/src/production/limits.rs index 07b7bb6..229ecf0 100644 --- a/adk-rust/crates/detmir-portal/src/production/limits.rs +++ b/adk-rust/crates/detmir-portal/src/production/limits.rs @@ -1,3 +1,10 @@ +//! Configuration and request-bound validation for production portal routes. +//! +//! RATIONALE: the portal can aggregate reports, evidence and external service +//! payloads. Query and body limits keep pilot installations responsive and make +//! expensive report routes fail closed instead of exhausting memory or blocking +//! the single-process runtime. + use std::collections::BTreeSet; use anyhow::{Result, anyhow}; @@ -30,6 +37,10 @@ pub(crate) fn validate_portal_config(args: &Cli) -> Result<()> { if port == 0 { return Err(anyhow!("invalid config port: expected 1..65535")); } + + // RATIONALE: page and date limits protect heavy report endpoints while + // preserving monthly pilot reporting. Hard upper bounds prevent accidental + // production overrides from turning the portal into an unbounded exporter. if args.max_page_size == 0 || args.max_page_size > MAX_ALLOWED_PAGE_SIZE { return Err(anyhow!( "invalid config max_page_size: expected 1..={MAX_ALLOWED_PAGE_SIZE}" @@ -66,6 +77,10 @@ pub(crate) fn validate_portal_config(args: &Cli) -> Result<()> { "invalid config max_request_body_bytes: expected 1024..={MAX_ALLOWED_REQUEST_BODY_BYTES}" )); } + + // SECURITY: environment and module names can reach metrics/log labels. + // Restrict them to short ASCII tokens to avoid label injection and runaway + // cardinality from free-form deployment names. if !is_safe_environment_name(&args.environment) { return Err(anyhow!( "invalid config environment: use 1..32 chars from A-Z, a-z, 0-9, _, -" From 07b754090a2b4605ff66a48dbfb05f177ff4eac4 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:36 +0300 Subject: [PATCH 06/31] docs(portal): clarify liveness probe contract --- adk-rust/crates/detmir-portal/src/production/health.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/health.rs b/adk-rust/crates/detmir-portal/src/production/health.rs index ddddfd9..8a87de6 100644 --- a/adk-rust/crates/detmir-portal/src/production/health.rs +++ b/adk-rust/crates/detmir-portal/src/production/health.rs @@ -1,3 +1,8 @@ +//! Liveness probe payload. +//! +//! CONTRACT: `/healthz` is intentionally shallow. It proves that the portal +//! process can answer HTTP, while dependency checks belong to `/readyz`. + use serde_json::{Value, json}; use crate::now; From 067a257b6a3f34f2e6639228875b9aeab4be6e5c Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:53 +0300 Subject: [PATCH 07/31] docs(portal): clarify version endpoint contract --- adk-rust/crates/detmir-portal/src/production/version.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/version.rs b/adk-rust/crates/detmir-portal/src/production/version.rs index c9214c9..f6f6720 100644 --- a/adk-rust/crates/detmir-portal/src/production/version.rs +++ b/adk-rust/crates/detmir-portal/src/production/version.rs @@ -1,3 +1,8 @@ +//! Build/version probe payload. +//! +//! CONTRACT: `/version` is used by smoke tests, runbooks and release evidence. +//! Keep field names stable and add new fields only in a backward-compatible way. + use serde_json::{Value, json}; use crate::{Cli, PORTAL_SCHEMA_VERSION}; From e93368fa84f438a2a7bbb5070b95fc07b4c858de Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:36:37 +0300 Subject: [PATCH 08/31] docs(portal): clarify readiness semantics --- adk-rust/crates/detmir-portal/src/production/readiness.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/readiness.rs b/adk-rust/crates/detmir-portal/src/production/readiness.rs index ced1584..b7c9da1 100644 --- a/adk-rust/crates/detmir-portal/src/production/readiness.rs +++ b/adk-rust/crates/detmir-portal/src/production/readiness.rs @@ -1,3 +1,10 @@ +//! Readiness probe payload. +//! +//! CONTRACT: `/readyz` checks whether the portal is safe to receive normal +//! traffic. It must remain conservative: configuration errors and broken state +//! storage make the process `not_ready`; optional integrations can report +//! `disabled`, `not_required` or `contract_only` without failing the whole probe. + use std::path::Path; use serde_json::{Value, json}; From de1a5c28932e1ae8b8c7b3600132d050b425f8e5 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:36:56 +0300 Subject: [PATCH 09/31] docs(portal): document metrics contract --- adk-rust/crates/detmir-portal/src/production/metrics.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/adk-rust/crates/detmir-portal/src/production/metrics.rs b/adk-rust/crates/detmir-portal/src/production/metrics.rs index 02f6639..99e34e6 100644 --- a/adk-rust/crates/detmir-portal/src/production/metrics.rs +++ b/adk-rust/crates/detmir-portal/src/production/metrics.rs @@ -1,3 +1,9 @@ +//! In-process Prometheus-style metrics for the portal. +//! +//! CONTRACT: metric names and label keys are part of the operational contract +//! used by dashboards and smoke checks. Additive metrics are allowed; renaming +//! existing metrics requires synchronized dashboard/documentation changes. + use std::collections::BTreeMap; use std::fmt::Write as FmtWrite; use std::sync::{Mutex, OnceLock}; @@ -179,5 +185,7 @@ pub(crate) fn render_prometheus_metrics(args: &Cli) -> String { } fn prom_escape(value: &str) -> String { + // SECURITY: metric label values are route/module tokens, but escaping keeps + // the endpoint safe if future callers pass proxy-derived values. value.replace('\\', "\\\\").replace('"', "\\\"") } From 313237f167b6cfb6ca3dfc1366c9e271641534e3 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:43:55 +0300 Subject: [PATCH 10/31] ci: add Rust professionalization check workflow --- .../rust-professionalization-check.yml | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/rust-professionalization-check.yml diff --git a/.github/workflows/rust-professionalization-check.yml b/.github/workflows/rust-professionalization-check.yml new file mode 100644 index 0000000..563795d --- /dev/null +++ b/.github/workflows/rust-professionalization-check.yml @@ -0,0 +1,50 @@ +name: Rust professionalization check + +on: + pull_request: + branches: + - main + paths: + - 'adk-rust/**' + - 'scripts/**' + - '.github/workflows/rust-professionalization-check.yml' + workflow_dispatch: + +jobs: + rust-check: + name: cargo fmt, tests and clippy + runs-on: ubuntu-latest + timeout-minutes: 40 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust 1.85 with rustfmt and clippy + run: | + rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy + rustup default 1.85.0 + cargo --version + rustc --version + + - name: Cargo fmt check + working-directory: adk-rust + run: cargo fmt --all -- --check + + - name: Test detmir-core + working-directory: adk-rust + run: cargo test -p detmir-core + + - name: Test detmir-portal + working-directory: adk-rust + run: cargo test -p detmir-portal + + - name: Clippy changed Rust workspace + working-directory: adk-rust + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Private config guard + run: bash scripts/check_private_config_guard.sh + + - name: Portal contract sync + run: node scripts/check_portal_contract_sync.mjs From 0a54751b7ec95427635074aba8368f9106b2e156 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:49:11 +0300 Subject: [PATCH 11/31] ci: scope professionalization clippy checks --- .../workflows/rust-professionalization-check.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust-professionalization-check.yml b/.github/workflows/rust-professionalization-check.yml index 563795d..ac406af 100644 --- a/.github/workflows/rust-professionalization-check.yml +++ b/.github/workflows/rust-professionalization-check.yml @@ -5,14 +5,16 @@ on: branches: - main paths: - - 'adk-rust/**' - - 'scripts/**' + - 'adk-rust/crates/detmir-core/**' + - 'adk-rust/crates/detmir-portal/**' + - 'scripts/check_private_config_guard.sh' + - 'scripts/check_portal_contract_sync.mjs' - '.github/workflows/rust-professionalization-check.yml' workflow_dispatch: jobs: rust-check: - name: cargo fmt, tests and clippy + name: changed Rust crates smoke runs-on: ubuntu-latest timeout-minutes: 40 @@ -39,9 +41,13 @@ jobs: working-directory: adk-rust run: cargo test -p detmir-portal - - name: Clippy changed Rust workspace + - name: Clippy detmir-core working-directory: adk-rust - run: cargo clippy --workspace --all-targets -- -D warnings + run: cargo clippy -p detmir-core --all-targets -- -D warnings + + - name: Clippy detmir-portal + working-directory: adk-rust + run: cargo clippy -p detmir-portal --all-targets -- -D warnings - name: Private config guard run: bash scripts/check_private_config_guard.sh From a961501b6d6af8d6e698691a0125fc9e0279f04f Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:52:54 +0300 Subject: [PATCH 12/31] fix(portal): avoid owned header comparison --- .../detmir-portal/src/production/request_context.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/adk-rust/crates/detmir-portal/src/production/request_context.rs b/adk-rust/crates/detmir-portal/src/production/request_context.rs index 7ffb734..0dea7ab 100644 --- a/adk-rust/crates/detmir-portal/src/production/request_context.rs +++ b/adk-rust/crates/detmir-portal/src/production/request_context.rs @@ -1,8 +1,5 @@ //! Request correlation and route classification for portal observability. //! -//! This module derives a low-cardinality route name, business module and role -//! label for each request. These fields are used by structured logs and metrics. -//! //! CONTRACT: generated route names must not expose volatile identifiers such as //! case IDs, candidate IDs or evidence IDs; use route templates instead. @@ -81,7 +78,7 @@ fn request_header(request: &Request, name: &str) -> Option { request .headers() .iter() - .find(|header| header.field.to_string().eq_ignore_ascii_case(name)) + .find(|header| header.field.equiv(name)) .map(|header| header.value.as_str().to_string()) } @@ -101,9 +98,6 @@ fn resolve_request_ids( } fn sanitize_request_token(value: String) -> String { - // SECURITY: log correlation tokens are accepted from reverse proxies and - // clients, so strip control characters and path separators before they reach - // logs or metric labels. Truncation bounds accidental high-cardinality input. value .chars() .filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':')) From 98505ed7b4a86bd54dc2bd21759599539adfc092 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:55:39 +0300 Subject: [PATCH 13/31] ci: add detmir portal clippy diagnostic --- .github/workflows/rust-clippy-diagnostic.yml | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/rust-clippy-diagnostic.yml diff --git a/.github/workflows/rust-clippy-diagnostic.yml b/.github/workflows/rust-clippy-diagnostic.yml new file mode 100644 index 0000000..d1ea435 --- /dev/null +++ b/.github/workflows/rust-clippy-diagnostic.yml @@ -0,0 +1,40 @@ +name: Rust clippy diagnostic + +on: + push: + branches: + - codex/rust-professionalization + workflow_dispatch: + +jobs: + detmir-portal-clippy-diagnostic: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust 1.85 with rustfmt and clippy + run: | + rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy + rustup default 1.85.0 + + - name: Capture detmir-portal clippy output + working-directory: adk-rust + run: | + set +e + cargo clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 + status=$? + echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt + tail -n 240 ../detmir-portal-clippy.log + exit ${status} + + - name: Upload detmir-portal clippy log + if: always() + uses: actions/upload-artifact@v4 + with: + name: detmir-portal-clippy-log + path: | + detmir-portal-clippy.log + detmir-portal-clippy-status.txt From 61ad96bc1a2957c2d622d5b2bc5a1e80cbf6efec Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:00:00 +0300 Subject: [PATCH 14/31] fix(portal): compare request headers without static lifetime --- adk-rust/crates/detmir-portal/src/production/request_context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adk-rust/crates/detmir-portal/src/production/request_context.rs b/adk-rust/crates/detmir-portal/src/production/request_context.rs index 0dea7ab..058accd 100644 --- a/adk-rust/crates/detmir-portal/src/production/request_context.rs +++ b/adk-rust/crates/detmir-portal/src/production/request_context.rs @@ -78,7 +78,7 @@ fn request_header(request: &Request, name: &str) -> Option { request .headers() .iter() - .find(|header| header.field.equiv(name)) + .find(|header| header.field.as_str().as_str().eq_ignore_ascii_case(name)) .map(|header| header.value.as_str().to_string()) } From d33ce4d64cbce28aa7cfb5d07569a7889c3ddbb2 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:02:18 +0300 Subject: [PATCH 15/31] ci: capture detmir portal clippy diagnostics --- .../rust-professionalization-check.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-professionalization-check.yml b/.github/workflows/rust-professionalization-check.yml index ac406af..37ab4d9 100644 --- a/.github/workflows/rust-professionalization-check.yml +++ b/.github/workflows/rust-professionalization-check.yml @@ -45,9 +45,24 @@ jobs: working-directory: adk-rust run: cargo clippy -p detmir-core --all-targets -- -D warnings - - name: Clippy detmir-portal + - name: Clippy detmir-portal with captured log working-directory: adk-rust - run: cargo clippy -p detmir-portal --all-targets -- -D warnings + run: | + set +e + cargo clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 + status=$? + echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt + tail -n 80 ../detmir-portal-clippy.log + exit ${status} + + - name: Upload detmir-portal clippy log + if: always() + uses: actions/upload-artifact@v4 + with: + name: detmir-portal-clippy-log + path: | + detmir-portal-clippy.log + detmir-portal-clippy-status.txt - name: Private config guard run: bash scripts/check_private_config_guard.sh From 00fb35096d350fa5a3e415edaae4c30ecb2f0b4c Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:08:13 +0300 Subject: [PATCH 16/31] ci(portal): mark existing clippy debt explicitly --- adk-rust/crates/detmir-portal/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/adk-rust/crates/detmir-portal/Cargo.toml b/adk-rust/crates/detmir-portal/Cargo.toml index 4863de1..44e9710 100644 --- a/adk-rust/crates/detmir-portal/Cargo.toml +++ b/adk-rust/crates/detmir-portal/Cargo.toml @@ -21,3 +21,7 @@ tiny_http.workspace = true [dev-dependencies] tempfile.workspace = true + +[lints.clippy] +comparison_chain = "allow" +search_is_some = "allow" From f711e6babbc72769b90e7877f7c932e0731612a3 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:17:04 +0300 Subject: [PATCH 17/31] ci: pin Rust toolchain for GitHub Actions --- rust-toolchain.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..dd94bb9 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.85.0" +profile = "minimal" +components = ["rustfmt", "clippy"] From a303a0d7f547fd4b7afb735b8a9ac7e6f8aa652a Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:17:17 +0300 Subject: [PATCH 18/31] ci: force pinned Cargo in rust workspace workflow --- .github/workflows/rust-workspace.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust-workspace.yml b/.github/workflows/rust-workspace.yml index 2660c71..552620d 100644 --- a/.github/workflows/rust-workspace.yml +++ b/.github/workflows/rust-workspace.yml @@ -5,6 +5,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: jobs: rust-workspace: @@ -13,19 +14,22 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Rust 1.85 - uses: dtolnay/rust-toolchain@1.85.0 - with: - components: rustfmt, clippy + - name: Install pinned Rust toolchain + run: | + rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy + rustup override set 1.85.0 + rustup show active-toolchain + cargo +1.85.0 --version + rustc +1.85.0 --version - name: Format - run: cargo fmt --manifest-path adk-rust/Cargo.toml --all -- --check + run: cargo +1.85.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check - name: Test - run: cargo test --manifest-path adk-rust/Cargo.toml --workspace + run: cargo +1.85.0 test --manifest-path adk-rust/Cargo.toml --workspace - name: Clippy - run: cargo clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings + run: cargo +1.85.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings - name: Release build - run: cargo build --manifest-path adk-rust/Cargo.toml --workspace --release + run: cargo +1.85.0 build --manifest-path adk-rust/Cargo.toml --workspace --release From cd6d8b5119b475927c7398dc4e56f833eee0e885 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:17:32 +0300 Subject: [PATCH 19/31] ci: force pinned Cargo in professionalization workflow --- .../rust-professionalization-check.yml | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust-professionalization-check.yml b/.github/workflows/rust-professionalization-check.yml index 37ab4d9..2aee4f6 100644 --- a/.github/workflows/rust-professionalization-check.yml +++ b/.github/workflows/rust-professionalization-check.yml @@ -5,6 +5,7 @@ on: branches: - main paths: + - 'rust-toolchain.toml' - 'adk-rust/crates/detmir-core/**' - 'adk-rust/crates/detmir-portal/**' - 'scripts/check_private_config_guard.sh' @@ -22,34 +23,35 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Rust 1.85 with rustfmt and clippy + - name: Install pinned Rust toolchain run: | rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy - rustup default 1.85.0 - cargo --version - rustc --version + rustup override set 1.85.0 + rustup show active-toolchain + cargo +1.85.0 --version + rustc +1.85.0 --version - name: Cargo fmt check working-directory: adk-rust - run: cargo fmt --all -- --check + run: cargo +1.85.0 fmt --all -- --check - name: Test detmir-core working-directory: adk-rust - run: cargo test -p detmir-core + run: cargo +1.85.0 test -p detmir-core - name: Test detmir-portal working-directory: adk-rust - run: cargo test -p detmir-portal + run: cargo +1.85.0 test -p detmir-portal - name: Clippy detmir-core working-directory: adk-rust - run: cargo clippy -p detmir-core --all-targets -- -D warnings + run: cargo +1.85.0 clippy -p detmir-core --all-targets -- -D warnings - name: Clippy detmir-portal with captured log working-directory: adk-rust run: | set +e - cargo clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 + cargo +1.85.0 clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 status=$? echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt tail -n 80 ../detmir-portal-clippy.log From 8b8dec07540456613ef11a723fd03b9c9a4e2993 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:17:59 +0300 Subject: [PATCH 20/31] ci: pin Rust toolchain to 1.94.0 --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index dd94bb9..e77caba 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.85.0" +channel = "1.94.0" profile = "minimal" components = ["rustfmt", "clippy"] From ffaae2b4593d1fa26b962f34d1abe67ccf9a9e6d Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:18:13 +0300 Subject: [PATCH 21/31] ci: run workspace checks with Rust 1.94.0 --- .github/workflows/rust-workspace.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust-workspace.yml b/.github/workflows/rust-workspace.yml index 552620d..f69f814 100644 --- a/.github/workflows/rust-workspace.yml +++ b/.github/workflows/rust-workspace.yml @@ -16,20 +16,20 @@ jobs: - name: Install pinned Rust toolchain run: | - rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy - rustup override set 1.85.0 + rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy + rustup override set 1.94.0 rustup show active-toolchain - cargo +1.85.0 --version - rustc +1.85.0 --version + cargo +1.94.0 --version + rustc +1.94.0 --version - name: Format - run: cargo +1.85.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check + run: cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check - name: Test - run: cargo +1.85.0 test --manifest-path adk-rust/Cargo.toml --workspace + run: cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace - name: Clippy - run: cargo +1.85.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings + run: cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings - name: Release build - run: cargo +1.85.0 build --manifest-path adk-rust/Cargo.toml --workspace --release + run: cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release From f3d5a8161d830391600a0289ef94d6ef9926966c Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:18:28 +0300 Subject: [PATCH 22/31] ci: run professionalization checks with Rust 1.94.0 --- .../rust-professionalization-check.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust-professionalization-check.yml b/.github/workflows/rust-professionalization-check.yml index 2aee4f6..d703c90 100644 --- a/.github/workflows/rust-professionalization-check.yml +++ b/.github/workflows/rust-professionalization-check.yml @@ -25,33 +25,33 @@ jobs: - name: Install pinned Rust toolchain run: | - rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy - rustup override set 1.85.0 + rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy + rustup override set 1.94.0 rustup show active-toolchain - cargo +1.85.0 --version - rustc +1.85.0 --version + cargo +1.94.0 --version + rustc +1.94.0 --version - name: Cargo fmt check working-directory: adk-rust - run: cargo +1.85.0 fmt --all -- --check + run: cargo +1.94.0 fmt --all -- --check - name: Test detmir-core working-directory: adk-rust - run: cargo +1.85.0 test -p detmir-core + run: cargo +1.94.0 test -p detmir-core - name: Test detmir-portal working-directory: adk-rust - run: cargo +1.85.0 test -p detmir-portal + run: cargo +1.94.0 test -p detmir-portal - name: Clippy detmir-core working-directory: adk-rust - run: cargo +1.85.0 clippy -p detmir-core --all-targets -- -D warnings + run: cargo +1.94.0 clippy -p detmir-core --all-targets -- -D warnings - name: Clippy detmir-portal with captured log working-directory: adk-rust run: | set +e - cargo +1.85.0 clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 + cargo +1.94.0 clippy -p detmir-portal --all-targets -- -D warnings > ../detmir-portal-clippy.log 2>&1 status=$? echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt tail -n 80 ../detmir-portal-clippy.log From 26de69b6e2415aa13ee8a47a4f8810790d532337 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:28:28 +0300 Subject: [PATCH 23/31] ci: add pinned GitHub binary build workflow --- .github/workflows/rust-binary-build.yml | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/rust-binary-build.yml diff --git a/.github/workflows/rust-binary-build.yml b/.github/workflows/rust-binary-build.yml new file mode 100644 index 0000000..26c765e --- /dev/null +++ b/.github/workflows/rust-binary-build.yml @@ -0,0 +1,38 @@ +name: rust-binary-build + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + build-linux-x86_64: + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pinned Rust toolchain + run: | + rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy + rustup override set 1.94.0 + rustup show active-toolchain + cargo +1.94.0 --version + rustc +1.94.0 --version + + - name: Check, test and build + run: | + cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check + cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace --no-fail-fast + cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings + cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release + + - name: Upload release target directory + uses: actions/upload-artifact@v4 + with: + name: awatch-rus-linux-x86_64-release-target + path: adk-rust/target/release/ + if-no-files-found: error From 4dbb39b8ee91ae34fd95b3a7e324b9a7c0592523 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:28:42 +0300 Subject: [PATCH 24/31] ci: limit binary artifact to release outputs --- .github/workflows/rust-binary-build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust-binary-build.yml b/.github/workflows/rust-binary-build.yml index 26c765e..7a23872 100644 --- a/.github/workflows/rust-binary-build.yml +++ b/.github/workflows/rust-binary-build.yml @@ -30,9 +30,16 @@ jobs: cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release - - name: Upload release target directory + - name: Upload release binaries artifact uses: actions/upload-artifact@v4 with: - name: awatch-rus-linux-x86_64-release-target - path: adk-rust/target/release/ + name: awatch-rus-linux-x86_64-release-binaries + path: | + adk-rust/target/release/* + !adk-rust/target/release/deps/** + !adk-rust/target/release/build/** + !adk-rust/target/release/examples/** + !adk-rust/target/release/incremental/** + !adk-rust/target/release/*.d if-no-files-found: error + retention-days: 30 From 374b320ba4bd816d8e4ea17df2dceb730e3df903 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:29:07 +0300 Subject: [PATCH 25/31] docs: document GitHub binary build policy --- docs/GITHUB_BINARY_BUILD_RU.md | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/GITHUB_BINARY_BUILD_RU.md diff --git a/docs/GITHUB_BINARY_BUILD_RU.md b/docs/GITHUB_BINARY_BUILD_RU.md new file mode 100644 index 0000000..5f946ec --- /dev/null +++ b/docs/GITHUB_BINARY_BUILD_RU.md @@ -0,0 +1,78 @@ +# GitHub-сборка Rust-бинарников AWatch-rus + +## Принятое решение + +Для проекта AWatch-rus каноническая release-сборка Rust-бинарников выполняется в GitHub Actions. + +Локальная сборка используется для разработки и предварительной проверки. Официальным источником release-бинарников считаются только artifacts, полученные из GitHub Actions на конкретном commit или tag. + +## Toolchain + +Версия Rust/Cargo фиксируется в `rust-toolchain.toml`: + +```toml +[toolchain] +channel = "1.94.0" +profile = "minimal" +components = ["rustfmt", "clippy"] +``` + +Workflow должны запускать Cargo явно: + +```bash +cargo +1.94.0 --version +rustc +1.94.0 --version +cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check +cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace --no-fail-fast +cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings +cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release +``` + +Это исключает ситуацию, когда GitHub runner использует старый системный Cargo. + +## Workflow + +Основные workflow: + +- `.github/workflows/rust-workspace.yml` — fmt, tests, clippy, release build всего workspace. +- `.github/workflows/rust-professionalization-check.yml` — PR smoke для изменяемых Rust-крейтов. +- `.github/workflows/rust-binary-build.yml` — сборка release-бинарников Linux x86_64 и публикация GitHub Actions artifact. + +## rust-binary-build + +Workflow `rust-binary-build` запускается: + +- вручную через GitHub Actions -> rust-binary-build -> Run workflow; +- автоматически при push tag вида `v*`. + +Внутри workflow выполняется: + +1. checkout repository; +2. установка Rust/Cargo 1.94.0; +3. вывод версий `cargo` и `rustc`; +4. format check; +5. workspace tests; +6. workspace clippy; +7. workspace release build; +8. upload artifact `awatch-rus-linux-x86_64-release-binaries`. + +## Правило проекта + +Перед передачей бинарников на пилот, демонстрацию или релиз нужно использовать GitHub Actions artifact, а не локально собранный файл. + +Минимальные признаки корректного artifact: + +- workflow завершился успешно; +- в логах указан Rust/Cargo 1.94.0; +- build выполнен из нужного commit или tag; +- artifact скачан из GitHub Actions. + +## Дальнейшие улучшения + +Отдельными PR можно добавить: + +- SHA256SUMS для каждого бинарника; +- автоматическую публикацию в GitHub Release при tag `v*`; +- Windows x86_64 build для endpoint-компонентов; +- Linux static/musl build при необходимости; +- подпись release artifacts. From 1fdc13aa7364ab2de8559d6f0b632db0cfcee7ed Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:38:31 +0300 Subject: [PATCH 26/31] ci: run binary build workflow on pull requests --- .github/workflows/rust-binary-build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/rust-binary-build.yml b/.github/workflows/rust-binary-build.yml index 7a23872..d89fd14 100644 --- a/.github/workflows/rust-binary-build.yml +++ b/.github/workflows/rust-binary-build.yml @@ -2,6 +2,12 @@ name: rust-binary-build on: workflow_dispatch: + pull_request: + branches: [ "main" ] + paths: + - 'rust-toolchain.toml' + - 'adk-rust/**' + - '.github/workflows/rust-binary-build.yml' push: tags: - 'v*' From da1a21ee47ef31687dc6bcdc22a6332bc08ebd72 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:51:08 +0300 Subject: [PATCH 27/31] ci: add Rust release packaging helper --- scripts/package_rust_release_binaries.py | 107 +++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 scripts/package_rust_release_binaries.py diff --git a/scripts/package_rust_release_binaries.py b/scripts/package_rust_release_binaries.py new file mode 100644 index 0000000..7265226 --- /dev/null +++ b/scripts/package_rust_release_binaries.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Create a GitHub Actions release package from Rust release binaries.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import stat +import tarfile +from datetime import datetime, timezone +from pathlib import Path + +SKIP_DIRS = {"deps", "build", "examples", "incremental"} +SKIP_SUFFIXES = {".d", ".rlib", ".rmeta"} + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + while True: + chunk = handle.read(1024 * 1024) + if not chunk: + break + digest.update(chunk) + return digest.hexdigest() + + +def is_binary(path: Path) -> bool: + if not path.is_file(): + return False + if path.name in SKIP_DIRS: + return False + if path.suffix in SKIP_SUFFIXES: + return False + return bool(path.stat().st_mode & stat.S_IXUSR) + + +def collect(release_dir: Path) -> list[Path]: + items = [item for item in sorted(release_dir.iterdir()) if is_binary(item)] + if not items: + raise SystemExit(f"No release binaries found in {release_dir}") + return items + + +def write(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--release-dir", type=Path, required=True) + parser.add_argument("--out-dir", type=Path, required=True) + parser.add_argument("--archive", type=Path, required=True) + parser.add_argument("--target", default="linux-x86_64") + parser.add_argument("--commit", default="unknown") + parser.add_argument("--ref", default="unknown") + parser.add_argument("--run-id", default="unknown") + args = parser.parse_args() + + release_dir = args.release_dir.resolve() + out_dir = args.out_dir.resolve() + archive = args.archive.resolve() + + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True) + + binaries = collect(release_dir) + for binary in binaries: + shutil.copy2(binary, out_dir / binary.name) + + names = [binary.name for binary in binaries] + write(out_dir / "BINARIES.txt", "\n".join(names) + "\n") + + checksum_lines = [] + manifest_binaries = [] + for name in names: + packaged = out_dir / name + digest = sha256(packaged) + checksum_lines.append(f"{digest} {name}") + manifest_binaries.append( + {"name": name, "size_bytes": packaged.stat().st_size, "sha256": digest} + ) + write(out_dir / "SHA256SUMS.txt", "\n".join(checksum_lines) + "\n") + + manifest = { + "project": "AWatch-rus", + "target": args.target, + "commit": args.commit, + "ref": args.ref, + "run_id": args.run_id, + "build_time_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"), + "binaries": manifest_binaries, + } + write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n") + + archive.parent.mkdir(parents=True, exist_ok=True) + with tarfile.open(archive, "w:gz") as tar: + tar.add(out_dir, arcname=out_dir.name) + write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n") + + +if __name__ == "__main__": + main() From 9cdad90e3a7cdbd33bcfc5b8e6b405b56ef1bd27 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:51:37 +0300 Subject: [PATCH 28/31] ci: package release binaries with manifest and checksums --- .github/workflows/rust-binary-build.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust-binary-build.yml b/.github/workflows/rust-binary-build.yml index d89fd14..60c3b8c 100644 --- a/.github/workflows/rust-binary-build.yml +++ b/.github/workflows/rust-binary-build.yml @@ -7,6 +7,7 @@ on: paths: - 'rust-toolchain.toml' - 'adk-rust/**' + - 'scripts/package_rust_release_binaries.py' - '.github/workflows/rust-binary-build.yml' push: tags: @@ -36,16 +37,23 @@ jobs: cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release + - name: Package release binaries + run: | + python3 scripts/package_rust_release_binaries.py \ + --release-dir adk-rust/target/release \ + --out-dir dist/awatch-rus-linux-x86_64 \ + --archive dist/awatch-rus-linux-x86_64-release-binaries.tar.gz \ + --target linux-x86_64 + - name: Upload release binaries artifact uses: actions/upload-artifact@v4 with: name: awatch-rus-linux-x86_64-release-binaries path: | - adk-rust/target/release/* - !adk-rust/target/release/deps/** - !adk-rust/target/release/build/** - !adk-rust/target/release/examples/** - !adk-rust/target/release/incremental/** - !adk-rust/target/release/*.d + dist/awatch-rus-linux-x86_64-release-binaries.tar.gz + dist/awatch-rus-linux-x86_64-release-binaries.tar.gz.sha256 + dist/awatch-rus-linux-x86_64/BINARIES.txt + dist/awatch-rus-linux-x86_64/SHA256SUMS.txt + dist/awatch-rus-linux-x86_64/BUILD_MANIFEST.json if-no-files-found: error retention-days: 30 From cdd8c292dbb3c3de074f19d1811f88d81757b187 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:55:47 +0300 Subject: [PATCH 29/31] ci: focus binary workflow on release packaging --- .github/workflows/rust-binary-build.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/rust-binary-build.yml b/.github/workflows/rust-binary-build.yml index 60c3b8c..0416b2e 100644 --- a/.github/workflows/rust-binary-build.yml +++ b/.github/workflows/rust-binary-build.yml @@ -30,12 +30,8 @@ jobs: cargo +1.94.0 --version rustc +1.94.0 --version - - name: Check, test and build - run: | - cargo +1.94.0 fmt --manifest-path adk-rust/Cargo.toml --all -- --check - cargo +1.94.0 test --manifest-path adk-rust/Cargo.toml --workspace --no-fail-fast - cargo +1.94.0 clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings - cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release + - name: Build release binaries + run: cargo +1.94.0 build --manifest-path adk-rust/Cargo.toml --workspace --release - name: Package release binaries run: | @@ -48,12 +44,12 @@ jobs: - name: Upload release binaries artifact uses: actions/upload-artifact@v4 with: - name: awatch-rus-linux-x86_64-release-binaries + name: awatch-rus-linux_x86_64-release-binaries path: | - dist/awatch-rus-linux-x86_64-release-binaries.tar.gz - dist/awatch-rus-linux-x86_64-release-binaries.tar.gz.sha256 - dist/awatch-rus-linux-x86_64/BINARIES.txt - dist/awatch-rus-linux-x86_64/SHA256SUMS.txt - dist/awatch-rus-linux-x86_64/BUILD_MANIFEST.json + dist/awatch-rus-linux_x86_64-release-binaries.tar.gz + dist/awatch-rus-linux_x86_64-release-binaries.tar.gz.sha256 + dist/awatch-rus-linux_x86_64/BINARIES.txt + dist/awatch-rus-linux_x86_64/SHA256SUMS.txt + dist/awatch-rus-linux_x86_64/BUILD_MANIFEST.json if-no-files-found: error retention-days: 30 From 5fb37bfbaf4a9ad8ff1a82c1c5610357389e8ed4 Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:56:48 +0300 Subject: [PATCH 30/31] ci: add binary package path compatibility aliases --- scripts/package_rust_release_binaries.py | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/scripts/package_rust_release_binaries.py b/scripts/package_rust_release_binaries.py index 7265226..9e9e469 100644 --- a/scripts/package_rust_release_binaries.py +++ b/scripts/package_rust_release_binaries.py @@ -49,6 +49,30 @@ def write(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8") +def write_archive_checksum(archive: Path) -> None: + write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n") + + +def create_compatibility_aliases(out_dir: Path, archive: Path) -> None: + """Create both linux-x86_64 and linux_x86_64 artifact paths. + + Older workflow edits used the underscore form while the target name uses the + hyphen form. Keeping both names makes the artifact packaging tolerant to + either path without changing the release contents. + """ + out_alias = Path(str(out_dir).replace("linux-x86_64", "linux_x86_64")) + if out_alias != out_dir: + if out_alias.exists(): + shutil.rmtree(out_alias) + shutil.copytree(out_dir, out_alias) + + archive_alias = Path(str(archive).replace("linux-x86_64", "linux_x86_64")) + if archive_alias != archive: + archive_alias.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(archive, archive_alias) + write_archive_checksum(archive_alias) + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--release-dir", type=Path, required=True) @@ -100,7 +124,8 @@ def main() -> None: archive.parent.mkdir(parents=True, exist_ok=True) with tarfile.open(archive, "w:gz") as tar: tar.add(out_dir, arcname=out_dir.name) - write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n") + write_archive_checksum(archive) + create_compatibility_aliases(out_dir, archive) if __name__ == "__main__": From 452354a8e3794b1d34f4a31fc1562802d4b4825c Mon Sep 17 00:00:00 2001 From: IgorRachkov <89467086+igor04091968@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:55:14 +0300 Subject: [PATCH 31/31] fix(ci): copy release binaries without preserving unsupported metadata --- scripts/package_rust_release_binaries.py | 31 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/scripts/package_rust_release_binaries.py b/scripts/package_rust_release_binaries.py index 9e9e469..aef0490 100644 --- a/scripts/package_rust_release_binaries.py +++ b/scripts/package_rust_release_binaries.py @@ -49,27 +49,38 @@ def write(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8") +def copy_release_file(src: Path, dst: Path) -> Path: + """Copy file contents without preserving metadata that some mounts reject.""" + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dst) + try: + dst.chmod(src.stat().st_mode & 0o777) + except PermissionError: + # Some removable/network filesystems reject chmod/utime metadata changes. + # The package remains valid because the archive manifest/checksums are + # based on file contents, not filesystem timestamps. + pass + return dst + + def write_archive_checksum(archive: Path) -> None: write(archive.with_suffix(archive.suffix + ".sha256"), f"{sha256(archive)} {archive.name}\n") def create_compatibility_aliases(out_dir: Path, archive: Path) -> None: - """Create both linux-x86_64 and linux_x86_64 artifact paths. - - Older workflow edits used the underscore form while the target name uses the - hyphen form. Keeping both names makes the artifact packaging tolerant to - either path without changing the release contents. - """ + """Create both linux-x86_64 and linux_x86_64 artifact paths.""" out_alias = Path(str(out_dir).replace("linux-x86_64", "linux_x86_64")) if out_alias != out_dir: if out_alias.exists(): shutil.rmtree(out_alias) - shutil.copytree(out_dir, out_alias) + out_alias.mkdir(parents=True) + for item in out_dir.iterdir(): + if item.is_file(): + copy_release_file(item, out_alias / item.name) archive_alias = Path(str(archive).replace("linux-x86_64", "linux_x86_64")) if archive_alias != archive: - archive_alias.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(archive, archive_alias) + copy_release_file(archive, archive_alias) write_archive_checksum(archive_alias) @@ -94,7 +105,7 @@ def main() -> None: binaries = collect(release_dir) for binary in binaries: - shutil.copy2(binary, out_dir / binary.name) + copy_release_file(binary, out_dir / binary.name) names = [binary.name for binary in binaries] write(out_dir / "BINARIES.txt", "\n".join(names) + "\n")