diff options
47 files changed, 708 insertions, 878 deletions
diff --git a/.env.template b/.env.template index 86fc934..45cb623 100644 --- a/.env.template +++ b/.env.template @@ -5,5 +5,7 @@ DISCORD_BOT_TOKEN="" BOT_NIXPKGS_PATH="" BOT_NIXPKGS_BRANCHES="staging,staging-next,master,nixpkgs-unstable,nixos-unstable-small,nixos-unstable,nixos-24.05-small,nixos-24.05,nixpkgs-24.05-darwin" -RUST_LOG="bot=debug,warn" +RUST_LOG="git_tracker=debug,discord_bot=debug,warn" +# For production +# RUST_LOG="discord_bot=info,warn" RUST_BACKTRACE=1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 365325d..c3f7a7e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,39 +5,60 @@ on: branches: [main] paths: - "**.nix" - - "flake.lock" - "**.rs" - - "Cargo.toml" + - ".github/workflows/ci.yaml" - "Cargo.lock" + - "Cargo.toml" + - "flake.lock" pull_request: paths: - "**.nix" - - "flake.lock" - "**.rs" - - "Cargo.toml" + - ".github/workflows/ci.yaml" - "Cargo.lock" + - "Cargo.toml" + - "flake.lock" workflow_dispatch: jobs: build: name: Build - runs-on: ubuntu-latest + strategy: + matrix: + os: [macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Run build run: | cargo build --locked --release + nix: + name: Nix + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v13 + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v7 + + - name: Run build + run: nix build --print-build-logs --show-trace + treefmt: name: Treefmt @@ -52,11 +73,11 @@ jobs: - name: Run check run: | - nix flake check --print-build-logs --show-trace + nix fmt -- --fail-on-change release-gate: name: CI Release gate - needs: [build, treefmt] + needs: [build, nix, treefmt] runs-on: ubuntu-latest diff --git a/.github/workflows/clippy.yaml b/.github/workflows/clippy.yaml index 2d3ea70..c94f6ce 100644 --- a/.github/workflows/clippy.yaml +++ b/.github/workflows/clippy.yaml @@ -3,15 +3,17 @@ name: Clippy on: push: paths: - - 'Cargo.toml' - - 'Cargo.lock' - '**.rs' + - '.github/workflows/clippy.yaml' + - 'Cargo.lock' + - 'Cargo.toml' branches: [main] pull_request: paths: - - 'Cargo.toml' - - 'Cargo.lock' - '**.rs' + - '.github/workflows/clippy.yaml' + - 'Cargo.lock' + - 'Cargo.toml' workflow_dispatch: jobs: @@ -27,33 +29,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: "clippy" - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v13 - - name: Install SARIF tools - run: | - cargo install clippy-sarif sarif-fmt - - - name: Fetch Cargo deps - run: | - cargo fetch --locked + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v7 - name: Run Clippy - continue-on-error: true + id: clippy-run run: | - cargo clippy \ - --all-features \ - --all-targets \ - --message-format=json \ - | clippy-sarif | tee /tmp/clippy.sarif | sarif-fmt + nix build --print-build-logs .#checks.x86_64-linux.clippy-sarif + [ -L result ] || exit 1 + echo "sarif-file=$(readlink -f result)" >> "$GITHUB_OUTPUT" - name: Upload results uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: /tmp/clippy.sarif + sarif_file: ${{ steps.clippy-run.outputs.sarif-file }} wait-for-processing: true diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 4fba73a..efce40e 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -5,17 +5,19 @@ on: branches: [main] paths: - "**.nix" - - "flake.lock" - "**.rs" - - "Cargo.toml" + - ".github/workflows/docker.yaml" - "Cargo.lock" + - "Cargo.toml" + - "flake.lock" pull_request: paths: - "**.nix" - - "flake.lock" - "**.rs" - - "Cargo.toml" + - ".github/workflows/docker.yaml" - "Cargo.lock" + - "Cargo.toml" + - "flake.lock" workflow_dispatch: jobs: @@ -25,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - arch: [x86_64, arm64] + arch: [amd64, arm64] runs-on: ubuntu-latest @@ -111,7 +113,7 @@ jobs: env: TAG: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest run: | - architectures=("x86_64" "arm64") + architectures=("amd64" "arm64") for arch in "${architectures[@]}"; do docker load < images/container-"$arch"/*.tar.gz docker tag nixpkgs-tracker-bot:latest-"$arch" "$TAG"-"$arch" @@ -119,7 +121,7 @@ jobs: done docker manifest create "$TAG" \ - --amend "$TAG"-x86_64 \ + --amend "$TAG"-amd64 \ --amend "$TAG"-arm64 docker manifest push "$TAG" @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -171,67 +171,6 @@ dependencies = [ ] [[package]] -name = "bot-client" -version = "0.2.0" -dependencies = [ - "bot-commands", - "bot-config", - "bot-consts", - "bot-error", - "bot-http", - "bot-jobs", - "log", - "serenity", - "tokio", -] - -[[package]] -name = "bot-commands" -version = "0.2.0" -dependencies = [ - "bot-config", - "bot-consts", - "bot-error", - "bot-http", - "git-tracker", - "log", - "serenity", -] - -[[package]] -name = "bot-config" -version = "0.2.0" - -[[package]] -name = "bot-consts" -version = "0.2.0" - -[[package]] -name = "bot-error" -version = "0.2.0" - -[[package]] -name = "bot-http" -version = "0.2.0" -dependencies = [ - "log", - "reqwest 0.12.5", - "serde", -] - -[[package]] -name = "bot-jobs" -version = "0.2.0" -dependencies = [ - "bot-config", - "bot-consts", - "bot-error", - "git2", - "log", - "tokio", -] - -[[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -433,6 +372,20 @@ dependencies = [ ] [[package]] +name = "discord-bot" +version = "0.2.0" +dependencies = [ + "dotenvy", + "env_logger", + "eyre", + "git-tracker", + "log", + "nixpkgs-tracker-http", + "serenity", + "tokio", +] + +[[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -496,6 +449,16 @@ dependencies = [ ] [[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] name = "fastrand" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -650,9 +613,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.5.0", "libc", @@ -923,6 +886,12 @@ dependencies = [ ] [[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -982,9 +951,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -1086,14 +1055,12 @@ dependencies = [ ] [[package]] -name = "nixpkgs-tracker-bot" +name = "nixpkgs-tracker-http" version = "0.2.0" dependencies = [ - "bot-client", - "bot-error", - "dotenvy", - "env_logger", - "tokio", + "log", + "reqwest 0.12.5", + "serde", ] [[package]] @@ -1,32 +1,29 @@ [workspace] +resolver = "2" members = [ - "crates/bot", - "crates/bot-client", - "crates/bot-config", - "crates/bot-consts", - "crates/bot-error", - "crates/bot-http", - "crates/bot-jobs", - "crates/git-tracker" + "crates/*", ] -resolver = "2" + +[workspace.package] +version = "0.2.0" +authors = ["seth <getchoo at tuta dot io>"] +edition = "2021" +repository = "https://github.com/getchoo/nixpkgs-tracker-bot" +license = "MIT" [workspace.dependencies] -bot = { path = "./crates/bot" } -bot-client = { path = "./crates/bot-client" } -bot-commands = { path = "./crates/bot-commands" } -bot-config = { path = "./crates/bot-config" } -bot-consts = { path = "./crates/bot-consts" } -bot-error = { path = "./crates/bot-error" } -bot-http = { path = "./crates/bot-http" } -bot-jobs = { path = "./crates/bot-jobs" } git-tracker = { path = "./crates/git-tracker" } +nixpkgs-tracker-http = { path = "./crates/nixpkgs-tracker-http" } -git2 = { version = "0.18.3", default-features = false } log = "0.4.22" -serenity = { version = "0.12.2", features = ["unstable_discord_api"] } -tokio = { version = "1.39.2", features = [ - "macros", - "rt-multi-thread", - "signal" -] } + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +complexity = "warn" +correctness = "deny" +pedantic = "warn" +perf = "warn" +style = "warn" +suspicious = "deny" diff --git a/crates/bot-client/Cargo.toml b/crates/bot-client/Cargo.toml deleted file mode 100644 index a2ba2a0..0000000 --- a/crates/bot-client/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "bot-client" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Discord client for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] -bot-commands = { workspace = true } -bot-config = { workspace = true } -bot-consts = { workspace = true } -bot-error = { workspace = true } -bot-http = { workspace = true } -bot-jobs = { workspace = true } -log = { workspace = true } -serenity = { workspace = true } -tokio = { workspace = true } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-commands/Cargo.toml b/crates/bot-commands/Cargo.toml deleted file mode 100644 index 3594c70..0000000 --- a/crates/bot-commands/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "bot-commands" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Discord application commands for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] -bot-config = { workspace = true } -bot-consts = { workspace = true } -bot-error = { workspace = true } -bot-http = { workspace = true } -git-tracker = { workspace = true } -log = { workspace = true } -serenity = { workspace = true } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" -# NOTE: THIS ISN'T IN OTHER CRATES BUT IS HERE -# this is because we don't really care about error docs here -# and it could mess with poise's comment system in the future :p -missing-errors-doc = "allow" diff --git a/crates/bot-commands/src/track.rs b/crates/bot-commands/src/track.rs deleted file mode 100644 index 1f22d0e..0000000 --- a/crates/bot-commands/src/track.rs +++ /dev/null @@ -1,121 +0,0 @@ -use bot_config::Config; -use bot_consts::{NIXPKGS_REMOTE, NIXPKGS_URL}; -use bot_error::Error; -use bot_http::{self as http, GithubClientExt}; -use git_tracker::Tracker; - -use log::trace; -use serenity::all::CreateEmbed; -use serenity::builder::{CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup}; -use serenity::model::application::{ - CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue, -}; -use serenity::prelude::Context; - -const REPO_OWNER: &str = "NixOS"; -const REPO_NAME: &str = "nixpkgs"; - -/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs -/// branches in [`branches`], using the repository at path [`repository_path`] -/// -/// # Errors -/// -/// Will return [`Err`] if we can't start tracking a repository at the given path, -/// or if we can't determine if the branch has given commit -fn collect_statuses_in<'a>( - repository_path: &str, - commit_sha: &str, - branches: impl IntoIterator<Item = &'a String>, -) -> Result<Vec<String>, Error> { - // start tracking nixpkgs - let tracker = Tracker::from_path(repository_path)?; - - // check to see what branches it's in - let mut status_results = vec![]; - for branch_name in branches { - trace!("Checking for commit in {branch_name}"); - let full_branch_name = format!("{NIXPKGS_REMOTE}/{branch_name}"); - let has_pr = tracker.branch_contains_sha(&full_branch_name, commit_sha)?; - - if has_pr { - status_results.push(format!("`{branch_name}` ✅")); - } - } - - Ok(status_results) -} - -pub async fn respond( - ctx: &Context, - http: &http::Client, - config: &Config, - command: &CommandInteraction, -) -> Result<(), Error> { - // this will probably take a while - command.defer(&ctx).await?; - - let options = command.data.options(); - let Some(ResolvedOption { - value: ResolvedValue::Integer(pr), - .. - }) = options.first() - else { - let resp = CreateInteractionResponseFollowup::new() - .content("Please provide a valid pull request!"); - command.create_followup(&ctx, resp).await?; - - return Ok(()); - }; - - let Ok(pr_id) = u64::try_from(*pr) else { - let resp = - CreateInteractionResponseFollowup::new().content("PR numbers aren't negative..."); - command.create_followup(&ctx, resp).await?; - - return Ok(()); - }; - - // find out what commit our PR was merged in - let Some(commit_sha) = http.merge_commit_for(REPO_OWNER, REPO_NAME, pr_id).await? else { - let response = CreateInteractionResponseFollowup::new() - .content("It seems this pull request is very old. I can't track it"); - command.create_followup(&ctx, response).await?; - - return Ok(()); - }; - - let status_results = collect_statuses_in( - &config.nixpkgs_path, - &commit_sha, - config.nixpkgs_branches.iter(), - )?; - - // if we don't find the commit in any branches from above, we can pretty safely assume - // it's an unmerged PR - let embed_description: String = if status_results.is_empty() { - "It doesn't look like this PR has been merged yet! (or maybe I just haven't updated)" - .to_string() - } else { - status_results.join("\n") - }; - - let embed = CreateEmbed::new() - .title(format!("Nixpkgs PR #{} Status", *pr)) - .url(format!("{NIXPKGS_URL}/pull/{pr}")) - .description(embed_description); - - let resp = CreateInteractionResponseFollowup::new().embed(embed); - command.create_followup(&ctx, resp).await?; - - Ok(()) -} - -pub fn register() -> CreateCommand { - CreateCommand::new("track") - .description("Track a nixpkgs PR") - .add_integration_type(InstallationContext::User) - .add_option( - CreateCommandOption::new(CommandOptionType::Integer, "pull_request", "PR to track") - .required(true), - ) -} diff --git a/crates/bot-config/Cargo.toml b/crates/bot-config/Cargo.toml deleted file mode 100644 index 57b9a67..0000000 --- a/crates/bot-config/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "bot-config" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Configuration for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-consts/Cargo.toml b/crates/bot-consts/Cargo.toml deleted file mode 100644 index 16d7726..0000000 --- a/crates/bot-consts/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "bot-consts" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Constants for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-error/Cargo.toml b/crates/bot-error/Cargo.toml deleted file mode 100644 index c6f6ed1..0000000 --- a/crates/bot-error/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "bot-error" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Shared Err variant used for (most of) nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-error/src/lib.rs b/crates/bot-error/src/lib.rs deleted file mode 100644 index f34e60e..0000000 --- a/crates/bot-error/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Error = Box<dyn std::error::Error + Send + Sync>; diff --git a/crates/bot-http/Cargo.toml b/crates/bot-http/Cargo.toml deleted file mode 100644 index 0888fda..0000000 --- a/crates/bot-http/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "bot-http" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "HTTP client for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] -log = { workspace = true } -reqwest = { version = "0.12.5", default-features = false, features = ["charset", "http2", "rustls-tls", "json"] } -serde = { version = "1.0.207", features = ["derive"] } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-http/src/github.rs b/crates/bot-http/src/github.rs deleted file mode 100644 index 7822eb8..0000000 --- a/crates/bot-http/src/github.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::{ClientExt as _, Error}; -use crate::model::PullRequest; - -use std::future::Future; - -const GITHUB_API: &str = "https://api.github.com"; - -pub trait ClientExt { - /// Get the commit that merged [`pr`] in [`repo_owner`]/[`repo_name`] - /// - /// # Errors - /// - /// Will return [`Err`] if the merge commit cannot be found - fn merge_commit_for( - &self, - repo_owner: &str, - repo_name: &str, - pr: u64, - ) -> impl Future<Output = Result<Option<String>, Error>> + Send; -} - -impl ClientExt for super::Client { - async fn merge_commit_for( - &self, - repo_owner: &str, - repo_name: &str, - pr: u64, - ) -> Result<Option<String>, Error> { - let url = format!("{GITHUB_API}/repos/{repo_owner}/{repo_name}/pulls/{pr}"); - let resp: PullRequest = self.get_json(&url).await?; - let merge_commit = resp.merge_commit_sha; - - Ok(merge_commit) - } -} diff --git a/crates/bot-http/src/lib.rs b/crates/bot-http/src/lib.rs deleted file mode 100644 index ab32cd4..0000000 --- a/crates/bot-http/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::future::Future; - -use log::trace; -use serde::de::DeserializeOwned; - -mod github; -mod model; -mod teawie; - -pub use github::ClientExt as GithubClientExt; -pub use teawie::ClientExt as TeawieClientExt; - -pub type Client = reqwest::Client; -pub type Response = reqwest::Response; -pub type Error = reqwest::Error; - -/// Fun trait for functions we use with [Client] -pub trait ClientExt { - fn default() -> Self; - fn get_request(&self, url: &str) -> impl Future<Output = Result<Response, Error>> + Send; - fn get_json<T: DeserializeOwned>( - &self, - url: &str, - ) -> impl Future<Output = Result<T, Error>> + Send; -} - -impl ClientExt for Client { - /// Create the default [`Client`] - fn default() -> Self { - reqwest::Client::builder() - .user_agent(format!( - "nixpkgs-tracker-bot/{}", - option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "development") - )) - .build() - .unwrap() - } - - /// Perform a GET request to [`url`] - /// - /// # Errors - /// - /// Will return [`Err`] if the request fails - async fn get_request(&self, url: &str) -> Result<Response, Error> { - trace!("Making GET request to {url}"); - - let resp = self.get(url).send().await?; - resp.error_for_status_ref()?; - - Ok(resp) - } - - /// Perform a GET request to [`url`] and decode the json response - /// - /// # Errors - /// - /// Will return [`Err`] if the request fails or cannot be deserialized - async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, Error> { - let resp = self.get_request(url).await?; - let json = resp.json().await?; - Ok(json) - } -} diff --git a/crates/bot-http/src/teawie.rs b/crates/bot-http/src/teawie.rs deleted file mode 100644 index ea4f53e..0000000 --- a/crates/bot-http/src/teawie.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::{ClientExt as _, Error}; -use crate::model::RandomTeawie; - -use std::future::Future; - -const TEAWIE_API: &str = "https://api.getchoo.com"; - -pub trait ClientExt { - /// Get a random teawie - /// - /// # Errors - /// - /// Will return [`Err`] if the request fails or the response cannot be deserialized - fn random_teawie(&self) -> impl Future<Output = Result<Option<String>, Error>> + Send; -} - -impl ClientExt for super::Client { - async fn random_teawie(&self) -> Result<Option<String>, Error> { - let url = format!("{TEAWIE_API}/random_teawie"); - let resp: RandomTeawie = self.get_json(&url).await?; - - Ok(resp.url) - } -} diff --git a/crates/bot-jobs/Cargo.toml b/crates/bot-jobs/Cargo.toml deleted file mode 100644 index 21b0248..0000000 --- a/crates/bot-jobs/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "bot-jobs" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "Background jobs for nixpkgs-tracker-bot" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -publish = false - -[dependencies] -bot-config = { workspace = true } -bot-consts = { workspace = true } -bot-error = { workspace = true } -git2 = { workspace = true, features = ["https"] } -log = { workspace = true } -tokio = { workspace = true } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/bot-jobs/src/lib.rs b/crates/bot-jobs/src/lib.rs deleted file mode 100644 index d65c929..0000000 --- a/crates/bot-jobs/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -use bot_config::Config; -use bot_error::Error; - -use std::time::Duration; - -use log::error; - -mod repo; - -/// Run our jobs an initial time, then loop them on a separate thread -/// -/// # Errors -/// -/// Will return [`Err`] if any jobs fail -pub fn dispatch(config: Config) -> Result<(), Error> { - repo::fetch_or_update_repository(&config.nixpkgs_path, &config.nixpkgs_branches)?; - - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(repo::TTL_SECS)).await; - if let Err(why) = - repo::fetch_or_update_repository(&config.nixpkgs_path, &config.nixpkgs_branches) - { - error!("Failed to fetch or update repository!\n{why:?}"); - }; - } - }); - - Ok(()) -} diff --git a/crates/bot-jobs/src/repo.rs b/crates/bot-jobs/src/repo.rs deleted file mode 100644 index 4d3e214..0000000 --- a/crates/bot-jobs/src/repo.rs +++ /dev/null @@ -1,77 +0,0 @@ -use bot_consts::{NIXPKGS_REMOTE, NIXPKGS_URL}; -use bot_error::Error; - -use std::{io::Write, path::Path}; - -use git2::{AutotagOption, FetchOptions, RemoteCallbacks, Repository}; -use log::{debug, info, trace, warn}; - -pub const TTL_SECS: u64 = 60 * 5; // 5 minutes - -// much of this is shamelessly lifted from -// https://github.com/rust-lang/git2-rs/blob/9a5c9706ff578c936be644dd1e8fe155bdc4d129/examples/pull.rs - -/// basic set of options for fetching from remotes -fn fetch_options<'a>() -> FetchOptions<'a> { - let mut remote_callbacks = RemoteCallbacks::new(); - remote_callbacks.transfer_progress(|progress| { - if progress.received_objects() == progress.total_objects() { - trace!( - "Resolving deltas {}/{}\r", - progress.indexed_deltas(), - progress.total_deltas() - ); - } else { - trace!( - "Received {}/{} objects ({}) in {} bytes\r", - progress.received_objects(), - progress.total_objects(), - progress.indexed_objects(), - progress.received_bytes() - ); - } - std::io::stdout().flush().ok(); - true - }); - - let mut fetch_opts = FetchOptions::new(); - fetch_opts.remote_callbacks(remote_callbacks); - - fetch_opts -} - -/// update the given branches in the [`repository`] using the nixpkgs remote -fn update_branches_in(repository: &Repository, branches: &[String]) -> Result<(), Error> { - let mut remote = repository.find_remote(NIXPKGS_REMOTE)?; - // download all the refs - remote.download(branches, Some(&mut fetch_options()))?; - remote.disconnect()?; - // and (hopefully) update what they refer to for later - remote.update_tips(None, true, AutotagOption::Auto, None)?; - - Ok(()) -} - -pub fn fetch_or_update_repository(path: &str, branches: &[String]) -> Result<(), Error> { - // Open our repository or clone it if it doesn't exist - let path = Path::new(path); - let repository = if path.exists() { - Repository::open(path)? - } else { - warn!( - "Couldn't find repository at {}! Cloning a fresh one from {NIXPKGS_URL}", - path.display() - ); - Repository::clone(NIXPKGS_URL, path)?; - info!("Finished cloning to {}", path.display()); - - // bail early as we already have a fresh copy - return Ok(()); - }; - - debug!("Updating repository at {}", path.display()); - update_branches_in(&repository, branches)?; - debug!("Finished updating!"); - - Ok(()) -} diff --git a/crates/bot/Cargo.toml b/crates/bot/Cargo.toml deleted file mode 100644 index 69feace..0000000 --- a/crates/bot/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "nixpkgs-tracker-bot" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "A small Discord app that helps you track where nixpkgs PRs have reached" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" - -[dependencies] -bot-error = { workspace = true } -bot-client = { workspace = true } -dotenvy = "0.15.7" -env_logger = "0.11.5" -tokio = { workspace = true } - -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" diff --git a/crates/discord-bot/Cargo.toml b/crates/discord-bot/Cargo.toml new file mode 100644 index 0000000..dd35306 --- /dev/null +++ b/crates/discord-bot/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "discord-bot" +version.workspace = true +authors.workspace = true +edition.workspace = true +description = "Small Discord app that helps you track where nixpkgs PRs have reached" +repository.workspace = true +license.workspace = true + +publish = false + +[[bin]] +name = "nixpkgs-tracker-bot" +path = "src/main.rs" + +[dependencies] +dotenvy = "0.15.7" +env_logger = "0.11.5" +eyre = "0.6.12" +git-tracker.workspace = true +log.workspace = true +nixpkgs-tracker-http.workspace = true +serenity = { version = "0.12.2", features = ["unstable_discord_api"] } +tokio = { version = "1.39.2", features = [ + "macros", + "rt-multi-thread", + "signal" +] } + +[lints] +workspace = true diff --git a/crates/bot-commands/src/about.rs b/crates/discord-bot/src/commands/about.rs index 2e5efae..e663faf 100644 --- a/crates/bot-commands/src/about.rs +++ b/crates/discord-bot/src/commands/about.rs @@ -1,6 +1,9 @@ -use bot_error::Error; -use bot_http::TeawieClientExt; +use std::sync::Arc; +use crate::http::TeawieClientExt; + +use eyre::Result; +use log::warn; use serenity::builder::{ CreateCommand, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage, @@ -11,11 +14,10 @@ use serenity::prelude::Context; const VERSION: &str = env!("CARGO_PKG_VERSION"); const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); -pub async fn respond( - ctx: &Context, - http: &bot_http::Client, - command: &CommandInteraction, -) -> Result<(), Error> { +pub async fn respond<T>(ctx: &Context, http: &Arc<T>, command: &CommandInteraction) -> Result<()> +where + T: TeawieClientExt, +{ let mut embed = CreateEmbed::new() .title("About nixpkgs-tracker-bot") .description("I help track what branches PRs to nixpkgs have reached. If you've used [Nixpkgs Pull Request Tracker](https://nixpk.gs/pr-tracker.html), you probably know what this is about.") @@ -25,9 +27,13 @@ pub async fn respond( ("Issues/Feature Requests", &format!("[getchoo/nixpkgs-tracker-bot/issues]({REPOSITORY}/issues)"), true) ]); - if let Some(teawie_url) = http.random_teawie().await? { + let random_teawie = http.random_teawie().await?; + + if let Some(teawie_url) = random_teawie.url { let footer = CreateEmbedFooter::new("Images courtesy of @sympathytea"); embed = embed.image(teawie_url).footer(footer); + } else if let Some(error) = random_teawie.error { + warn!("Error from TeawieAPI: {error:#?}"); }; let message = CreateInteractionResponseMessage::new().embed(embed); diff --git a/crates/bot-commands/src/lib.rs b/crates/discord-bot/src/commands/mod.rs index 79fce17..79fce17 100644 --- a/crates/bot-commands/src/lib.rs +++ b/crates/discord-bot/src/commands/mod.rs diff --git a/crates/bot-commands/src/ping.rs b/crates/discord-bot/src/commands/ping.rs index b18a0b6..30150dc 100644 --- a/crates/bot-commands/src/ping.rs +++ b/crates/discord-bot/src/commands/ping.rs @@ -1,12 +1,11 @@ -use bot_error::Error; - +use eyre::Result; use serenity::builder::{ CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, }; use serenity::model::application::{CommandInteraction, InstallationContext}; use serenity::prelude::Context; -pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> { +pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<()> { let message = CreateInteractionResponseMessage::new().content("Pong!"); let response = CreateInteractionResponse::Message(message); command.create_response(&ctx, response).await?; diff --git a/crates/discord-bot/src/commands/track.rs b/crates/discord-bot/src/commands/track.rs new file mode 100644 index 0000000..f071ebf --- /dev/null +++ b/crates/discord-bot/src/commands/track.rs @@ -0,0 +1,133 @@ +use crate::{config::Config, consts::NIXPKGS_REMOTE, http::GitHubClientExt}; + +use std::sync::Arc; + +use eyre::Result; +use log::debug; +use serenity::builder::{ + CreateCommand, CreateCommandOption, CreateEmbed, CreateInteractionResponseFollowup, +}; +use serenity::model::{ + application::{ + CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue, + }, + Timestamp, +}; +use serenity::prelude::Context; + +const REPO_OWNER: &str = "NixOS"; +const REPO_NAME: &str = "nixpkgs"; + +pub async fn respond<T>( + ctx: &Context, + http: &Arc<T>, + config: &Config, + command: &CommandInteraction, +) -> Result<()> +where + T: GitHubClientExt, +{ + // this will probably take a while + command.defer(&ctx).await?; + + let options = command.data.options(); + let Some(ResolvedOption { + value: ResolvedValue::Integer(pr), + .. + }) = options.first() + else { + let resp = CreateInteractionResponseFollowup::new() + .content("PR numbers aren't negative or that big..."); + command.create_followup(&ctx, resp).await?; + + return Ok(()); + }; + + let Ok(id) = u64::try_from(*pr) else { + let resp = CreateInteractionResponseFollowup::new() + .content("PR numbers aren't negative or that big..."); + command.create_followup(&ctx, resp).await?; + + return Ok(()); + }; + + // find out what commit our PR was merged in + let pull_request = http.pull_request(REPO_OWNER, REPO_NAME, id).await?; + if !pull_request.merged { + let response = CreateInteractionResponseFollowup::new() + .content("It looks like that PR isn't merged yet! Try again when it is 😄"); + command.create_followup(&ctx, response).await?; + + return Ok(()); + } + + // seems older PRs may not have this + let Some(commit_sha) = pull_request.merge_commit_sha else { + let response = CreateInteractionResponseFollowup::new() + .content("It seems this pull request is very old. I can't track it"); + command.create_followup(&ctx, response).await?; + + return Ok(()); + }; + + let status_results = git_tracker::collect_statuses_in( + &config.nixpkgs_path, + &commit_sha, + &config.nixpkgs_branches, + )?; + + // find branches containing our PR and trim the remote ref prefix + let found_branches: Vec<String> = status_results + .iter() + .filter(|&(_, has_pr)| *has_pr) + .map(|(branch_name, _)| { + // remove the ref prefix that we add in our Config struct + let start_pos = format!("{NIXPKGS_REMOTE}/").len(); + branch_name[start_pos..].to_string() + }) + .collect(); + + // if we didn't find any, bail + if found_branches.is_empty() { + let response = CreateInteractionResponseFollowup::new() + .content("This PR has been merged...but I can't seem to find it anywhere. I might not be tracking it's base branch"); + command.create_followup(&ctx, response).await?; + + return Ok(()); + } + + let mut embed = CreateEmbed::new() + .title(format!("Nixpkgs PR #{} Status", pull_request.number)) + .url(&pull_request.html_url) + .description(&pull_request.title) + .fields( + found_branches + .iter() + .map(|branch_name| (branch_name, "✅", true)), + ); + + if let Some(merged_at) = pull_request.merged_at { + if let Ok(timestamp) = Timestamp::parse(&merged_at) { + embed = embed.timestamp(timestamp); + } else { + debug!("Couldn't parse timestamp from GitHub! Ignoring."); + } + } else { + debug!("Couldn't find `merged_at` information for a supposedly merged PR! Ignoring."); + } + + let resp = CreateInteractionResponseFollowup::new().embed(embed); + command.create_followup(&ctx, resp).await?; + + Ok(()) +} + +pub fn register() -> CreateCommand { + CreateCommand::new("track") + .description("Track a nixpkgs PR") + .add_integration_type(InstallationContext::User) + .add_option( + CreateCommandOption::new(CommandOptionType::Integer, "pull_request", "PR to track") + .required(true), + ) +} diff --git a/crates/bot-config/src/lib.rs b/crates/discord-bot/src/config.rs index 0691884..5076eb9 100644 --- a/crates/bot-config/src/lib.rs +++ b/crates/discord-bot/src/config.rs @@ -1,3 +1,5 @@ +use crate::consts::NIXPKGS_REMOTE; + use std::env; /// The Discord client's configuration @@ -14,7 +16,7 @@ impl Config { fn split_string_list(branches: &str) -> Vec<String> { branches .split(',') - .map(|branch| branch.trim().to_string()) + .map(|branch| format!("{NIXPKGS_REMOTE}/{}", branch.trim())) .collect() } diff --git a/crates/bot-consts/src/lib.rs b/crates/discord-bot/src/consts.rs index 9396da0..9396da0 100644 --- a/crates/bot-consts/src/lib.rs +++ b/crates/discord-bot/src/consts.rs diff --git a/crates/bot-client/src/handler.rs b/crates/discord-bot/src/handler.rs index 2cd0082..4abb99b 100644 --- a/crates/bot-client/src/handler.rs +++ b/crates/discord-bot/src/handler.rs @@ -1,6 +1,6 @@ -use crate::{SharedConfig, SharedHttp}; -use bot_error::Error; +use crate::{commands, SharedConfig, SharedHttp}; +use eyre::{OptionExt, Result}; use log::{debug, error, info, trace, warn}; use serenity::all::CreateBotAuthParameters; use serenity::async_trait; @@ -19,8 +19,8 @@ use serenity::prelude::{Context, EventHandler}; pub struct Handler; impl Handler { - async fn register_commands(&self, ctx: &Context) -> Result<(), Error> { - let commands = bot_commands::to_vec(); + async fn register_commands(&self, ctx: &Context) -> Result<()> { + let commands = commands::to_vec(); let commands_len = commands.len(); for command in commands { Command::create_global_command(&ctx.http, command).await?; @@ -31,7 +31,7 @@ impl Handler { } /// Dispatch our commands from a [`CommandInteraction`] - async fn dispatch_command(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> { + async fn dispatch_command(ctx: &Context, command: &CommandInteraction) -> Result<()> { let command_name = command.data.name.as_str(); // grab our configuration & http client from the aether @@ -39,19 +39,19 @@ impl Handler { let read = ctx.data.read().await; let http = read .get::<SharedHttp>() - .ok_or("Couldn't get shared HTTP client! WHY??????")? + .ok_or_eyre("Couldn't get shared HTTP client! WHY??????")? .clone(); let config = read .get::<SharedConfig>() - .ok_or("Couldn't get shared bot configuration!")? + .ok_or_eyre("Couldn't get shared bot configuration!")? .clone(); (http, config) }; match command_name { - "about" => bot_commands::about::respond(ctx, &http, command).await?, - "ping" => bot_commands::ping::respond(ctx, command).await?, - "track" => bot_commands::track::respond(ctx, &http, &config, command).await?, + "about" => commands::about::respond(ctx, &http, command).await?, + "ping" => commands::ping::respond(ctx, command).await?, + "track" => commands::track::respond(ctx, &http, &config, command).await?, _ => { let message = CreateInteractionResponseMessage::new().content(format!( "It doesn't look like you can use `{command_name}`. Sorry :(" diff --git a/crates/discord-bot/src/jobs.rs b/crates/discord-bot/src/jobs.rs new file mode 100644 index 0000000..40d34cc --- /dev/null +++ b/crates/discord-bot/src/jobs.rs @@ -0,0 +1,36 @@ +use crate::{config::Config, consts::NIXPKGS_REMOTE, consts::NIXPKGS_URL}; + +use std::{path::Path, time::Duration}; + +use eyre::Result; +use git_tracker::ManagedRepository; +use log::error; + +const TTL_SECS: u64 = 60 * 5; // 5 minutes + +/// Run our jobs an initial time, then loop them on a separate thread +/// +/// # Errors +/// +/// Will return [`Err`] if any jobs fail +pub fn dispatch(config: Config) -> Result<()> { + let managed_repository = ManagedRepository { + path: Path::new(&config.nixpkgs_path).to_path_buf(), + tracked_branches: config.nixpkgs_branches, + upstream_remote_url: NIXPKGS_URL.to_string(), + upstream_remote_name: NIXPKGS_REMOTE.to_string(), + }; + + managed_repository.fetch_or_update()?; + + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_secs(TTL_SECS)).await; + if let Err(why) = managed_repository.fetch_or_update() { + error!("Could not fetch or update repository!\n{why:?}"); + }; + } + }); + + Ok(()) +} diff --git a/crates/bot-client/src/lib.rs b/crates/discord-bot/src/lib.rs index 851b853..7ac2e98 100644 --- a/crates/bot-client/src/lib.rs +++ b/crates/discord-bot/src/lib.rs @@ -1,15 +1,18 @@ -use bot_config::Config; -use bot_error::Error; -use bot_http as http; - use std::sync::Arc; +use eyre::Result; use log::trace; use serenity::prelude::{Client, GatewayIntents, TypeMapKey}; +mod commands; +mod config; +mod consts; mod handler; +mod jobs; +use config::Config; use handler::Handler; +use nixpkgs_tracker_http as http; /// Container for [`http::Client`] struct SharedHttp; @@ -26,7 +29,7 @@ impl TypeMapKey for SharedConfig { } /// Fetch our bot token -fn token() -> Result<String, Error> { +fn token() -> Result<String> { let token = std::env::var("DISCORD_BOT_TOKEN")?; Ok(token) } @@ -41,7 +44,7 @@ fn token() -> Result<String, Error> { /// # Panics /// /// Will [`panic!`] if the bot token isn't found or the ctrl+c handler can't be made -pub async fn get() -> Result<Client, Error> { +pub async fn client() -> Result<Client> { let token = token().expect("Couldn't find token in environment! Is DISCORD_BOT_TOKEN set?"); let intents = GatewayIntents::default(); @@ -51,7 +54,7 @@ pub async fn get() -> Result<Client, Error> { .await?; // add state stuff - let http_client = <http::Client as http::ClientExt>::default(); + let http_client = <http::Client as http::Ext>::default(); let config = Config::from_env()?; { @@ -73,7 +76,7 @@ pub async fn get() -> Result<Client, Error> { }); // run our jobs - bot_jobs::dispatch(config)?; + jobs::dispatch(config)?; Ok(client) } diff --git a/crates/bot/src/main.rs b/crates/discord-bot/src/main.rs index 390e79b..acbf9fd 100644 --- a/crates/bot/src/main.rs +++ b/crates/discord-bot/src/main.rs @@ -1,9 +1,9 @@ #[tokio::main] -async fn main() -> Result<(), bot_error::Error> { +async fn main() -> eyre::Result<()> { dotenvy::dotenv().ok(); env_logger::try_init()?; - let mut client = bot_client::get().await?; + let mut client = discord_bot::client().await?; client.start().await?; Ok(()) diff --git a/crates/git-tracker/Cargo.toml b/crates/git-tracker/Cargo.toml index 09584cb..4805502 100644 --- a/crates/git-tracker/Cargo.toml +++ b/crates/git-tracker/Cargo.toml @@ -1,27 +1,17 @@ [package] name = "git-tracker" -version = "0.2.0" -edition = "2021" - -authors = ["seth <getchoo at tuta dot io>"] -description = "A library that helps you track commits and branches in a Git repository" -repository = "https://github.com/getchoo/nixpkgs-tracker-bot" +version.workspace = true +edition.workspace = true +authors.workspace = true +description = "Library that helps you track commits and branches in a Git repository" +repository.workspace = true publish = false [dependencies] -git2 = { workspace = true } -log = { workspace = true } +git2 = { version = "0.19.0", default-features = false, features = ["https"] } +log.workspace = true thiserror = "1.0.63" -[lints.rust] -async_fn_in_trait = "allow" -unsafe_code = "forbid" - -[lints.clippy] -complexity = "warn" -correctness = "deny" -pedantic = "warn" -perf = "warn" -style = "warn" -suspicious = "deny" +[lints] +workspace = true diff --git a/crates/git-tracker/src/lib.rs b/crates/git-tracker/src/lib.rs index cb0907b..0bf17dc 100644 --- a/crates/git-tracker/src/lib.rs +++ b/crates/git-tracker/src/lib.rs @@ -1,4 +1,35 @@ //! A library that helps you track commits and branches in a Git repository +use log::trace; +mod managed_repository; mod tracker; -pub use tracker::{Error, Tracker}; +pub use managed_repository::ManagedRepository; +pub use tracker::Tracker; + +/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs +/// branches in [`branches`], using the repository at path [`repository_path`] +/// +/// NOTE: `branches` should contain the full ref (i.e., `origin/main`) +/// +/// # Errors +/// +/// Will return [`Err`] if we can't start tracking a repository at the given path, +/// or if we can't determine if the branch has given commit +pub fn collect_statuses_in( + repository_path: &str, + commit_sha: &str, + branches: &Vec<String>, +) -> Result<Vec<(String, bool)>, tracker::Error> { + // start tracking nixpkgs + let tracker = Tracker::from_path(repository_path)?; + + // check to see what branches it's in + let mut status_results = Vec::new(); + for branch_name in branches { + trace!("Checking for commit in {branch_name}"); + let has_pr = tracker.branch_contains_sha(branch_name, commit_sha)?; + status_results.push((branch_name.to_string(), has_pr)); + } + + Ok(status_results) +} diff --git a/crates/git-tracker/src/managed_repository.rs b/crates/git-tracker/src/managed_repository.rs new file mode 100644 index 0000000..0a41bd0 --- /dev/null +++ b/crates/git-tracker/src/managed_repository.rs @@ -0,0 +1,95 @@ +use git2::{AutotagOption, FetchOptions, RemoteCallbacks, RemoteUpdateFlags, Repository}; +use log::{debug, info, trace, warn}; +use std::{io::Write, path::PathBuf}; + +// much of this is shamelessly lifted from +// https://github.com/rust-lang/git2-rs/blob/9a5c9706ff578c936be644dd1e8fe155bdc4d129/examples/pull.rs + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("libgit2 error")] + Git(#[from] git2::Error), +} + +pub struct ManagedRepository { + pub path: PathBuf, + pub tracked_branches: Vec<String>, + pub upstream_remote_url: String, + pub upstream_remote_name: String, +} + +impl ManagedRepository { + /// basic set of options for fetching from remotes + fn fetch_options<'a>() -> FetchOptions<'a> { + let mut remote_callbacks = RemoteCallbacks::new(); + remote_callbacks.transfer_progress(|progress| { + if progress.received_objects() == progress.total_objects() { + trace!( + "Resolving deltas {}/{}\r", + progress.indexed_deltas(), + progress.total_deltas() + ); + } else { + trace!( + "Received {}/{} objects ({}) in {} bytes\r", + progress.received_objects(), + progress.total_objects(), + progress.indexed_objects(), + progress.received_bytes() + ); + } + std::io::stdout().flush().ok(); + true + }); + + let mut fetch_opts = FetchOptions::new(); + fetch_opts.remote_callbacks(remote_callbacks); + + fetch_opts + } + + /// Update the given branches in the [`repository`] using the nixpkgs remote + fn update_branches_in(&self, repository: &Repository) -> Result<(), Error> { + let mut remote = repository.find_remote(&self.upstream_remote_name)?; + // download all the refs + remote.download(&self.tracked_branches, Some(&mut Self::fetch_options()))?; + remote.disconnect()?; + // and (hopefully) update what they refer to for later + remote.update_tips( + None, + RemoteUpdateFlags::UPDATE_FETCHHEAD, + AutotagOption::Auto, + None, + )?; + + Ok(()) + } + + /// Fetch the repository or update it if it exists + /// + /// # Errors + /// Will return [`Err`] if the repository cannot be opened, cloned, or updated + pub fn fetch_or_update(&self) -> Result<(), Error> { + // Open our repository or clone it if it doesn't exist + let repository = if self.path.exists() { + Repository::open(self.path.as_path())? + } else { + warn!( + "Couldn't find repository at {}! Cloning a fresh one from {}", + self.path.display(), + self.upstream_remote_url + ); + Repository::clone(&self.upstream_remote_url, self.path.as_path())?; + info!("Finished cloning to {}", self.path.display()); + + // bail early as we already have a fresh copy + return Ok(()); + }; + + debug!("Updating repository at {}", self.path.display()); + self.update_branches_in(&repository)?; + debug!("Finished updating!"); + + Ok(()) + } +} diff --git a/crates/git-tracker/src/tracker.rs b/crates/git-tracker/src/tracker.rs index e26f82d..e6a3f54 100644 --- a/crates/git-tracker/src/tracker.rs +++ b/crates/git-tracker/src/tracker.rs @@ -2,6 +2,11 @@ use std::path::Path; use git2::{Branch, BranchType, Commit, ErrorCode, Oid, Reference, Repository}; +/// Helper struct for tracking Git objects +pub struct Tracker { + repository: Repository, +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("libgit2 error")] @@ -10,11 +15,6 @@ pub enum Error { RepositoryPathNotFound(String), } -/// Helper struct for tracking Git objects -pub struct Tracker { - repository: Repository, -} - impl Tracker { /// Create a new [`Tracker`] using the repository at [`path`] /// @@ -76,7 +76,7 @@ impl Tracker { .repository .graph_descendant_of(head.id(), commit.id())?; - Ok(has_commit || is_head) + Ok(is_head || has_commit) } /// Check if a [`Branch`] named [`branch_name`] has a commit with the SHA [`commit_sha`] diff --git a/crates/nixpkgs-tracker-http/Cargo.toml b/crates/nixpkgs-tracker-http/Cargo.toml new file mode 100644 index 0000000..45b897d --- /dev/null +++ b/crates/nixpkgs-tracker-http/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nixpkgs-tracker-http" +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +publish = false + +[dependencies] +log.workspace = true +reqwest = { version = "0.12.5", default-features = false, features = [ + "charset", + "http2", + "rustls-tls", + "json" +] } +serde = { version = "1.0.204", features = ["derive"] } + +[lints] +workspace = true diff --git a/crates/nixpkgs-tracker-http/src/github.rs b/crates/nixpkgs-tracker-http/src/github.rs new file mode 100644 index 0000000..12ee832 --- /dev/null +++ b/crates/nixpkgs-tracker-http/src/github.rs @@ -0,0 +1,40 @@ +use super::{Error, PullRequest}; + +use std::future::Future; + +use log::trace; + +const GITHUB_API: &str = "https://api.github.com"; + +pub trait Ext { + /// GET `/repos/{repo_owner}/{repo_name}/pulls/{id}` + /// + /// # Errors + /// + /// Will return [`Err`] if the merge commit cannot be found + fn pull_request( + &self, + repo_owner: &str, + repo_name: &str, + id: u64, + ) -> impl Future<Output = Result<PullRequest, Error>> + Send; +} + +impl Ext for super::Client { + async fn pull_request( + &self, + repo_owner: &str, + repo_name: &str, + id: u64, + ) -> Result<PullRequest, Error> { + let url = format!("{GITHUB_API}/repos/{repo_owner}/{repo_name}/pulls/{id}"); + + let request = self.get(&url).build()?; + trace!("Making GET request to `{}`", request.url()); + let response = self.execute(request).await?; + response.error_for_status_ref()?; + let pull_request: PullRequest = response.json().await?; + + Ok(pull_request) + } +} diff --git a/crates/nixpkgs-tracker-http/src/lib.rs b/crates/nixpkgs-tracker-http/src/lib.rs new file mode 100644 index 0000000..873ebb8 --- /dev/null +++ b/crates/nixpkgs-tracker-http/src/lib.rs @@ -0,0 +1,28 @@ +mod github; +mod model; +mod teawie; + +pub use github::Ext as GitHubClientExt; +pub use model::*; +pub use teawie::Ext as TeawieClientExt; + +pub type Client = reqwest::Client; +pub type Error = reqwest::Error; + +/// Fun trait for functions we use with [Client] +pub trait Ext { + fn default() -> Self; +} + +impl Ext for Client { + /// Create the default [`Client`] + fn default() -> Self { + reqwest::Client::builder() + .user_agent(format!( + "nixpkgs-tracker-bot/{}", + option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "development") + )) + .build() + .unwrap() + } +} diff --git a/crates/bot-http/src/model.rs b/crates/nixpkgs-tracker-http/src/model.rs index afd4717..9439538 100644 --- a/crates/bot-http/src/model.rs +++ b/crates/nixpkgs-tracker-http/src/model.rs @@ -3,6 +3,11 @@ use serde::Deserialize; /// Bad version of `/repos/{owner}/{repo}/pulls/{pull_number}` for Github's api #[derive(Clone, Debug, Deserialize)] pub struct PullRequest { + pub html_url: String, + pub number: u64, + pub title: String, + pub merged: bool, + pub merged_at: Option<String>, pub merge_commit_sha: Option<String>, } @@ -10,4 +15,5 @@ pub struct PullRequest { #[derive(Clone, Debug, Deserialize)] pub struct RandomTeawie { pub url: Option<String>, + pub error: Option<String>, } diff --git a/crates/nixpkgs-tracker-http/src/teawie.rs b/crates/nixpkgs-tracker-http/src/teawie.rs new file mode 100644 index 0000000..97af63c --- /dev/null +++ b/crates/nixpkgs-tracker-http/src/teawie.rs @@ -0,0 +1,30 @@ +use crate::{Error, RandomTeawie}; + +use std::future::Future; + +use log::trace; + +const TEAWIE_API: &str = "https://api.getchoo.com"; + +pub trait Ext { + /// Get a random teawie + /// + /// # Errors + /// + /// Will return [`Err`] if the request fails or the response cannot be deserialized + fn random_teawie(&self) -> impl Future<Output = Result<RandomTeawie, Error>> + Send; +} + +impl Ext for super::Client { + async fn random_teawie(&self) -> Result<RandomTeawie, Error> { + let url = format!("{TEAWIE_API}/random_teawie"); + + let request = self.get(&url).build()?; + trace!("Making GET request to {}", request.url()); + let response = self.execute(request).await?; + response.error_for_status_ref()?; + let random_teawie: RandomTeawie = response.json().await?; + + Ok(random_teawie) + } +} @@ -1,26 +1,5 @@ { "nodes": { - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": [] - }, - "locked": { - "lastModified": 1722580276, - "narHash": "sha256-VaNcSh7n8OaFW/DJsR6Fm23V+EGpSei0DyF71RKB+90=", - "owner": "nix-community", - "repo": "fenix", - "rev": "286f371b3cfeaa5c856c8e6dfb893018e86cc947", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1722415718, @@ -39,7 +18,6 @@ }, "root": { "inputs": { - "fenix": "fenix", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } @@ -4,13 +4,8 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - fenix = { - url = "github:nix-community/fenix"; - inputs = { - nixpkgs.follows = "nixpkgs"; - rust-analyzer-src.follows = ""; - }; - }; + # Inputs below this are optional + # `inputs.treefmt-nix.follows = ""` treefmt-nix = { url = "github:numtide/treefmt-nix"; @@ -22,7 +17,6 @@ { self, nixpkgs, - fenix, treefmt-nix, }: let @@ -39,21 +33,49 @@ treefmtFor = forAllSystems (system: treefmt-nix.lib.evalModule nixpkgsFor.${system} ./treefmt.nix); in { - checks = forAllSystems (system: { - treefmt = treefmtFor.${system}.config.build.check self; - }); + checks = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + clippy-sarif = pkgs.stdenv.mkDerivation { + name = "check-clippy-sarif"; + inherit (self.packages.${system}.nixpkgs-tracker-bot) src cargoDeps; + + nativeBuildInputs = with pkgs; [ + cargo + clippy + clippy-sarif + pkg-config + rustPlatform.cargoSetupHook + rustc + sarif-fmt + ]; + + buildInputs = [ pkgs.openssl ]; + + buildPhase = '' + cargo clippy \ + --all-features \ + --all-targets \ + --tests \ + --message-format=json \ + | clippy-sarif | tee $out | sarif-fmt + ''; + }; + + treefmt = treefmtFor.${system}.config.build.check self; + } + ); devShells = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; - inputsFrom = [ self.packages.${system}.nixpkgs-tracker-bot ]; in { default = pkgs.mkShell { - inherit inputsFrom; - RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; - packages = [ pkgs.clippy pkgs.rustfmt @@ -61,14 +83,11 @@ self.formatter.${system} ]; - }; - ci = pkgs.mkShell { - inherit inputsFrom; - packages = [ - pkgs.clippy - pkgs.rustfmt - ]; + inputsFrom = [ self.packages.${system}.nixpkgs-tracker-bot ]; + env = { + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; }; } ); @@ -81,37 +100,21 @@ system: let pkgs = nixpkgsFor.${system}; - packages = self.packages.${system}; - - mkStaticWith = pkgs.callPackage ./nix/static.nix { - inherit (packages) nixpkgs-tracker-bot; - fenix = fenix.packages.${system}; - }; + packages' = self.packages.${system}; - containerWith = - nixpkgs-tracker-bot: - let - arch = nixpkgs-tracker-bot.stdenv.hostPlatform.ubootArch; - in - pkgs.dockerTools.buildLayeredImage { - name = "nixpkgs-tracker-bot"; - tag = "latest-${arch}"; - config.Cmd = [ (lib.getExe nixpkgs-tracker-bot) ]; - architecture = arch; - }; + staticWith = pkgs.callPackage ./nix/static.nix { }; + containerize = pkgs.callPackage ./nix/containerize.nix { }; in { - nixpkgs-tracker-bot = pkgs.callPackage ./nix/package.nix { - version = self.shortRev or self.dirtyShortRev or "unknown"; - }; + nixpkgs-tracker-bot = pkgs.callPackage ./nix/package.nix { }; - default = packages.nixpkgs-tracker-bot; + default = packages'.nixpkgs-tracker-bot; - static-x86_64 = mkStaticWith { arch = "x86_64"; }; - static-arm64 = mkStaticWith { arch = "aarch64"; }; + static-x86_64 = staticWith { arch = "x86_64"; }; + static-arm64 = staticWith { arch = "aarch64"; }; - container-x86_64 = containerWith packages.static-x86_64; - container-arm64 = containerWith packages.static-arm64; + container-amd64 = containerize packages'.static-x86_64; + container-arm64 = containerize packages'.static-arm64; } ); }; diff --git a/nix/containerize.nix b/nix/containerize.nix new file mode 100644 index 0000000..d83b998 --- /dev/null +++ b/nix/containerize.nix @@ -0,0 +1,16 @@ +{ lib, dockerTools }: +let + containerize = + nixpkgs-tracker-bot: + let + inherit (nixpkgs-tracker-bot.passthru) crossPkgs; + architecture = crossPkgs.go.GOARCH; + in + dockerTools.buildLayeredImage { + name = "nixpkgs-tracker-bot"; + tag = "latest-${architecture}"; + config.Cmd = [ (lib.getExe nixpkgs-tracker-bot) ]; + inherit architecture; + }; +in +containerize diff --git a/nix/package.nix b/nix/package.nix index 261e785..b216242 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -3,13 +3,12 @@ rustPlatform, openssl, pkg-config, - version, lto ? true, optimizeSize ? false, }: rustPlatform.buildRustPackage { pname = "nixpkgs-tracker-bot"; - inherit version; + inherit ((lib.importTOML ../Cargo.toml).workspace.package) version; src = lib.fileset.toSource { root = ../.; diff --git a/nix/static.nix b/nix/static.nix index 86a1cc3..8def285 100644 --- a/nix/static.nix +++ b/nix/static.nix @@ -1,45 +1,16 @@ -{ - lib, - fenix, - pkgsCross, - nixpkgs-tracker-bot, -}: +{ pkgsCross }: let crossPkgsFor = with pkgsCross; { x86_64 = musl64.pkgsStatic; aarch64 = aarch64-multiplatform; }; - - rustcTargetFor = lib.mapAttrs (lib.const ( - pkgs: pkgs.stdenv.hostPlatform.rust.rustcTarget - )) crossPkgsFor; - rustStdFor = lib.mapAttrs (lib.const ( - rustcTarget: fenix.targets.${rustcTarget}.stable.rust-std - )) rustcTargetFor; - - toolchain = - with fenix; - combine ( - [ - stable.cargo - stable.rustc - ] - ++ lib.attrValues rustStdFor - ); - - crossPlatformFor = lib.mapAttrs (lib.const ( - pkgs: - pkgs.makeRustPlatform ( - lib.genAttrs [ - "cargo" - "rustc" - ] (lib.const toolchain) - ) - )) crossPkgsFor; in { arch }: -nixpkgs-tracker-bot.override { - rustPlatform = crossPlatformFor.${arch}; - inherit (crossPkgsFor.${arch}) openssl; - optimizeSize = true; -} +let + crossPkgs = crossPkgsFor.${arch}; +in +(crossPkgs.callPackage ./package.nix { optimizeSize = true; }).overrideAttrs (old: { + passthru = old.passthru or { } // { + inherit crossPkgs; + }; +}) diff --git a/treefmt.nix b/treefmt.nix index f3dcaac..81102bc 100644 --- a/treefmt.nix +++ b/treefmt.nix @@ -1,11 +1,10 @@ { projectRootFile = ".git/config"; - # TODO: add actionlint - # https://github.com/numtide/treefmt-nix/pull/146 programs = { + actionlint.enable = true; deadnix.enable = true; - nixfmt-rfc-style.enable = true; + nixfmt.enable = true; rustfmt.enable = true; statix.enable = true; }; |
