mirror of
https://gitea.0xace.cc/rust/github-release-bot.git
synced 2024-11-24 21:36:40 +00:00
first commit
This commit is contained in:
commit
c7d2b176f6
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
**
|
||||||
|
!Cargo.toml
|
||||||
|
!Cargo.lock
|
||||||
|
!src
|
||||||
|
!docker
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/data
|
||||||
|
config.yaml
|
1692
Cargo.lock
generated
Normal file
1692
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "github-release-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["ace <ace@0xace.cc>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_yaml = "0.8"
|
||||||
|
octocrab = "0.8"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
futures = "0.3"
|
||||||
|
clap = "3.0.0-beta.2"
|
||||||
|
#telegram-bot = { git = "https://github.com/telegram-rs/telegram-bot", rev = "07a9f9a1c76eaab2259bdc6241691187a46d69d1" }
|
||||||
|
telegram-bot = { git = "https://github.com/telegram-rs/telegram-bot", rev = "65ad5cfd578e9a1260ce6daac714eb2153c0bec7" }
|
||||||
|
log = "0.4"
|
||||||
|
stderrlog = "0.5"
|
||||||
|
rand = "0.8"
|
||||||
|
chrono ="0.4"
|
15
Makefile
Normal file
15
Makefile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
VERSION=0.1.0
|
||||||
|
IMAGE=github-release-bot
|
||||||
|
ifneq ($(REGISTRY),)
|
||||||
|
_REGISTRY =$(REGISTRY)/
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: build push all
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker build -t $(_REGISTRY)$(IMAGE):$(VERSION) -f docker/Dockerfile .
|
||||||
|
|
||||||
|
push:
|
||||||
|
docker push $(_REGISTRY)$(IMAGE):$(VERSION)
|
||||||
|
|
||||||
|
all: build push
|
61
README.md
Normal file
61
README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# GitHub release bot
|
||||||
|
Telegram bot for GitHub releases notification. Bot simply fetches latest release and send notification.
|
||||||
|
|
||||||
|
**Warning**: GitHub will ban you with small interval and spread time. Use adequate interval and spread time or use TOR.
|
||||||
|
|
||||||
|
Interval - set how often check GitHub for new releases in seconds. Default - 28800 seconds (8 hours).
|
||||||
|
|
||||||
|
Spread - time in seconds for random offset calculation per repo within interval.
|
||||||
|
Default 600 seconds (10 minutes). Offset calculated as RANDOM(u64) % SPREAD.
|
||||||
|
|
||||||
|
With default values bot will launch main loop check every 8 hours and get latest release within (RANDOM(u64) % 600) seconds per repo.
|
||||||
|
|
||||||
|
## Example config.yaml
|
||||||
|
```yaml
|
||||||
|
- owner: go-gitea
|
||||||
|
repo: gitea
|
||||||
|
- owner: kubernetes-sigs
|
||||||
|
repo: kubespray
|
||||||
|
```
|
||||||
|
## Binary
|
||||||
|
### Build binary
|
||||||
|
```ShellSession
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
### Run binary
|
||||||
|
```ShellSession
|
||||||
|
github-release-bot \
|
||||||
|
--token ${TOKEN} --chatid ${CHATID} \
|
||||||
|
--config ${CONFIG} --datadir ${DATADIR} \
|
||||||
|
--interval ${INTERVAL} --spread ${SPREAD}
|
||||||
|
```
|
||||||
|
### Binary params
|
||||||
|
- token - required, Telegram token
|
||||||
|
- chatid - required, Telegram chat id
|
||||||
|
- config - file required, default config file name "config.yaml"
|
||||||
|
- interval - optional, default 28800 seconds (8 hours)
|
||||||
|
- spread - optional, default 600 seconds (10 minutes)
|
||||||
|
- datadir - optional, dir for storing files with latest releases, default "data"
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
### Docker build
|
||||||
|
```ShellSession
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
### Docker run
|
||||||
|
```ShellSession
|
||||||
|
docker run -d -v ./config.yaml:/opt/config.yaml -v ./data:/opt/data registry.0xace.cc/ghp/github-release-bot:latest \
|
||||||
|
-e TOKEN=${TOKEN} -e CHATID=${CHATID} \
|
||||||
|
-e CONFIG=${CONFIG} -e DATADIR=${DATADIR} \
|
||||||
|
-e INTERVAL=${INTERVAL} -e SPREAD=${SPREAD}
|
||||||
|
```
|
||||||
|
### Docker params
|
||||||
|
OS ENV used to pass arguments:
|
||||||
|
- TOKEN - required, Telegram token
|
||||||
|
- CHATID - required, Telegram chat id
|
||||||
|
- CONFIG - file required, default config file name "config.yaml"
|
||||||
|
- INTERVAL - optional, default 28800 seconds (8 hours)
|
||||||
|
- SPREAD - optional, default 600 seconds (10 minutes)
|
||||||
|
- DATADIR - optional, dir for storing files with latest releases, default "data"
|
||||||
|
|
||||||
|
|
11
docker/Dockerfile
Normal file
11
docker/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM ekidd/rust-musl-builder as builder
|
||||||
|
ADD --chown=rust:rust . ./
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
COPY --from=builder /home/rust/src/target/x86_64-unknown-linux-musl/release/github-release-bot /opt/github-release-bot
|
||||||
|
COPY ./docker/entrypoint.sh /opt/entrypoint.sh
|
||||||
|
RUN chmod +x /opt/entrypoint.sh
|
||||||
|
WORKDIR /opt
|
||||||
|
ENTRYPOINT ["/opt/entrypoint.sh"]
|
20
docker/entrypoint.sh
Normal file
20
docker/entrypoint.sh
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ -z "${TOKEN}" ]]; then
|
||||||
|
echo "Must set Telegram token"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${CHATID}" ]]; then
|
||||||
|
echo "Must set Telegram chat id"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INTERVAL="${INTERVAL:-28800}"
|
||||||
|
SPREAD="${SPREAD:-600}"
|
||||||
|
CONFIG="${CONFIG:-config.yaml}"
|
||||||
|
DATADIR="${DATADIR:-data}"
|
||||||
|
|
||||||
|
/opt/github-release-bot --token ${TOKEN} --chatid ${CHATID} --interval ${INTERVAL} --spread ${SPREAD} --config ${CONFIG} --datadir ${DATADIR}
|
||||||
|
|
164
src/main.rs
Normal file
164
src/main.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
use serde_yaml;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use octocrab;
|
||||||
|
use telegram_bot::*;
|
||||||
|
use clap::Clap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::time::Duration;
|
||||||
|
use futures;
|
||||||
|
use tokio;
|
||||||
|
use log::*;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
struct Repo {
|
||||||
|
owner: String,
|
||||||
|
repo: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repo {
|
||||||
|
async fn init(&mut self) {
|
||||||
|
tokio::time::sleep(Duration::from_millis(0)).await;
|
||||||
|
let _ = create_repo_files(&self.owner, &self.repo).await;
|
||||||
|
}
|
||||||
|
async fn resolve(&mut self) {
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
let random_spred = rand::random::<u64>();
|
||||||
|
println!("{} - LOG - {}/{} will be checked in {} secs ({} interval and {} random spread)",
|
||||||
|
Local::now().trunc_subsecs(0).to_rfc3339(), &self.owner, &self.repo, opts.interval+(random_spred % opts.spread), opts.interval, random_spred % opts.spread);
|
||||||
|
tokio::time::sleep(Duration::from_secs(opts.interval+(random_spred % opts.spread))).await;
|
||||||
|
let _res = github_get_latest_release(&self.owner, &self.repo).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn print_result(&self) {
|
||||||
|
// println!("{}, {}", self.owner, self.repo);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap)]
|
||||||
|
// #[clap(version = "1.0")]
|
||||||
|
struct Opts {
|
||||||
|
/// Config file
|
||||||
|
#[clap(long, default_value = "config.yaml")]
|
||||||
|
config: String,
|
||||||
|
/// Data dir
|
||||||
|
#[clap(long, default_value = "data")]
|
||||||
|
datadir: String,
|
||||||
|
/// Telegram token
|
||||||
|
#[clap(long)]
|
||||||
|
token: String,
|
||||||
|
/// Check interval
|
||||||
|
#[clap(long, default_value = "28800")]
|
||||||
|
interval: u64,
|
||||||
|
/// Spread interval
|
||||||
|
#[clap(long, default_value = "600")]
|
||||||
|
spread: u64,
|
||||||
|
/// Telegram chat id
|
||||||
|
#[clap(long, allow_hyphen_values(true))]
|
||||||
|
chatid: i64,
|
||||||
|
/// Verbose level, accept multiple occurrences
|
||||||
|
#[clap(short, long, parse(from_occurrences))]
|
||||||
|
verbose: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
stderrlog::new()
|
||||||
|
.module(module_path!())
|
||||||
|
.verbosity(opts.verbose+1)
|
||||||
|
.timestamp(stderrlog::Timestamp::Second)
|
||||||
|
.quiet(false)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
println!("{} - LOG - Config file name: {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.config);
|
||||||
|
debug!("Using token: {}", opts.token);
|
||||||
|
println!("{} - LOG - Using chat id: {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.chatid);
|
||||||
|
println!("{} - LOG - Using check interval {} secs + random spread from {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.interval, opts.spread);
|
||||||
|
|
||||||
|
let config: Vec<Repo> = get_config(&opts.config).await?;
|
||||||
|
let items: Vec<_> = config.iter().map(|x| Repo { owner: x.owner.trim().to_string(), repo: x.repo.trim().to_string() }).collect();
|
||||||
|
|
||||||
|
let _items = join_parallel(items.into_iter().map(|mut item| async {
|
||||||
|
item.init().await;
|
||||||
|
item
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let items: Vec<_> = config.iter().map(|x| Repo { owner: x.owner.trim().to_string(), repo: x.repo.trim().to_string() }).collect();
|
||||||
|
let _items = join_parallel(items.into_iter().map(|mut item| async {
|
||||||
|
item.resolve().await;
|
||||||
|
item
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_parallel<T: Send + 'static>(
|
||||||
|
futs: impl IntoIterator<Item = impl Future<Output = T> + Send + 'static>,
|
||||||
|
) -> Vec<T> {
|
||||||
|
let tasks: Vec<_> = futs.into_iter().map(tokio::spawn).collect();
|
||||||
|
futures::future::join_all(tasks)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(Result::unwrap)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_config(filename: impl AsRef<std::path::Path>) -> Result<Vec<Repo>, Box<dyn std::error::Error>> {
|
||||||
|
let config = std::fs::File::open(filename)?;
|
||||||
|
let contents: Vec<Repo> = serde_yaml::from_reader(config)?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_repo_files(owner: &String, repo: &String) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
debug!("Create repo file {}-{}", owner, repo);
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
let _repofile = tokio::fs::OpenOptions::new().write(true).create(true).open(opts.datadir.to_string() + "/" + &owner.trim().to_string() + "-" + &repo.trim().to_string()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_file(filename: impl AsRef<std::path::Path>) -> Result<String, Box<dyn std::error::Error>>{
|
||||||
|
let contents = tokio::fs::read_to_string(filename).await?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn overwrite_repo_file(filename: impl AsRef<std::path::Path>, release_tag: &String) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut file = tokio::fs::File::create(filename).await?;
|
||||||
|
tokio::io::AsyncWriteExt::write_all(&mut file, release_tag.as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn telegram_notify(_release_tag: &String, release_url: &String) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
let api = Api::new(opts.token);
|
||||||
|
let chat = ChatId::new(opts.chatid);
|
||||||
|
api.send(chat.text(release_url)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn github_get_latest_release(owner: &String, repo: &String) -> octocrab::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let opts: Opts = Opts::parse();
|
||||||
|
let release = octocrab::instance()
|
||||||
|
.repos(owner, repo)
|
||||||
|
.releases()
|
||||||
|
.get_latest()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let version_in_file = read_file(opts.datadir.to_string() + "/" + &owner.trim().to_string() + "-" + &repo.trim().to_string()).await?;
|
||||||
|
match version_in_file.to_string() == release.tag_name.to_string() {
|
||||||
|
true => {
|
||||||
|
info!("{}/{}: current known version is {:?}", owner, repo, version_in_file.to_string());
|
||||||
|
println!("{} - LOG - {}/{}: known version matches with latest release {}", Local::now().trunc_subsecs(0).to_rfc3339(), owner, repo, release.tag_name.to_string());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
info!("{}/{}: current known version is {:?}", owner, repo, version_in_file.to_string());
|
||||||
|
println!("{} - LOG - {}/{}: known version does not match with latest release {}. Sending new release URL {}", Local::now().trunc_subsecs(0).to_rfc3339(), owner, repo, release.tag_name.to_string(), release.html_url);
|
||||||
|
let _ = overwrite_repo_file(opts.datadir.to_string() + "/" + &owner.to_string() + "-" + &repo.to_string(), &release.tag_name.to_string()).await?;
|
||||||
|
let _ = telegram_notify(&release.tag_name.to_string(), &release.html_url.to_string()).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user