Compare commits

...
4 changed files with 304 additions and 9 deletions
+74
View File
@@ -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
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
+78
View File
@@ -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.
+143
View File
@@ -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()