diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94a521f..978ce1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,45 +2,37 @@ name: CI on: + workflow_call: ~ pull_request: + branches: + - main push: branches: - main - tags: - - "v*" -# contents: read covers all jobs. A future release-publishing job will need -# contents: write — set it at the job level there rather than widening here. permissions: contents: read env: - # Derived from the tag (v1.2.3 → 1.2.3) on tag pushes; falls back to - # 0.0.0-dev for branch builds so nfpm always has a valid version string. - VERSION: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '0.0.0-dev' }} # Bump this when a new nfpm release is available. NFPM_VERSION: "2.46.3" +## Concurrency only allowed in the main branch. +## So old builds running for old commits within the same Pull Request are cancelled +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo registry - uses: actions/cache@v6 + - name: Install environment + uses: ./.github/workflows/env-install with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- + components: rustfmt, clippy - name: Check formatting run: cargo fmt --all -- --check @@ -52,73 +44,127 @@ jobs: run: cargo test --all build: - runs-on: ubuntu-latest - needs: test - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + needs: + - test + strategy: + matrix: + os: + - runner: ubuntu-24.04 + target: x86_64-unknown-linux-musl + arch: amd64 + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-musl + arch: arm64 + runs-on: ${{ matrix.os.runner }} steps: - uses: actions/checkout@v7 - - name: Install Rust stable + musl target - uses: dtolnay/rust-toolchain@stable + - name: Install environment + uses: ./.github/workflows/env-install with: - targets: x86_64-unknown-linux-musl + targets: ${{ matrix.os.target }} - name: Install musl tools run: sudo apt-get install -y musl-tools - name: Build static release binary - run: cargo build --release --target x86_64-unknown-linux-musl + run: cargo build --release --target ${{ matrix.os.target }} - - name: Upload binary artifact - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7 with: - name: fix-perms-x86_64-musl - path: target/x86_64-unknown-linux-musl/release/fix-perms + name: fix-perms-${{ matrix.os.arch }} + path: target/${{ matrix.os.target }}/release/fix-perms if-no-files-found: error package: - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + needs: + - build + strategy: + matrix: + os: + - runner: ubuntu-24.04 + target: x86_64-unknown-linux-musl + arch: amd64 + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-musl + arch: arm64 + runs-on: ${{ matrix.os.runner }} steps: - uses: actions/checkout@v7 - - name: Install Rust stable - run: rustup toolchain install stable + - name: Install environment + uses: ./.github/workflows/env-install - - name: Download binary artifact - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v8 with: - name: fix-perms-x86_64-musl - path: target/x86_64-unknown-linux-musl/release/ + name: fix-perms-${{ matrix.os.arch }} + path: target/${{ matrix.os.target }}/release/ - name: Restore binary permissions - run: chmod +x target/x86_64-unknown-linux-musl/release/fix-perms - - - name: Derive package version - id: version - run: | - # Strip the leading 'v' from tag names (v1.2.3 → 1.2.3). - # Both dpkg and rpm reject version strings that start with 'v'. - ref="${GITHUB_REF_NAME}" - echo "value=${ref#v}" >> "$GITHUB_OUTPUT" + run: chmod +x target/${{ matrix.os.target }}/release/fix-perms - name: Install nfpm run: | curl -fsSL \ - "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_amd64.deb" \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_$(dpkg --print-architecture).deb" \ -o /tmp/nfpm.deb sudo dpkg -i /tmp/nfpm.deb rm /tmp/nfpm.deb - name: Build packages - run: cargo xtask build - env: - VERSION: ${{ steps.version.outputs.value }} + run: cargo xtask build --target ${{ matrix.os.target }} + + - uses: actions/attest@v4 + with: + subject-path: "dist/*" - - name: Upload packages - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7 with: - name: ignity-extensions-packages - path: dist/ + name: ignity-extensions-packages-${{ matrix.os.arch }} + path: "dist/*" if-no-files-found: error + + sanitize: + needs: + - package + strategy: + matrix: + os: + - runner: ubuntu-24.04 + arch: amd64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.os.runner }} + steps: + - uses: actions/download-artifact@v8 + with: + name: ignity-extensions-packages-${{ matrix.os.arch }} + path: dist + + - name: Prepare smoke test + run: | + mkdir -p /tmp/smoke/dir + touch /tmp/smoke/dir/file + echo "/tmp/smoke $(id -u):$(id -g) 0600 0700" > /tmp/smoke.spec + + - name: Verify deb install + run: | + sudo dpkg -i dist/ignity-extensions_*.deb + fix-perms < /tmp/smoke.spec + test "$(stat -c %a /tmp/smoke/dir)" = "700" + test "$(stat -c %a /tmp/smoke/dir/file)" = "600" + sudo dpkg -r ignity-extensions + + - name: Verify rpm install + run: | + sudo apt-get install -qy rpm + sudo rpm -i --nodeps dist/ignity-extensions-*.rpm + chmod -R 0777 /tmp/smoke + fix-perms < /tmp/smoke.spec + test "$(stat -c %a /tmp/smoke/dir)" = "700" + test "$(stat -c %a /tmp/smoke/dir/file)" = "600" diff --git a/.github/workflows/env-install/action.yml b/.github/workflows/env-install/action.yml new file mode 100644 index 0000000..72cf115 --- /dev/null +++ b/.github/workflows/env-install/action.yml @@ -0,0 +1,32 @@ +--- +name: Environment installation +description: Install the Rust toolchain and restore the cargo cache +inputs: + targets: + description: "Comma-separated list of additional Rust targets to install" + required: false + default: "" + components: + description: "Comma-separated list of additional Rust components to install" + required: false + default: "" +runs: + using: "composite" + steps: + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ inputs.targets }} + components: ${{ inputs.components }} + + - name: Cache cargo registry + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ inputs.targets }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-cargo-${{ inputs.targets }}- diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 0000000..1120ff2 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,38 @@ +--- +name: Pre release + +on: + workflow_dispatch: + inputs: + version: + description: "The version to release (e.g. 1.2.3). This workflow will automatically perform the required version bumps" + required: true + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} + +jobs: + pre-release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup github actions bot + uses: ./.github/workflows/setup-github-bot + + - name: Install environment + uses: ./.github/workflows/env-install + + - name: Run pre-release step + run: cargo xtask release:pre + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..680893c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +--- +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: "The next iteration version (e.g. 1.2.4-dev). This workflow will automatically perform the required version bumps" + required: true + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} + +jobs: + ci: + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/ci.yml + secrets: inherit + + release: + runs-on: ubuntu-latest + needs: + - ci + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - uses: actions/download-artifact@v8 + with: + pattern: ignity-extensions-packages-* + merge-multiple: true + path: dist + + - name: Install environment + uses: ./.github/workflows/env-install + + - name: Release + env: + GH_TOKEN: ${{ github.token }} + run: cargo xtask release + + post-release: + needs: + - release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup github actions bot + uses: ./.github/workflows/setup-github-bot + + - name: Install environment + uses: ./.github/workflows/env-install + + - name: Run post-release step + run: cargo xtask release:post + env: + GH_TOKEN: ${{ github.token }} + NEXT_VERSION: ${{ inputs.version }} diff --git a/.github/workflows/setup-github-bot/action.yml b/.github/workflows/setup-github-bot/action.yml new file mode 100644 index 0000000..c8b50ac --- /dev/null +++ b/.github/workflows/setup-github-bot/action.yml @@ -0,0 +1,12 @@ +--- +name: Setup github actions bot +description: Setup github actions bot to commit and create PRs + +runs: + using: "composite" + steps: + - name: Setup git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + shell: bash diff --git a/README.md b/README.md index 9d89954..f139f51 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,17 @@ cargo build --release --target x86_64-unknown-linux-musl ``` - The binary at `target/x86_64-unknown-linux-musl/release/fix-perms` has zero runtime dependencies. +- Use `aarch64-unknown-linux-musl` as target to build for `arm64`. + +### Package + +- To build `.deb` and `.rpm` packages from a static binary, you need [nfpm](https://nfpm.goreleaser.com/). + +```bash +cargo xtask build --target x86_64-unknown-linux-musl +``` + +- It will create packages in `dist/` with the version defined in `Cargo.toml`. ## 📖 Usage @@ -142,15 +153,13 @@ path account fmode dmode ### How to create a versioned release -- To release a new version, create and push a tag. - -```bash -git tag v0.1.0 -git push --tags -``` - -- The CI pipeline derives the package version from the tag (`v0.1.0` → `1.0.0`). -- It will build the static binary, then produce a `.deb` and `.rpm` package uploaded as CI artifacts. +- The version lives in the root `Cargo.toml` (`[workspace.package] version`) and is used for the packages. +- Run the `Pre release` workflow with the version to release (e.g. `0.2.0`). +- It opens a PR bumping `Cargo.toml` and `Cargo.lock`, merge it. +- Run the `Release` workflow with the next iteration version (e.g. `0.3.0-dev`). +- It runs the CI, creates the `v0.2.0` tag and a GitHub release with `.deb` and `.rpm` packages for `amd64` and `arm64`. +- It then opens a PR bumping the version to the next iteration, merge it. +- The release steps are also available locally through `cargo xtask release:pre`, `cargo xtask release` and `cargo xtask release:post`. ### How to add a new extension @@ -170,7 +179,7 @@ members = ["crates/fix-perms", "crates/"] ```yaml contents: - - src: target/x86_64-unknown-linux-musl/release/ + - src: target/${TARGET}/release/ dst: /usr/local/bin/ file_info: mode: 0755 diff --git a/nfpm.yaml b/nfpm.yaml index 8d57119..3859327 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ --- name: ignity-extensions version: ${VERSION} -arch: amd64 +arch: ${ARCH} platform: linux maintainer: techcode-io description: | @@ -11,7 +11,7 @@ license: MIT homepage: https://github.com/techcode-io/ignity-extensions contents: - - src: target/x86_64-unknown-linux-musl/release/fix-perms + - src: target/${TARGET}/release/fix-perms dst: /usr/local/bin/fix-perms file_info: mode: 0755 diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 458d14f..dd5395c 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,50 +1,81 @@ use std::env; use std::fs; -use std::path::PathBuf; -use std::process::Command; +use std::path::{Path, PathBuf}; +use std::process::{self, Command}; + +const CARGO_TOML_PATH: &str = "Cargo.toml"; +const DEFAULT_TARGET: &str = "x86_64-unknown-linux-musl"; +const NFPM_CONFIG_PATH: &str = "nfpm.yaml"; fn main() { - let args: Vec = env::args().collect(); - let task = args.get(1).map(|s| s.as_str()).unwrap_or("build"); + let args: Vec = env::args().skip(1).collect(); + let task = args.first().map(|s| s.as_str()).unwrap_or("build"); match task { - "build" => build_packages(), - _ => eprintln!("Unknown task: {}", task), + "build" => build_packages(&args[1..]), + "release:pre" => release_pre(), + "release" => release(), + "release:post" => release_post(), + _ => fail(&format!("Unknown task: {}", task)), } } -fn build_packages() { +fn fail(message: &str) -> ! { + eprintln!("{}", message); + process::exit(1); +} + +fn build_packages(args: &[String]) { + let target = match args { + [] => DEFAULT_TARGET.to_string(), + [flag, value] if flag == "--target" => value.clone(), + _ => fail("Usage: cargo xtask build [--target ]"), + }; + let arch = + target_arch(&target).unwrap_or_else(|| fail(&format!("Unsupported target: {}", target))); + let dist_dir = PathBuf::from("dist"); // Create dist directory fs::create_dir_all(&dist_dir).expect("Failed to create dist directory"); - let version = env::var("VERSION").unwrap_or_else(|_| { - // Get version from Cargo.toml or git - "0.1.0".to_string() - }); + // The VERSION environment variable overrides the workspace version. + let version = env::var("VERSION").unwrap_or_else(|_| project_version()); - println!("Building packages with version: {}", version); + println!( + "Building packages with version: {} ({}, {})", + version, target, arch + ); + + // nfpm doesn't expand environment variables in `contents.src`, so render + // the target triple into a per-arch copy of the config. + let template = fs::read_to_string(NFPM_CONFIG_PATH) + .unwrap_or_else(|_| fail(&format!("The `{}` can't be read.", NFPM_CONFIG_PATH))); + let config = PathBuf::from("target").join(format!("nfpm.{}.yaml", arch)); + fs::create_dir_all("target").expect("Failed to create target directory"); + fs::write(&config, template.replace("${TARGET}", &target)) + .unwrap_or_else(|_| fail(&format!("The `{}` can't be written.", config.display()))); // Build .deb package - run_nfpm("deb", &version); + run_nfpm(&config, "deb", &version, arch); // Build .rpm package - run_nfpm("rpm", &version); + run_nfpm(&config, "rpm", &version, arch); println!("Packages built successfully in {:?}", dist_dir); } -fn run_nfpm(packager: &str, version: &str) { +fn run_nfpm(config: &Path, packager: &str, version: &str, arch: &str) { let output = Command::new("nfpm") .arg("package") .arg("--config") - .arg("nfpm.yaml") + .arg(config) .arg("--packager") .arg(packager) .arg("--target") .arg("dist") .env("VERSION", version) + .env("ARCH", arch) .output() .unwrap_or_else(|_| panic!("Failed to run nfpm for {}", packager)); @@ -54,8 +85,272 @@ fn run_nfpm(packager: &str, version: &str) { packager, String::from_utf8_lossy(&output.stderr) ); - std::process::exit(1); + process::exit(1); } println!("Built {} package", packager); } + +/// Create a release branch and PR to prepare for release. +fn release_pre() { + let version = env::var("RELEASE_VERSION") + .unwrap_or_else(|_| fail("You must define `RELEASE_VERSION` environment variable.")); + if !is_release_version(&version) { + fail("The `RELEASE_VERSION` should match semver format (X.Y.Z)."); + } + + let branch = format!("feat/pre-release-v{}", version); + bump_version_pr( + &branch, + &version, + &format!("release: ignity-extensions v{}", version), + ); +} + +/// Create a release tag and GitHub release with the built packages. +fn release() { + let version = project_version(); + let tag = format!("v{}", version); + + if !run_status("git", &["tag", &tag]) { + fail(&format!("The release `{}` already exists.", tag)); + } + run("git", &["push", "origin", &tag]); + + let mut assets: Vec = fs::read_dir("dist") + .unwrap_or_else(|_| fail("The `dist` directory can't be read.")) + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|path| path.is_file() && is_package(path)) + .map(|path| path.to_string_lossy().into_owned()) + .collect(); + assets.sort(); + if assets.is_empty() { + fail("No packages found in `dist`."); + } + + let title = format!("ignity-extensions {}", tag); + let mut args = vec![ + "release", + "create", + &tag, + "--title", + &title, + "--generate-notes", + "--verify-tag", + ]; + args.extend(assets.iter().map(|s| s.as_str())); + run("gh", &args); +} + +/// Prepare for the next development iteration after release. +fn release_post() { + let version = env::var("NEXT_VERSION") + .unwrap_or_else(|_| fail("You must define `NEXT_VERSION` environment variable.")); + if !is_dev_version(&version) { + fail("The `NEXT_VERSION` should match semver format (X.Y.Z-dev)."); + } + + let branch = format!("feat/post-release-v{}", version); + bump_version_pr(&branch, &version, "chore: prepare for next iteration"); +} + +fn bump_version_pr(branch: &str, version: &str, message: &str) { + run("git", &["checkout", "-b", branch]); + + set_version(version); + + run("git", &["add", "--all"]); + run("git", &["commit", "--no-verify", "-m", message]); + run("git", &["push", "origin", branch]); + run( + "gh", + &[ + "pr", "create", "--base", "main", "--head", branch, "--title", message, "--body", + message, + ], + ); +} + +/// Update the workspace version in Cargo.toml and refresh Cargo.lock. +fn set_version(version: &str) { + let path = Path::new(CARGO_TOML_PATH); + let manifest = fs::read_to_string(path) + .unwrap_or_else(|_| fail(&format!("The `{}` can't be read.", CARGO_TOML_PATH))); + let updated = replace_workspace_version(&manifest, version).unwrap_or_else(|| { + fail(&format!( + "No `[workspace.package]` version found in `{}`.", + CARGO_TOML_PATH + )) + }); + fs::write(path, updated) + .unwrap_or_else(|_| fail(&format!("The `{}` can't be written.", CARGO_TOML_PATH))); + + run("cargo", &["update", "--workspace"]); +} + +fn project_version() -> String { + let manifest = fs::read_to_string(CARGO_TOML_PATH) + .unwrap_or_else(|_| fail(&format!("The `{}` can't be read.", CARGO_TOML_PATH))); + read_workspace_version(&manifest).unwrap_or_else(|| { + fail(&format!( + "No `[workspace.package]` version found in `{}`.", + CARGO_TOML_PATH + )) + }) +} + +fn run(cmd: &str, args: &[&str]) { + if !run_status(cmd, args) { + fail(&format!("Command failed: {} {}", cmd, args.join(" "))); + } +} + +fn run_status(cmd: &str, args: &[&str]) -> bool { + Command::new(cmd) + .args(args) + .status() + .unwrap_or_else(|_| fail(&format!("Failed to run {}", cmd))) + .success() +} + +fn is_package(path: &Path) -> bool { + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + name.starts_with("ignity-extensions") && (name.ends_with(".deb") || name.ends_with(".rpm")) +} + +fn target_arch(target: &str) -> Option<&'static str> { + match target { + "x86_64-unknown-linux-musl" => Some("amd64"), + "aarch64-unknown-linux-musl" => Some("arm64"), + _ => None, + } +} + +fn is_release_version(version: &str) -> bool { + let parts: Vec<&str> = version.split('.').collect(); + parts.len() == 3 + && parts + .iter() + .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())) +} + +fn is_dev_version(version: &str) -> bool { + version.strip_suffix("-dev").is_some_and(is_release_version) +} + +/// Locate the value span of `version = "..."` inside `[workspace.package]`. +fn workspace_version_span(manifest: &str) -> Option<(usize, usize)> { + let mut in_section = false; + let mut offset = 0; + for line in manifest.split_inclusive('\n') { + let trimmed = line.trim(); + if trimmed.starts_with('[') { + in_section = trimmed == "[workspace.package]"; + } else if in_section { + if let Some(rest) = trimmed.strip_prefix("version") { + if let Some(value) = rest.trim_start().strip_prefix('=') { + let value = value.trim(); + if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') { + let start = offset + line.find('"')? + 1; + return Some((start, start + value.len() - 2)); + } + } + } + } + offset += line.len(); + } + None +} + +fn read_workspace_version(manifest: &str) -> Option { + workspace_version_span(manifest).map(|(start, end)| manifest[start..end].to_string()) +} + +fn replace_workspace_version(manifest: &str, version: &str) -> Option { + workspace_version_span(manifest) + .map(|(start, end)| format!("{}{}{}", &manifest[..start], version, &manifest[end..])) +} + +#[cfg(test)] +mod tests { + use super::*; + + const MANIFEST: &str = r#"[workspace] +members = ["crates/fix-perms", "xtask"] + +[workspace.dependencies] +version = "9.9.9" + +[workspace.package] +edition = "2021" +version = "0.1.0" +license = "MIT" +"#; + + #[test] + fn reads_workspace_version() { + assert_eq!(read_workspace_version(MANIFEST).as_deref(), Some("0.1.0")); + } + + #[test] + fn reads_nothing_without_workspace_package() { + assert_eq!( + read_workspace_version("[package]\nversion = \"1.0.0\"\n"), + None + ); + } + + #[test] + fn replaces_only_workspace_version() { + let updated = replace_workspace_version(MANIFEST, "0.2.0-dev").unwrap(); + assert_eq!( + read_workspace_version(&updated).as_deref(), + Some("0.2.0-dev") + ); + assert!(updated.contains("version = \"9.9.9\"")); + assert_eq!(updated, MANIFEST.replace("\"0.1.0\"", "\"0.2.0-dev\"")); + } + + #[test] + fn reads_root_manifest() { + let manifest = + fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("../Cargo.toml")) + .unwrap(); + assert!(read_workspace_version(&manifest).is_some()); + } + + #[test] + fn validates_release_version() { + assert!(is_release_version("0.1.0")); + assert!(is_release_version("10.20.30")); + assert!(!is_release_version("v0.1.0")); + assert!(!is_release_version("0.1")); + assert!(!is_release_version("0.1.0-dev")); + assert!(!is_release_version("0..1")); + } + + #[test] + fn validates_dev_version() { + assert!(is_dev_version("0.2.0-dev")); + assert!(!is_dev_version("0.2.0")); + assert!(!is_dev_version("0.2-dev")); + } + + #[test] + fn maps_target_to_arch() { + assert_eq!(target_arch("x86_64-unknown-linux-musl"), Some("amd64")); + assert_eq!(target_arch("aarch64-unknown-linux-musl"), Some("arm64")); + assert_eq!(target_arch("x86_64-apple-darwin"), None); + } + + #[test] + fn detects_packages() { + assert!(is_package(Path::new( + "dist/ignity-extensions_0.1.0_amd64.deb" + ))); + assert!(is_package(Path::new( + "dist/ignity-extensions-0.1.0-1.aarch64.rpm" + ))); + assert!(!is_package(Path::new("dist/fix-perms"))); + } +}