Compare commits

...

9 Commits

Author SHA1 Message Date
Sergio Castaño Arteaga 8e7eafba8d
Upgrade dependencies and base images (#628)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-06-13 13:50:40 +02:00
Sergio Castaño Arteaga 9b4063c6f8
More labeling improvements (#627)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-06-12 13:46:28 +02:00
Sergio Castaño Arteaga 426286cf74
Improve labeling (#626)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-06-12 11:14:00 +02:00
Sergio Castaño Arteaga 9183e10d6a
Upgrade dependencies and base images (#617)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-05-12 08:10:27 +02:00
Sergio Castaño Arteaga 3915b82736
Update ADOPTERS.md (#613)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-05-01 09:54:50 +02:00
Sergio Castaño Arteaga 38a1ef34ce
Update runner image (#611)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-04-23 09:46:14 +02:00
Yacine Kheddache c4d5cbeb8e
Update ADOPTERS.md (#610)
Add Microcks 
Fix #609

Signed-off-by: Yacine Kheddache <yacine@microcks.io>
2025-04-22 18:51:47 +02:00
Sergio Castaño Arteaga 216216c007
Upgrade dependencies and base images (#598)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2025-03-17 11:35:32 +01:00
Sergio Castaño Arteaga fbdb69ced9
Update list of adopters (#572)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
2024-12-16 08:44:00 +01:00
14 changed files with 532 additions and 467 deletions

View File

@ -9,7 +9,7 @@ permissions: read-all
jobs:
lint-and-test:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
@ -24,7 +24,7 @@ jobs:
with:
python-version: 3.7
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.1
uses: helm/chart-testing-action@v2.7.0
- name: Run chart-testing (list-changed)
id: list-changed
run: |
@ -35,7 +35,7 @@ jobs:
- name: Run chart-testing (lint)
run: ct lint --config .ct.yaml --target-branch ${{ github.event.repository.default_branch }}
- name: Create kind cluster
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.12.0
if: steps.list-changed.outputs.changed == 'true'
- name: Run chart-testing (install)
run: ct install --config .ct.yaml --target-branch ${{ github.event.repository.default_branch }}

View File

@ -15,7 +15,7 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.83.0
toolchain: 1.87.0
components: clippy, rustfmt
- name: Run clippy
run: cargo clippy --all-targets --all-features -- --deny warnings
@ -30,6 +30,6 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.83.0
toolchain: 1.87.0
- name: Run backend tests
run: cargo test

View File

@ -9,7 +9,7 @@ permissions: read-all
jobs:
build-and-publish-images:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

View File

@ -2,12 +2,26 @@
If your organization is using GitVote, please consider adding it to this list by submitting a pull request.
- [AsyncAPI](https://github.com/asyncapi/community/blob/master/voting.md)
- [AWS Labs](https://github.com/awslabs)
- [CloudNativePG](https://cloudnative-pg.io)
- [CNCF](https://cncf.io)
- [DevRel Foundation](https://dev-rel.org/)
- [Fintech Open Source Foundation](www.finos.org)
- [JSON Schema](https://json-schema.org)
- [K8sGateway](https://k8sgateway.io)
- [KServe](https://kserve.github.io/website/latest/)
- [Kuadrant](https://kuadrant.io)
- [Kuma](https://kuma.io)
- [Kyverno](https://kyverno.io)
- [Microcks](https://microcks.io/)
- [NFDI4Health](https://github.com/nfdi4health)
- [Open Component Model](https://ocm.software)
- [OpenGemini](https://opengemini.org)
- [OpenSSF](https://openssf.org)
- [ORAS](https://oras.land)
- [OSCAL Compass](https://github.com/oscal-compass)
- [Ratify Project](https://ratify.dev)
- [ResBaz Arizona](https://researchbazaar.arizona.edu)
- [TODO Group](https://todogroup.org)
- [Universal Blue](https://universal-blue.org)
- [WasmEdge](https://wasmedge.org/)

702
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,58 +4,56 @@ description = "GitVote server"
version = "1.4.0"
license = "Apache-2.0"
edition = "2021"
rust-version = "1.83"
rust-version = "1.87"
[dependencies]
anyhow = "1.0.94"
askama = "0.12.1"
askama_axum = "0.4.0"
anyhow = "1.0.98"
askama = { version = "0.14.0", features = ["serde_json"] }
async-channel = "2.3.1"
async-trait = "0.1.83"
axum = { version = "0.7.9", features = ["macros"] }
clap = { version = "4.5.23", features = ["derive"] }
deadpool-postgres = { version = "0.14.0", features = ["serde"] }
async-trait = "0.1.88"
axum = { version = "0.8.4", features = ["macros"] }
clap = { version = "4.5.40", features = ["derive"] }
deadpool-postgres = { version = "0.14.1", features = ["serde"] }
figment = { version = "0.10.19", features = ["yaml", "env"] }
futures = "0.3.31"
graphql_client = { version = "0.14.0", features = ["reqwest"] }
hex = "0.4.3"
hmac = "0.12.1"
http = "0.2.12"
humantime = "2.1.0"
http = "1.3.1"
humantime = "2.2.0"
humantime-serde = "1.1.1"
ignore = "0.4.23"
jsonwebtoken = "9.3.0"
lazy_static = "1.5.0"
octocrab = "=0.33.3"
openssl = { version = "0.10.68", features = ["vendored"] }
postgres-openssl = "0.5.0"
jsonwebtoken = "9.3.1"
octocrab = "0.44.1"
openssl = { version = "0.10.73", features = ["vendored"] }
postgres-openssl = "0.5.1"
regex = "1.11.1"
reqwest = "0.12.9"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
reqwest = "0.12.20"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_yaml = "0.9.34"
sha2 = "0.10.8"
thiserror = "2.0.6"
time = { version = "0.3.37", features = ["serde"] }
tokio = { version = "1.42.0", features = [
sha2 = "0.10.9"
thiserror = "2.0.12"
time = { version = "0.3.41", features = ["serde"] }
tokio = { version = "1.45.1", features = [
"macros",
"rt-multi-thread",
"signal",
"time",
] }
tokio-postgres = { version = "0.7.12", features = [
tokio-postgres = { version = "0.7.13", features = [
"with-uuid-1",
"with-serde_json-1",
"with-time-0_3",
] }
tokio-util = { version = "0.7.13", features = ["rt"] }
tower = { version = "0.5.1", features = ["util"] }
tower-http = { version = "0.6.2", features = ["trace"] }
tokio-util = { version = "0.7.15", features = ["rt"] }
tower = { version = "0.5.2", features = ["util"] }
tower-http = { version = "0.6.6", features = ["trace"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
uuid = { version = "1.11.0", features = ["serde", "v4"] }
uuid = { version = "1.17.0", features = ["serde", "v4"] }
[dev-dependencies]
http-body = "1.0.1"
hyper = "1.5.1"
hyper = "1.6.0"
mockall = "0.13.1"

View File

@ -1,5 +1,5 @@
# Build gitvote
FROM rust:1-alpine3.21 as builder
FROM rust:1-alpine3.22 as builder
RUN apk --no-cache add musl-dev perl make
WORKDIR /gitvote
COPY src src
@ -10,7 +10,7 @@ WORKDIR /gitvote/src
RUN cargo build --release
# Final stage
FROM alpine:3.21.0
FROM alpine:3.22.0
RUN apk --no-cache add ca-certificates && addgroup -S gitvote && adduser -S gitvote -G gitvote
USER gitvote
WORKDIR /home/gitvote

View File

@ -1,10 +1,10 @@
# Build tern
FROM golang:1.23.4-alpine3.21 AS tern
FROM golang:1.24.4-alpine3.22 AS tern
RUN apk --no-cache add git
RUN go install github.com/jackc/tern@latest
# Build final image
FROM alpine:3.21.0
FROM alpine:3.22.0
RUN addgroup -S gitvote && adduser -S gitvote -G gitvote
USER gitvote
WORKDIR /home/gitvote

View File

@ -2,9 +2,9 @@
//! GitHub events.
use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tracing::error;
use crate::{
@ -19,11 +19,10 @@ const CMD_CREATE_VOTE: &str = "vote";
const CMD_CANCEL_VOTE: &str = "cancel-vote";
const CMD_CHECK_VOTE: &str = "check-vote";
lazy_static! {
/// Regex used to detect commands in issues/prs comments.
static ref CMD: Regex = Regex::new(r"(?m)^/(vote|cancel-vote|check-vote)-?([a-zA-Z0-9]*)\s*$")
.expect("invalid CMD regexp");
}
/// Regex used to detect commands in issues/prs comments.
static CMD: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^/(vote|cancel-vote|check-vote)-?([a-zA-Z0-9]*)\s*$").expect("invalid CMD regexp")
});
/// Represents a command to be executed, usually created from a GitHub event.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -191,7 +191,7 @@ impl GH for GHApi {
issue_number: i64,
labels: &[&str],
) -> Result<()> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let labels = labels.iter().map(ToString::to_string).collect::<Vec<String>>();
client.issues(owner, repo).add_labels(issue_number as u64, &labels).await?;
Ok(())
@ -206,7 +206,7 @@ impl GH for GHApi {
issue_number: i64,
check_details: &CheckDetails,
) -> Result<()> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let pr = client.pulls(owner, repo).get(issue_number as u64).await?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/check-runs");
let mut body = json!({
@ -220,7 +220,7 @@ impl GH for GHApi {
});
if let Some(conclusion) = &check_details.conclusion {
body["conclusion"] = json!(conclusion);
};
}
let _: Value = client.post(url, Some(&body)).await?;
Ok(())
}
@ -235,7 +235,7 @@ impl GH for GHApi {
title: &str,
body: &str,
) -> Result<()> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
// Fetch some repository details needed to create a discussion
let response: graphql_client::Response<announcement_repo_query::ResponseData> = client
@ -326,7 +326,7 @@ impl GH for GHApi {
/// [GH::get_collaborators]
async fn get_collaborators(&self, inst_id: u64, owner: &str, repo: &str) -> Result<Vec<UserName>> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/collaborators");
let first_page: Page<User> = client.get(url, None::<&()>).await?;
let collaborators = client.all_pages(first_page).await?.into_iter().map(|u| u.login).collect();
@ -341,7 +341,7 @@ impl GH for GHApi {
repo: &str,
comment_id: i64,
) -> Result<Vec<Reaction>> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/issues/comments/{comment_id}/reactions",);
let first_page: Page<Reaction> = client.get(url, None::<&()>).await?;
let reactions = client.all_pages(first_page).await?;
@ -350,20 +350,19 @@ impl GH for GHApi {
/// [GH::get_config_file]
async fn get_config_file(&self, inst_id: u64, owner: &str, repo: &str) -> Option<String> {
let client = self.app_client.installation(InstallationId(inst_id));
let Ok(client) = self.app_client.installation(InstallationId(inst_id)) else {
return None;
};
// Try to get the config file from the repository. Otherwise try
// getting the organization wide config file in the .github repo.
let mut content: Option<String> = None;
for repo in &[repo, ORG_CONFIG_REPO] {
match client.repos(owner, *repo).get_content().path(CONFIG_FILE).send().await {
Ok(resp) => {
if resp.items.len() == 1 {
content = resp.items[0].decoded_content();
break;
}
if let Ok(resp) = client.repos(owner, *repo).get_content().path(CONFIG_FILE).send().await {
if resp.items.len() == 1 {
content = resp.items[0].decoded_content();
break;
}
Err(_) => continue,
}
}
@ -372,7 +371,7 @@ impl GH for GHApi {
/// [GH::get_pr_files]
async fn get_pr_files(&self, inst_id: u64, owner: &str, repo: &str, pr_number: i64) -> Result<Vec<File>> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/pulls/{pr_number}/files");
let first_page: Page<File> = client.get(url, None::<&()>).await?;
let files: Vec<File> = client.all_pages(first_page).await?;
@ -387,7 +386,7 @@ impl GH for GHApi {
team: &str,
exclude_maintainers: bool,
) -> Result<Vec<UserName>> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/orgs/{org}/teams/{team}/members");
let first_page: Page<User> = client
.get(
@ -404,7 +403,7 @@ impl GH for GHApi {
/// [GH::is_check_required]
async fn is_check_required(&self, inst_id: u64, owner: &str, repo: &str, branch: &str) -> Result<bool> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/branches/{branch}");
let branch: Branch = client.get(url, None::<&()>).await?;
let is_check_required = if let Some(required_checks) =
@ -426,7 +425,7 @@ impl GH for GHApi {
issue_number: i64,
body: &str,
) -> Result<i64> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let comment = client.issues(owner, repo).create_comment(issue_number as u64, body).await?;
Ok(comment.id.0 as i64)
}
@ -440,14 +439,21 @@ impl GH for GHApi {
issue_number: i64,
label: &str,
) -> Result<()> {
let client = self.app_client.installation(InstallationId(inst_id));
client.issues(owner, repo).remove_label(issue_number as u64, label).await?;
Ok(())
let client = self.app_client.installation(InstallationId(inst_id))?;
match client.issues(owner, repo).remove_label(issue_number as u64, label).await {
Ok(_) => Ok(()),
Err(octocrab::Error::GitHub { source, backtrace: _ })
if source.message == "Label does not exist" =>
{
Ok(())
}
Err(err) => Err(err.into()),
}
}
/// [GH::user_is_collaborator]
async fn user_is_collaborator(&self, inst_id: u64, owner: &str, repo: &str, user: &str) -> Result<bool> {
let client = self.app_client.installation(InstallationId(inst_id));
let client = self.app_client.installation(InstallationId(inst_id))?;
let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/collaborators/{user}",);
let resp = client._get(url).await?;
if resp.status() == StatusCode::NO_CONTENT {

View File

@ -2,11 +2,12 @@
//! supported endpoints.
use anyhow::{format_err, Error, Result};
use askama::Template;
use axum::{
body::Bytes,
extract::{FromRef, State},
http::{HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
response::{Html, IntoResponse},
routing::{get, post},
Router,
};
@ -65,7 +66,11 @@ pub(crate) fn setup_router(
/// Handler that returns the index document.
#[allow(clippy::unused_async)]
async fn index() -> impl IntoResponse {
tmpl::Index {}
let template = tmpl::Index {};
match template.render() {
Ok(html) => Ok(Html(html)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
/// Handler that processes webhook events from GitHub.
@ -92,7 +97,7 @@ async fn event(
.is_err()
{
return Err((StatusCode::BAD_REQUEST, "no valid signature found".to_string()));
};
}
// Parse event
let event = match Event::try_from((headers.get(GITHUB_EVENT_HEADER), &body[..])) {
@ -122,7 +127,7 @@ async fn event(
})?;
}
}
};
}
Ok("no command detected")
}
@ -191,7 +196,7 @@ async fn set_check_status(db: DynDB, gh: DynGH, event: &PullRequestEvent) -> Res
gh.create_check_run(inst_id, owner, repo, pr, &check_details).await?;
}
PullRequestEventAction::Other => {}
};
}
Ok(())
}

View File

@ -55,7 +55,7 @@ async fn main() -> Result<()> {
match cfg.log.format {
LogFormat::Json => ts.json().init(),
LogFormat::Pretty => ts.init(),
};
}
// Setup database
let mut builder = SslConnector::builder(SslMethod::tls())?;

View File

@ -45,7 +45,16 @@ const AUTO_CLOSE_FREQUENCY: Duration = Duration::from_secs(60 * 60 * 24);
const GITVOTE_LABEL: &str = "gitvote";
/// Label used to tag issues/prs where a vote is open.
const VOTE_OPEN_LABEL: &str = "vote open";
const VOTE_OPEN_LABEL: &str = "gitvote/open";
/// Label used to tag issues/prs where a vote is closed.
const VOTE_CLOSED_LABEL: &str = "gitvote/closed";
/// Label used to tag issues/prs where a vote passed.
const VOTE_PASSED_LABEL: &str = "gitvote/passed";
/// Label used to tag issues/prs where a vote failed.
const VOTE_FAILED_LABEL: &str = "gitvote/failed";
/// A votes processor is in charge of creating the requested votes, stopping
/// them at the scheduled time and publishing results, etc. It relies on some
@ -210,7 +219,13 @@ impl CommandsHandler {
self.gh.create_check_run(inst_id, owner, repo, i.issue_number, &check_details).await?;
}
// Add "vote open" label to issue/pr
// Update issue/pr labels
//
// We try to remove the vote closed and result labels just in case they
// were left from a previous vote that was closed
_ = self.gh.remove_label(inst_id, owner, repo, i.issue_number, VOTE_CLOSED_LABEL).await;
_ = self.gh.remove_label(inst_id, owner, repo, i.issue_number, VOTE_PASSED_LABEL).await;
_ = self.gh.remove_label(inst_id, owner, repo, i.issue_number, VOTE_FAILED_LABEL).await;
self.gh
.add_labels(
inst_id,
@ -272,8 +287,8 @@ impl CommandsHandler {
self.gh.create_check_run(inst_id, owner, repo, i.issue_number, &check_details).await?;
}
// Remove "vote open" label from issue/pr
self.gh.remove_label(inst_id, owner, repo, i.issue_number, VOTE_OPEN_LABEL).await?;
// Update issue/pr labels
_ = self.gh.remove_label(inst_id, owner, repo, i.issue_number, VOTE_OPEN_LABEL).await;
}
Ok(())
@ -447,8 +462,17 @@ impl VotesCloser {
self.gh.create_check_run(inst_id, owner, repo, vote.issue_number, &check_details).await?;
}
// Remove "vote open" label from issue/pr
self.gh.remove_label(inst_id, owner, repo, vote.issue_number, VOTE_OPEN_LABEL).await?;
// Update issue/pr labels
let mut labels_to_add = vec![VOTE_CLOSED_LABEL];
if let Some(results) = &results {
if results.passed {
labels_to_add.push(VOTE_PASSED_LABEL);
} else {
labels_to_add.push(VOTE_FAILED_LABEL);
}
}
_ = self.gh.remove_label(inst_id, owner, repo, vote.issue_number, VOTE_OPEN_LABEL).await;
self.gh.add_labels(inst_id, owner, repo, vote.issue_number, &labels_to_add).await?;
debug!("closed");
Ok(Some(()))
@ -928,6 +952,36 @@ mod tests {
})
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(COMMENT_ID))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_CLOSED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_PASSED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_FAILED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_add_labels()
.withf(|inst_id, owner, repo, issue_number, labels| {
*inst_id == INST_ID
@ -1003,6 +1057,36 @@ mod tests {
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_CLOSED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_PASSED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_remove_label()
.with(
eq(INST_ID),
eq(ORG),
eq(REPO),
eq(ISSUE_NUM),
eq(VOTE_FAILED_LABEL),
)
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_add_labels()
.withf(|inst_id, owner, repo, issue_number, labels| {
*inst_id == INST_ID
@ -1374,6 +1458,16 @@ mod tests {
.with(eq(INST_ID), eq(ORG), eq(REPO), eq(ISSUE_NUM), eq(VOTE_OPEN_LABEL))
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_add_labels()
.withf(|inst_id, owner, repo, issue_number, labels| {
*inst_id == INST_ID
&& owner == ORG
&& repo == REPO
&& *issue_number == ISSUE_NUM
&& labels == vec![VOTE_CLOSED_LABEL, VOTE_PASSED_LABEL]
})
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
let votes_closer = VotesCloser::new(Arc::new(db), Arc::new(gh));
votes_closer.close_finished_vote().await.unwrap();
@ -1434,6 +1528,16 @@ mod tests {
.with(eq(INST_ID), eq(ORG), eq(REPO), eq(ISSUE_NUM), eq(VOTE_OPEN_LABEL))
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_add_labels()
.withf(|inst_id, owner, repo, issue_number, labels| {
*inst_id == INST_ID
&& owner == ORG
&& repo == REPO
&& *issue_number == ISSUE_NUM
&& labels == vec![VOTE_CLOSED_LABEL, VOTE_PASSED_LABEL]
})
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
let votes_closer = VotesCloser::new(Arc::new(db), Arc::new(gh));
votes_closer.close_finished_vote().await.unwrap();
@ -1498,6 +1602,16 @@ mod tests {
.with(eq(INST_ID), eq(ORG), eq(REPO), eq(ISSUE_NUM), eq(VOTE_OPEN_LABEL))
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
gh.expect_add_labels()
.withf(|inst_id, owner, repo, issue_number, labels| {
*inst_id == INST_ID
&& owner == ORG
&& repo == REPO
&& *issue_number == ISSUE_NUM
&& labels == vec![VOTE_CLOSED_LABEL, VOTE_FAILED_LABEL]
})
.times(1)
.returning(|_, _, _, _, _| Box::pin(future::ready(Ok(()))));
let votes_closer = VotesCloser::new(Arc::new(db), Arc::new(gh));
votes_closer.close_finished_vote().await.unwrap();

View File

@ -216,6 +216,7 @@ mod filters {
#[allow(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
pub(crate) fn non_binding(
votes: &HashMap<UserName, UserVote>,
_: &dyn askama::Values,
max: &i64,
) -> askama::Result<Vec<(UserName, UserVote)>> {
let mut non_binding_votes: Vec<(UserName, UserVote)> =