Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d19b3d478f | ||
|
|
0cd6e4f856 | ||
|
|
acf767360f | ||
|
|
9ad5b2fc34 | ||
|
|
8596cd057b | ||
|
|
42b0fdb718 | ||
|
|
452354a8e3 | ||
|
|
5fb37bfbaf | ||
|
|
cdd8c292db | ||
|
|
9cdad90e3a | ||
|
|
da1a21ee47 | ||
|
|
1fdc13aa73 | ||
|
|
374b320ba4 | ||
|
|
4dbb39b8ee | ||
|
|
26de69b6e2 | ||
|
|
f3d5a8161d |
@@ -0,0 +1,74 @@
|
|||||||
|
name: rust-binary-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
paths:
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
- 'adk-rust/**'
|
||||||
|
- 'scripts/package_rust_release_binaries.py'
|
||||||
|
- '.github/workflows/rust-binary-build.yml'
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
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: Build release binaries
|
||||||
|
run: 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 \
|
||||||
|
--commit "${GITHUB_SHA}" \
|
||||||
|
--ref "${GITHUB_REF}" \
|
||||||
|
--run-id "${GITHUB_RUN_ID}"
|
||||||
|
|
||||||
|
- name: Upload release binaries artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Publish GitHub Release assets
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
files: |
|
||||||
|
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
|
||||||
@@ -25,33 +25,33 @@ jobs:
|
|||||||
|
|
||||||
- name: Install pinned Rust toolchain
|
- name: Install pinned Rust toolchain
|
||||||
run: |
|
run: |
|
||||||
rustup toolchain install 1.85.0 --profile minimal --component rustfmt --component clippy
|
rustup toolchain install 1.94.0 --profile minimal --component rustfmt --component clippy
|
||||||
rustup override set 1.85.0
|
rustup override set 1.94.0
|
||||||
rustup show active-toolchain
|
rustup show active-toolchain
|
||||||
cargo +1.85.0 --version
|
cargo +1.94.0 --version
|
||||||
rustc +1.85.0 --version
|
rustc +1.94.0 --version
|
||||||
|
|
||||||
- name: Cargo fmt check
|
- name: Cargo fmt check
|
||||||
working-directory: adk-rust
|
working-directory: adk-rust
|
||||||
run: cargo +1.85.0 fmt --all -- --check
|
run: cargo +1.94.0 fmt --all -- --check
|
||||||
|
|
||||||
- name: Test detmir-core
|
- name: Test detmir-core
|
||||||
working-directory: adk-rust
|
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
|
- name: Test detmir-portal
|
||||||
working-directory: adk-rust
|
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
|
- name: Clippy detmir-core
|
||||||
working-directory: adk-rust
|
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
|
- name: Clippy detmir-portal with captured log
|
||||||
working-directory: adk-rust
|
working-directory: adk-rust
|
||||||
run: |
|
run: |
|
||||||
set +e
|
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=$?
|
status=$?
|
||||||
echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt
|
echo "clippy_exit_status=${status}" > ../detmir-portal-clippy-status.txt
|
||||||
tail -n 80 ../detmir-portal-clippy.log
|
tail -n 80 ../detmir-portal-clippy.log
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! External command execution helpers for the portal.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: these helpers are intentionally small and side-effect explicit.
|
||||||
|
//! They preserve stdout/stderr error text because readiness verification APIs
|
||||||
|
//! expose command failure diagnostics to operators.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
pub(crate) fn run_in_dir(dir: &Path, command: &mut Command) -> std::result::Result<(), String> {
|
||||||
|
let output = command
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.map_err(|err| format!("run command in {}: {err}", dir.display()))?;
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"{}{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,14 +23,18 @@ use serde_json::{Value, json};
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
|
||||||
|
|
||||||
|
mod command_runner;
|
||||||
mod executive_actions;
|
mod executive_actions;
|
||||||
|
mod portal_roles;
|
||||||
mod production;
|
mod production;
|
||||||
mod risk_narrative;
|
mod risk_narrative;
|
||||||
mod workforce_kpi_explain;
|
mod workforce_kpi_explain;
|
||||||
|
|
||||||
|
use command_runner::run_in_dir;
|
||||||
use executive_actions::{
|
use executive_actions::{
|
||||||
actions_from_center, build_action_center_from_report, filter_actions_for_role,
|
actions_from_center, build_action_center_from_report, filter_actions_for_role,
|
||||||
};
|
};
|
||||||
|
use portal_roles::PortalRole;
|
||||||
use production::{
|
use production::{
|
||||||
build_healthz, build_readyz, build_version, http_request_metadata, is_limited_api_route,
|
build_healthz, build_readyz, build_version, http_request_metadata, is_limited_api_route,
|
||||||
log_http_request, mark_request_started, record_http_metric, record_ingestion_accepted,
|
log_http_request, mark_request_started, record_http_metric, record_ingestion_accepted,
|
||||||
@@ -75,76 +79,6 @@ unsafe extern "C" {
|
|||||||
|
|
||||||
type SnapshotCache = Arc<Mutex<Option<CachedSnapshot>>>;
|
type SnapshotCache = Arc<Mutex<Option<CachedSnapshot>>>;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum PortalRole {
|
|
||||||
Executive,
|
|
||||||
Manager,
|
|
||||||
Security,
|
|
||||||
Forensics,
|
|
||||||
Admin,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PortalRole {
|
|
||||||
fn parse(value: &str) -> Option<Self> {
|
|
||||||
match value.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"executive" | "owner" | "rukovoditel" | "руководитель" => {
|
|
||||||
Some(Self::Executive)
|
|
||||||
}
|
|
||||||
"manager" | "workforce" | "руководитель_подразделения" => {
|
|
||||||
Some(Self::Manager)
|
|
||||||
}
|
|
||||||
"security" | "ib" | "soc" | "безопасность" => Some(Self::Security),
|
|
||||||
"forensics" | "investigation" | "расследования" => Some(Self::Forensics),
|
|
||||||
"admin" | "operations" | "operator" | "эксплуатация" => Some(Self::Admin),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Executive => "executive",
|
|
||||||
Self::Manager => "manager",
|
|
||||||
Self::Security => "security",
|
|
||||||
Self::Forensics => "forensics",
|
|
||||||
Self::Admin => "admin",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn label_ru(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Executive => "Руководитель",
|
|
||||||
Self::Manager => "Руководитель подразделения",
|
|
||||||
Self::Security => "Безопасность",
|
|
||||||
Self::Forensics => "Расследования",
|
|
||||||
Self::Admin => "Администратор",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allowed_scopes(self) -> &'static [&'static str] {
|
|
||||||
match self {
|
|
||||||
Self::Executive => &["executive", "workforce"],
|
|
||||||
Self::Manager => &["executive", "workforce"],
|
|
||||||
Self::Security => &["security", "incidents", "ueba", "pfsense"],
|
|
||||||
Self::Forensics => &["forensics", "incidents", "ueba"],
|
|
||||||
Self::Admin => &[
|
|
||||||
"executive",
|
|
||||||
"workforce",
|
|
||||||
"security",
|
|
||||||
"forensics",
|
|
||||||
"incidents",
|
|
||||||
"ueba",
|
|
||||||
"pfsense",
|
|
||||||
"admin",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_access(self, scope: &str) -> bool {
|
|
||||||
self.allowed_scopes().contains(&scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct CachedSnapshot {
|
struct CachedSnapshot {
|
||||||
created: Instant,
|
created: Instant,
|
||||||
@@ -1878,24 +1812,6 @@ fn read_json_file(path: &Path) -> Result<Value> {
|
|||||||
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
|
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_in_dir(dir: &Path, command: &mut Command) -> std::result::Result<(), String> {
|
|
||||||
let output = command
|
|
||||||
.current_dir(dir)
|
|
||||||
.output()
|
|
||||||
.map_err(|err| format!("run command in {}: {err}", dir.display()))?;
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"{}{}",
|
|
||||||
String::from_utf8_lossy(&output.stdout),
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
)
|
|
||||||
.trim()
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_flag(url: &str, key: &str) -> bool {
|
fn query_flag(url: &str, key: &str) -> bool {
|
||||||
let Some(query) = url.split_once('?').map(|(_, query)| query) else {
|
let Some(query) = url.split_once('?').map(|(_, query)| query) else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! Portal role model and access-scope contract.
|
||||||
|
//!
|
||||||
|
//! CONTRACT: role aliases, serialized values and allowed scopes are part of
|
||||||
|
//! the portal API/security boundary. Keep changes explicit and covered by
|
||||||
|
//! existing role-gate tests in `main.rs`.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub(crate) enum PortalRole {
|
||||||
|
Executive,
|
||||||
|
Manager,
|
||||||
|
Security,
|
||||||
|
Forensics,
|
||||||
|
Admin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PortalRole {
|
||||||
|
pub(crate) fn parse(value: &str) -> Option<Self> {
|
||||||
|
match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"executive" | "owner" | "rukovoditel" | "руководитель" => {
|
||||||
|
Some(Self::Executive)
|
||||||
|
}
|
||||||
|
"manager" | "workforce" | "руководитель_подразделения" => {
|
||||||
|
Some(Self::Manager)
|
||||||
|
}
|
||||||
|
"security" | "ib" | "soc" | "безопасность" => Some(Self::Security),
|
||||||
|
"forensics" | "investigation" | "расследования" => Some(Self::Forensics),
|
||||||
|
"admin" | "operations" | "operator" | "эксплуатация" => Some(Self::Admin),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Executive => "executive",
|
||||||
|
Self::Manager => "manager",
|
||||||
|
Self::Security => "security",
|
||||||
|
Self::Forensics => "forensics",
|
||||||
|
Self::Admin => "admin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn label_ru(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Executive => "Руководитель",
|
||||||
|
Self::Manager => "Руководитель подразделения",
|
||||||
|
Self::Security => "Безопасность",
|
||||||
|
Self::Forensics => "Расследования",
|
||||||
|
Self::Admin => "Администратор",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn allowed_scopes(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::Executive => &["executive", "workforce"],
|
||||||
|
Self::Manager => &["executive", "workforce"],
|
||||||
|
Self::Security => &["security", "incidents", "ueba", "pfsense"],
|
||||||
|
Self::Forensics => &["forensics", "incidents", "ueba"],
|
||||||
|
Self::Admin => &[
|
||||||
|
"executive",
|
||||||
|
"workforce",
|
||||||
|
"security",
|
||||||
|
"forensics",
|
||||||
|
"incidents",
|
||||||
|
"ueba",
|
||||||
|
"pfsense",
|
||||||
|
"admin",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn can_access(self, scope: &str) -> bool {
|
||||||
|
self.allowed_scopes().contains(&scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#!/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 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."""
|
||||||
|
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)
|
||||||
|
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:
|
||||||
|
copy_release_file(archive, archive_alias)
|
||||||
|
write_archive_checksum(archive_alias)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
copy_release_file(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_checksum(archive)
|
||||||
|
create_compatibility_aliases(out_dir, archive)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user