summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 23:00:30 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-04 23:00:30 +0300
commit71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a (patch)
treed336b1d64747aeebb1a80c2e7c4e9b5d24253751
parentf96ea38d595162813a460f80f84e20f8d7f241bc (diff)
downloadProject-Tick-71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a.tar.gz
Project-Tick-71ffb442e5f8072c6e0a974df9ae085bcf0e5d2a.zip
NOISSUE update bootstrap script paths in documentation for Linux and Windows
remove unnecessary badges from README add example environment configuration for Ofborg create production configuration for Ofborg correct RabbitMQ host in example configuration add push event handling in GitHub webhook receiver implement push filter task for handling push events extend build job structure to include push event information enhance build result structure to accommodate push event data add push event data handling in various message processing tasks update log message collector to prevent 404 errors on log links add push filter task to task module Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
-rw-r--r--.envrc2
-rw-r--r--.github/pull_request_template.md4
-rw-r--r--.gitignore2
-rw-r--r--CODE_OF_CONDUCT.md (renamed from meshmc/CODE_OF_CONDUCT.md)0
-rw-r--r--REUSE.toml5
-rw-r--r--bootstrap.cmd (renamed from meshmc/bootstrap.cmd)0
-rwxr-xr-xbootstrap.sh (renamed from meshmc/bootstrap.sh)0
-rw-r--r--flake.lock24
-rw-r--r--flake.nix78
-rw-r--r--lefthook.yml (renamed from meshmc/lefthook.yml)0
-rw-r--r--meshmc/BUILD.md4
-rw-r--r--meshmc/README.md2
-rw-r--r--ofborg/.env.example4
-rw-r--r--ofborg/config.production.json101
-rw-r--r--ofborg/example.config.json2
-rw-r--r--ofborg/tickborg/src/bin/build-faker.rs1
-rw-r--r--ofborg/tickborg/src/bin/github-webhook-receiver.rs18
-rw-r--r--ofborg/tickborg/src/bin/push-filter.rs105
-rw-r--r--ofborg/tickborg/src/config.rs16
-rw-r--r--ofborg/tickborg/src/ghevent/mod.rs2
-rw-r--r--ofborg/tickborg/src/ghevent/pushevent.rs53
-rw-r--r--ofborg/tickborg/src/message/buildjob.rs44
-rw-r--r--ofborg/tickborg/src/message/buildresult.rs22
-rw-r--r--ofborg/tickborg/src/message/common.rs11
-rw-r--r--ofborg/tickborg/src/message/mod.rs2
-rw-r--r--ofborg/tickborg/src/tasks/build.rs73
-rw-r--r--ofborg/tickborg/src/tasks/githubcommentposter.rs95
-rw-r--r--ofborg/tickborg/src/tasks/log_message_collector.rs3
-rw-r--r--ofborg/tickborg/src/tasks/mod.rs1
-rw-r--r--ofborg/tickborg/src/tasks/pushfilter.rs165
30 files changed, 793 insertions, 46 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000000..1d11c53545
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,2 @@
+use nix
+watch_file nix/*.nix
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index bd2abdad87..5b75623fd4 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,8 +1,8 @@
<!--
-Hey there! Thanks for your contribution for MeshMC.
+Hey there! Thanks for your contribution for Project Tick.
Please make sure that your commits are signed off and please sign CLA first.
-If you don't know how that works, check out our contribution guidelines: https://github.com/Project-Tick/MeshMC/blob/master/CONTRIBUTING.md#signing-your-work
+If you don't know how that works, check out our contribution guidelines: https://github.com/Project-Tick/Project-Tick/blob/master/CONTRIBUTING.md#signing-your-work
If you already created your commits, you can run `git rebase --signoff develop` to retroactively sign-off all your commits and `git push --force` to override what you have pushed already.
Note that signing and signing-off are two different things!
diff --git a/.gitignore b/.gitignore
index 6ac77d6f4d..ba0dc780cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,5 @@ node_modules/
#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
+
+tree.txt
diff --git a/meshmc/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index f3f877ff21..f3f877ff21 100644
--- a/meshmc/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
diff --git a/REUSE.toml b/REUSE.toml
index 5ec4afbba4..d4db8d268c 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -154,3 +154,8 @@ SPDX-FileCopyrightText = "MultiMC Contributors & PolyMC Contributors & PrismLaun
path = ["images4docker/**"]
SPDX-License-Identifier = "GPL-3.0-or-later"
SPDX-FileCopyrightText = "Project Tick"
+
+[[annotations]]
+path = ["ofborg/**"]
+SPDX-License-Identifier = "MIT"
+SPDX-FileCopyrightText = "NixOS Contributors & Project Tick"
diff --git a/meshmc/bootstrap.cmd b/bootstrap.cmd
index 9da8b46195..9da8b46195 100644
--- a/meshmc/bootstrap.cmd
+++ b/bootstrap.cmd
diff --git a/meshmc/bootstrap.sh b/bootstrap.sh
index 3762deaa2e..3762deaa2e 100755
--- a/meshmc/bootstrap.sh
+++ b/bootstrap.sh
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000000..55a204d6b5
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,24 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1775036866,
+ "narHash": "sha256-ByAX1LkhCwZ94+KnFAmnJSMAvui7kgCxjHgUHsWAbfI=",
+ "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
+ "type": "tarball",
+ "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre972949.6201e203d095/nixexprs.tar.xz"
+ },
+ "original": {
+ "type": "tarball",
+ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000000..d510656840
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,78 @@
+{
+ description = " Project Tick is a project dedicated to providing developers with ease of use and users with long-lasting software.";
+
+ inputs = {
+ nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
+ };
+
+ outputs =
+ {
+ self,
+ nixpkgs,
+ }:
+
+ let
+ inherit (nixpkgs) lib;
+
+ # While we only officially support aarch and x86_64 on Linux and MacOS,
+ # we expose a reasonable amount of other systems for users who want to
+ # build for most exotic platforms
+ systems = lib.systems.flakeExposed;
+
+ forAllSystems = lib.genAttrs systems;
+ nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system});
+ in
+
+ {
+ devShells = forAllSystems (
+ system:
+
+ let
+ pkgs = nixpkgsFor.${system};
+ llvm = pkgs.llvmPackages_22;
+ python = pkgs.python3;
+ mkShell = pkgs.mkShell.override { inherit (llvm) stdenv; };
+
+ packages' = self.packages.${system};
+
+ welcomeMessage = ''
+ Welcome to Project Tick!
+ '';
+ in
+
+ {
+ default = mkShell {
+ name = "project-tick";
+
+ packages = [
+
+ (pkgs.stdenvNoCC.mkDerivation {
+ pname = "clang-tidy-diff";
+ inherit (llvm.clang) version;
+
+ nativeBuildInputs = [
+ pkgs.installShellFiles
+ python.pkgs.wrapPython
+ ];
+
+ dontUnpack = true;
+ dontConfigure = true;
+ dontBuild = true;
+
+ postInstall = "installBin ${llvm.libclang.python}/share/clang/clang-tidy-diff.py";
+ postFixup = "wrapPythonPrograms";
+ })
+ ];
+
+ shellHook = ''
+ git submodule update --init --force
+
+ echo ${lib.escapeShellArg welcomeMessage}
+ '';
+ };
+ }
+ );
+
+ formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style);
+ };
+}
diff --git a/meshmc/lefthook.yml b/lefthook.yml
index f92640489b..f92640489b 100644
--- a/meshmc/lefthook.yml
+++ b/lefthook.yml
diff --git a/meshmc/BUILD.md b/meshmc/BUILD.md
index d9058f7eb9..118324d2f8 100644
--- a/meshmc/BUILD.md
+++ b/meshmc/BUILD.md
@@ -36,7 +36,7 @@ and sets up lefthook git hooks.
### Linux / macOS
```bash
-./bootstrap.sh
+../bootstrap.sh
```
Supported distributions: Debian, Ubuntu, Fedora, RHEL/CentOS, openSUSE, Arch Linux, macOS (via Homebrew).
@@ -44,7 +44,7 @@ Supported distributions: Debian, Ubuntu, Fedora, RHEL/CentOS, openSUSE, Arch Lin
### Windows
```cmd
-bootstrap.cmd
+..\bootstrap.cmd
```
Uses [Scoop](https://scoop.sh) for CLI tools and [vcpkg](https://github.com/microsoft/vcpkg) for C/C++ libraries.
diff --git a/meshmc/README.md b/meshmc/README.md
index 87d86cc82c..b8ab5172e2 100644
--- a/meshmc/README.md
+++ b/meshmc/README.md
@@ -1,6 +1,6 @@
MeshMC
======
-[![REUSE status](https://api.reuse.software/badge/github.com/Project-Tick/MeshMC)](https://api.reuse.software/info/github.com/Project-Tick/MeshMC) [![CLA assistant](https://cla-assistant.io/readme/badge/Project-Tick/MeshMC)](https://cla-assistant.io/Project-Tick/MeshMC) [![LICENSE](https://img.shields.io/badge/license-GPL--3.0--or--later-blue?logo=GNU)](https://www.gnu.org/licenses/gpl-3.0.html) [![Crowdin](https://badges.crowdin.net/projtlauncher/localized.svg)](https://crowdin.com/project/projtlauncher)
+[![LICENSE](https://img.shields.io/badge/license-GPL--3.0--or--later-blue?logo=GNU)](https://www.gnu.org/licenses/gpl-3.0.html) [![Crowdin](https://badges.crowdin.net/projtlauncher/localized.svg)](https://crowdin.com/project/projtlauncher)
MeshMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.
diff --git a/ofborg/.env.example b/ofborg/.env.example
new file mode 100644
index 0000000000..81b01b4b68
--- /dev/null
+++ b/ofborg/.env.example
@@ -0,0 +1,4 @@
+# Aşağıdakileri doldur
+GITHUB_APP_ID=
+GITHUB_WEBHOOK_SECRET=
+RABBITMQ_PASSWORD=changeme
diff --git a/ofborg/config.production.json b/ofborg/config.production.json
new file mode 100644
index 0000000000..3dc45cd943
--- /dev/null
+++ b/ofborg/config.production.json
@@ -0,0 +1,101 @@
+{
+ "runner": {
+ "identity": "mail-tickborg-1",
+ "repos": ["project-tick/Project-Tick"],
+ "trusted_users": ["AhmetSamet06"]
+ },
+ "checkout": {
+ "root": "/var/lib/tickborg/checkout"
+ },
+ "build": {
+ "system": "x86_64-linux",
+ "build_timeout_seconds": 1800
+ },
+ "github_app": {
+ "app_id": 0,
+ "private_key": "/etc/tickborg/github-private-key.pem",
+ "oauth_client_id": "GITHUB_APP_CLIENT_ID",
+ "oauth_client_secret_file": "/etc/tickborg/github-oauth-secret"
+ },
+ "github_webhook_receiver": {
+ "listen": "0.0.0.0:9899",
+ "webhook_secret_file": "/etc/tickborg/webhook-secret",
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ },
+ "evaluation_filter": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ },
+ "github_comment_filter": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ },
+ "github_comment_poster": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ },
+ "builder": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ },
+ "push_filter": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ },
+ "default_attrs": []
+ },
+ "log_message_collector": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ },
+ "logs_path": "/var/log/tickborg"
+ },
+ "log_api_config": {
+ "listen": "127.0.0.1:9898",
+ "logs_path": "/var/log/tickborg",
+ "serve_root": "https://logs.tickborg.projecttick.net/logfile"
+ },
+ "stats": {
+ "rabbitmq": {
+ "ssl": false,
+ "host": "localhost",
+ "virtualhost": "/",
+ "username": "tickborg",
+ "password_file": "/etc/tickborg/rabbitmq-password"
+ }
+ }
+}
diff --git a/ofborg/example.config.json b/ofborg/example.config.json
index 0344d169ea..420ae5efdd 100644
--- a/ofborg/example.config.json
+++ b/ofborg/example.config.json
@@ -13,7 +13,7 @@
},
"rabbitmq": {
"ssl": true,
- "host": "events.tickborg.project-tick.net",
+ "host": "events.tickborg.projecttick.net",
"virtualhost": "tickborg",
"username": "...",
"password": "..."
diff --git a/ofborg/tickborg/src/bin/build-faker.rs b/ofborg/tickborg/src/bin/build-faker.rs
index df8fcbfa50..086e96493d 100644
--- a/ofborg/tickborg/src/bin/build-faker.rs
+++ b/ofborg/tickborg/src/bin/build-faker.rs
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
logs: Some((Some("logs".to_owned()), Some(logbackrk.to_lowercase()))),
statusreport: Some((None, Some("scratch".to_owned()))),
request_id: "bogus-request-id".to_owned(),
+ push: None,
};
{
diff --git a/ofborg/tickborg/src/bin/github-webhook-receiver.rs b/ofborg/tickborg/src/bin/github-webhook-receiver.rs
index 910cd4b350..60698a5019 100644
--- a/ofborg/tickborg/src/bin/github-webhook-receiver.rs
+++ b/ofborg/tickborg/src/bin/github-webhook-receiver.rs
@@ -87,6 +87,24 @@ async fn setup_amqp(chan: &mut Channel) -> Result<(), Box<dyn Error + Send + Syn
no_wait: false,
})
.await?;
+
+ let queue_name = String::from("push-build-inputs");
+ chan.declare_queue(easyamqp::QueueConfig {
+ queue: queue_name.clone(),
+ passive: false,
+ durable: true,
+ exclusive: false,
+ auto_delete: false,
+ no_wait: false,
+ })
+ .await?;
+ chan.bind_queue(easyamqp::BindQueueConfig {
+ queue: queue_name.clone(),
+ exchange: "github-events".to_owned(),
+ routing_key: Some(String::from("push.*")),
+ no_wait: false,
+ })
+ .await?;
Ok(())
}
diff --git a/ofborg/tickborg/src/bin/push-filter.rs b/ofborg/tickborg/src/bin/push-filter.rs
new file mode 100644
index 0000000000..81d1597e0f
--- /dev/null
+++ b/ofborg/tickborg/src/bin/push-filter.rs
@@ -0,0 +1,105 @@
+use std::env;
+use std::error::Error;
+
+use tracing::{error, info};
+
+use tickborg::config;
+use tickborg::easyamqp::{self, ChannelExt, ConsumerExt};
+use tickborg::easylapin;
+use tickborg::tasks;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> {
+ tickborg::setup_log();
+
+ let arg = env::args()
+ .nth(1)
+ .unwrap_or_else(|| panic!("usage: {} <config>", std::env::args().next().unwrap()));
+ let cfg = config::load(arg.as_ref());
+
+ let Some(filter_cfg) = config::load(arg.as_ref()).push_filter else {
+ error!("No push filter configuration found!");
+ panic!();
+ };
+
+ let conn = easylapin::from_config(&filter_cfg.rabbitmq).await?;
+ let mut chan = conn.create_channel().await?;
+
+ chan.declare_exchange(easyamqp::ExchangeConfig {
+ exchange: "github-events".to_owned(),
+ exchange_type: easyamqp::ExchangeType::Topic,
+ passive: false,
+ durable: true,
+ auto_delete: false,
+ no_wait: false,
+ internal: false,
+ })
+ .await?;
+
+ // Declare the build-jobs exchange (fanout) that the builder consumes from
+ chan.declare_exchange(easyamqp::ExchangeConfig {
+ exchange: "build-jobs".to_owned(),
+ exchange_type: easyamqp::ExchangeType::Fanout,
+ passive: false,
+ durable: true,
+ auto_delete: false,
+ no_wait: false,
+ internal: false,
+ })
+ .await?;
+
+ // Declare the build-results exchange for the comment poster
+ chan.declare_exchange(easyamqp::ExchangeConfig {
+ exchange: "build-results".to_owned(),
+ exchange_type: easyamqp::ExchangeType::Fanout,
+ passive: false,
+ durable: true,
+ auto_delete: false,
+ no_wait: false,
+ internal: false,
+ })
+ .await?;
+
+ let queue_name = String::from("push-build-inputs");
+ chan.declare_queue(easyamqp::QueueConfig {
+ queue: queue_name.clone(),
+ passive: false,
+ durable: true,
+ exclusive: false,
+ auto_delete: false,
+ no_wait: false,
+ })
+ .await?;
+
+ chan.bind_queue(easyamqp::BindQueueConfig {
+ queue: queue_name.clone(),
+ exchange: "github-events".to_owned(),
+ routing_key: Some("push.*".to_owned()),
+ no_wait: false,
+ })
+ .await?;
+
+ let handle = easylapin::WorkerChannel(chan)
+ .consume(
+ tasks::pushfilter::PushFilterWorker::new(
+ cfg.acl(),
+ filter_cfg.default_attrs,
+ ),
+ easyamqp::ConsumeConfig {
+ queue: queue_name.clone(),
+ consumer_tag: format!("{}-push-filter", cfg.whoami()),
+ no_local: false,
+ no_ack: false,
+ no_wait: false,
+ exclusive: false,
+ },
+ )
+ .await?;
+
+ info!("Fetching push events from {}", &queue_name);
+ handle.await;
+
+ drop(conn); // Close connection.
+ info!("Closed the session... EOF");
+ Ok(())
+}
diff --git a/ofborg/tickborg/src/config.rs b/ofborg/tickborg/src/config.rs
index 7d7475e3b6..623b9f5e9a 100644
--- a/ofborg/tickborg/src/config.rs
+++ b/ofborg/tickborg/src/config.rs
@@ -30,6 +30,8 @@ pub struct Config {
pub mass_rebuilder: Option<MassRebuilder>,
/// Configuration for the builder
pub builder: Option<Builder>,
+ /// Configuration for the push filter
+ pub push_filter: Option<PushFilter>,
/// Configuration for the log message collector
pub log_message_collector: Option<LogMessageCollector>,
/// Configuration for the stats server
@@ -57,7 +59,7 @@ fn default_logs_path() -> String {
}
fn default_serve_root() -> String {
- "https://logs.tickborg.project-tick.net/logfile".into()
+ "https://logs.tickborg.projecttick.net/logfile".into()
}
/// Configuration for logapi
@@ -112,6 +114,18 @@ pub struct Builder {
pub rabbitmq: RabbitMqConfig,
}
+/// Configuration for the push filter
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
+pub struct PushFilter {
+ /// RabbitMQ broker to connect to
+ pub rabbitmq: RabbitMqConfig,
+ /// Default projects/attrs to build when no changed projects are detected.
+ /// If empty and no projects detected, push builds are skipped.
+ #[serde(default)]
+ pub default_attrs: Vec<String>,
+}
+
/// Configuration for the log message collector
#[derive(serde::Serialize, serde::Deserialize, Debug)]
#[serde(deny_unknown_fields)]
diff --git a/ofborg/tickborg/src/ghevent/mod.rs b/ofborg/tickborg/src/ghevent/mod.rs
index 243758800a..06d67acef6 100644
--- a/ofborg/tickborg/src/ghevent/mod.rs
+++ b/ofborg/tickborg/src/ghevent/mod.rs
@@ -1,9 +1,11 @@
mod common;
mod issuecomment;
mod pullrequestevent;
+mod pushevent;
pub use self::common::{Comment, GenericWebhook, Issue, Repository, User};
pub use self::issuecomment::{IssueComment, IssueCommentAction};
pub use self::pullrequestevent::{
PullRequest, PullRequestAction, PullRequestEvent, PullRequestState,
};
+pub use self::pushevent::{HeadCommit, PushEvent, Pusher};
diff --git a/ofborg/tickborg/src/ghevent/pushevent.rs b/ofborg/tickborg/src/ghevent/pushevent.rs
new file mode 100644
index 0000000000..9ae45ad9ae
--- /dev/null
+++ b/ofborg/tickborg/src/ghevent/pushevent.rs
@@ -0,0 +1,53 @@
+use crate::ghevent::Repository;
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct PushEvent {
+ #[serde(rename = "ref")]
+ pub git_ref: String,
+ pub before: String,
+ pub after: String,
+ pub created: bool,
+ pub deleted: bool,
+ pub forced: bool,
+ pub repository: Repository,
+ pub pusher: Pusher,
+ pub head_commit: Option<HeadCommit>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct Pusher {
+ pub name: String,
+ pub email: Option<String>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct HeadCommit {
+ pub id: String,
+ pub message: String,
+ pub timestamp: String,
+ pub added: Option<Vec<String>>,
+ pub removed: Option<Vec<String>>,
+ pub modified: Option<Vec<String>>,
+}
+
+impl PushEvent {
+ /// Branch adını döndürür (refs/heads/main -> main)
+ pub fn branch(&self) -> Option<&str> {
+ self.git_ref.strip_prefix("refs/heads/")
+ }
+
+ /// Tag push mi?
+ pub fn is_tag(&self) -> bool {
+ self.git_ref.starts_with("refs/tags/")
+ }
+
+ /// Branch silme event'i mi?
+ pub fn is_delete(&self) -> bool {
+ self.deleted
+ }
+
+ /// Boş commit (000...) mi?
+ pub fn is_zero_sha(&self) -> bool {
+ self.after.chars().all(|c| c == '0')
+ }
+}
diff --git a/ofborg/tickborg/src/message/buildjob.rs b/ofborg/tickborg/src/message/buildjob.rs
index b09eae58bf..c4cc61d1fa 100644
--- a/ofborg/tickborg/src/message/buildjob.rs
+++ b/ofborg/tickborg/src/message/buildjob.rs
@@ -1,5 +1,5 @@
use crate::commentparser::Subset;
-use crate::message::{Pr, Repo};
+use crate::message::{Pr, PushTrigger, Repo};
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct BuildJob {
@@ -10,6 +10,9 @@ pub struct BuildJob {
pub request_id: String,
pub logs: Option<ExchangeQueue>, // (Exchange, Routing Key)
pub statusreport: Option<ExchangeQueue>, // (Exchange, Routing Key)
+ /// If set, this build was triggered by a push event, not a PR.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub push: Option<PushTrigger>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
@@ -42,8 +45,47 @@ impl BuildJob {
logs: Some(logs.unwrap_or((Some("logs".to_owned()), Some(logbackrk)))),
statusreport: Some(statusreport.unwrap_or((Some("build-results".to_owned()), None))),
request_id,
+ push: None,
}
}
+
+ /// Create a build job triggered by a push event.
+ pub fn new_push(
+ repo: Repo,
+ push: PushTrigger,
+ attrs: Vec<String>,
+ request_id: String,
+ ) -> BuildJob {
+ let logbackrk = format!(
+ "{}.push.{}",
+ repo.full_name.to_lowercase(),
+ push.branch.replace('/', "-")
+ );
+
+ // Fill pr with push info so downstream consumers (comment poster, etc.)
+ // can still use pr.head_sha for commit statuses / check runs.
+ let pr = Pr {
+ number: 0,
+ head_sha: push.head_sha.clone(),
+ target_branch: Some(push.branch.clone()),
+ };
+
+ BuildJob {
+ repo,
+ pr,
+ subset: None,
+ attrs,
+ logs: Some((Some("logs".to_owned()), Some(logbackrk))),
+ statusreport: Some((Some("build-results".to_owned()), None)),
+ request_id,
+ push: Some(push),
+ }
+ }
+
+ /// Returns true if this build was triggered by a push event.
+ pub fn is_push(&self) -> bool {
+ self.push.is_some()
+ }
}
pub fn from(data: &[u8]) -> Result<BuildJob, serde_json::error::Error> {
diff --git a/ofborg/tickborg/src/message/buildresult.rs b/ofborg/tickborg/src/message/buildresult.rs
index 122edacae3..de482f64da 100644
--- a/ofborg/tickborg/src/message/buildresult.rs
+++ b/ofborg/tickborg/src/message/buildresult.rs
@@ -1,4 +1,4 @@
-use crate::message::{Pr, Repo};
+use crate::message::{Pr, PushTrigger, Repo};
use hubcaps::checks::Conclusion;
@@ -48,6 +48,7 @@ pub struct LegacyBuildResult {
pub status: BuildStatus,
pub skipped_attrs: Option<Vec<String>>,
pub attempted_attrs: Option<Vec<String>>,
+ pub push: Option<PushTrigger>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
@@ -70,6 +71,8 @@ pub enum BuildResult {
status: BuildStatus,
skipped_attrs: Option<Vec<String>>,
attempted_attrs: Option<Vec<String>>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ push: Option<PushTrigger>,
},
Legacy {
repo: Repo,
@@ -82,6 +85,8 @@ pub enum BuildResult {
status: Option<BuildStatus>,
skipped_attrs: Option<Vec<String>>,
attempted_attrs: Option<Vec<String>>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ push: Option<PushTrigger>,
},
}
@@ -100,6 +105,7 @@ impl BuildResult {
ref request_id,
ref attempted_attrs,
ref skipped_attrs,
+ ref push,
..
} => LegacyBuildResult {
repo: repo.to_owned(),
@@ -111,6 +117,7 @@ impl BuildResult {
status: self.status(),
attempted_attrs: attempted_attrs.to_owned(),
skipped_attrs: skipped_attrs.to_owned(),
+ push: push.to_owned(),
},
BuildResult::V1 {
ref repo,
@@ -121,6 +128,7 @@ impl BuildResult {
ref request_id,
ref attempted_attrs,
ref skipped_attrs,
+ ref push,
..
} => LegacyBuildResult {
repo: repo.to_owned(),
@@ -132,6 +140,7 @@ impl BuildResult {
status: self.status(),
attempted_attrs: attempted_attrs.to_owned(),
skipped_attrs: skipped_attrs.to_owned(),
+ push: push.to_owned(),
},
}
}
@@ -143,6 +152,17 @@ impl BuildResult {
}
}
+ pub fn push(&self) -> Option<PushTrigger> {
+ match self {
+ BuildResult::Legacy { push, .. } => push.to_owned(),
+ BuildResult::V1 { push, .. } => push.to_owned(),
+ }
+ }
+
+ pub fn is_push(&self) -> bool {
+ self.push().is_some()
+ }
+
pub fn status(&self) -> BuildStatus {
match *self {
BuildResult::Legacy {
diff --git a/ofborg/tickborg/src/message/common.rs b/ofborg/tickborg/src/message/common.rs
index c8fcd16ea2..75b5dec04f 100644
--- a/ofborg/tickborg/src/message/common.rs
+++ b/ofborg/tickborg/src/message/common.rs
@@ -6,9 +6,18 @@ pub struct Repo {
pub clone_url: String,
}
-#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Pr {
pub target_branch: Option<String>,
pub number: u64,
pub head_sha: String,
}
+
+/// Information about a push event trigger (direct push to a branch).
+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
+pub struct PushTrigger {
+ pub head_sha: String,
+ pub branch: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub before_sha: Option<String>,
+}
diff --git a/ofborg/tickborg/src/message/mod.rs b/ofborg/tickborg/src/message/mod.rs
index 03551cd1ce..8621f45668 100644
--- a/ofborg/tickborg/src/message/mod.rs
+++ b/ofborg/tickborg/src/message/mod.rs
@@ -4,4 +4,4 @@ pub mod buildresult;
mod common;
pub mod evaluationjob;
-pub use self::common::{Pr, Repo};
+pub use self::common::{Pr, PushTrigger, Repo};
diff --git a/ofborg/tickborg/src/tasks/build.rs b/ofborg/tickborg/src/tasks/build.rs
index 56583b28b4..2bac6d749b 100644
--- a/ofborg/tickborg/src/tasks/build.rs
+++ b/ofborg/tickborg/src/tasks/build.rs
@@ -121,6 +121,7 @@ impl JobActions {
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -209,6 +210,7 @@ impl JobActions {
skipped_attrs: Some(not_attempted_attrs),
attempted_attrs: None,
status: BuildStatus::Skipped,
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -249,6 +251,7 @@ impl JobActions {
status,
attempted_attrs: Some(attempted_attrs),
skipped_attrs: Some(not_attempted_attrs),
+ push: self.job.push.clone(),
};
let result_exchange = self.result_exchange.clone();
@@ -301,7 +304,12 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
dyn notifyworker::NotificationReceiver + std::marker::Send + std::marker::Sync,
>,
) {
- let span = debug_span!("job", pr = ?job.pr.number);
+ let is_push = job.is_push();
+ let span = if is_push {
+ debug_span!("job", push_branch = ?job.push.as_ref().map(|p| &p.branch), sha = %job.pr.head_sha)
+ } else {
+ debug_span!("job", pr = ?job.pr.number)
+ };
let _enter = span.enter();
let actions = self.actions(job, notifier);
@@ -312,10 +320,19 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
return;
}
- info!(
- "Working on https://github.com/{}/pull/{}",
- actions.job.repo.full_name, actions.job.pr.number
- );
+ if is_push {
+ let push = actions.job.push.as_ref().unwrap();
+ info!(
+ "Working on push to {}:{} ({})",
+ actions.job.repo.full_name, push.branch, push.head_sha
+ );
+ } else {
+ info!(
+ "Working on https://github.com/{}/pull/{}",
+ actions.job.repo.full_name, actions.job.pr.number
+ );
+ }
+
let project = self.cloner.project(
&actions.job.repo.full_name,
actions.job.repo.clone_url.clone(),
@@ -331,22 +348,38 @@ impl notifyworker::SimpleNotifyWorker for BuildWorker {
let refpath = co.checkout_origin_ref(target_branch.as_ref()).unwrap();
- if co.fetch_pr(actions.job.pr.number).is_err() {
- info!("Failed to fetch {}", actions.job.pr.number);
- actions.pr_head_missing().await;
- return;
- }
+ if is_push {
+ // For push builds: the commit is already on the branch, just verify it exists
+ if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
+ info!("Push commit {} doesn't exist after fetch", actions.job.pr.head_sha);
+ actions.commit_missing().await;
+ return;
+ }
+ // Checkout the exact pushed commit
+ if co.checkout_ref(actions.job.pr.head_sha.as_ref()).is_err() {
+ info!("Failed to checkout push commit {}", actions.job.pr.head_sha);
+ actions.merge_failed().await;
+ return;
+ }
+ } else {
+ // For PR builds: fetch PR ref, verify commit, merge
+ if co.fetch_pr(actions.job.pr.number).is_err() {
+ info!("Failed to fetch {}", actions.job.pr.number);
+ actions.pr_head_missing().await;
+ return;
+ }
- if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
- info!("Commit {} doesn't exist", actions.job.pr.head_sha);
- actions.commit_missing().await;
- return;
- }
+ if !co.commit_exists(actions.job.pr.head_sha.as_ref()) {
+ info!("Commit {} doesn't exist", actions.job.pr.head_sha);
+ actions.commit_missing().await;
+ return;
+ }
- if co.merge_commit(actions.job.pr.head_sha.as_ref()).is_err() {
- info!("Failed to merge {}", actions.job.pr.head_sha);
- actions.merge_failed().await;
- return;
+ if co.merge_commit(actions.job.pr.head_sha.as_ref()).is_err() {
+ info!("Failed to merge {}", actions.job.pr.head_sha);
+ actions.merge_failed().await;
+ return;
+ }
}
// Determine which projects to build from the requested attrs
@@ -528,6 +561,7 @@ mod tests {
logs: Some((Some(String::from("logs")), Some(String::from("build.log")))),
statusreport: Some((Some(String::from("build-results")), None)),
request_id: "bogus-request-id".to_owned(),
+ push: None,
};
let dummyreceiver = Arc::new(notifyworker::DummyNotificationReceiver::new());
@@ -574,6 +608,7 @@ mod tests {
logs: Some((Some(String::from("logs")), Some(String::from("build.log")))),
statusreport: Some((Some(String::from("build-results")), None)),
request_id: "bogus-request-id".to_owned(),
+ push: None,
};
let dummyreceiver = Arc::new(notifyworker::DummyNotificationReceiver::new());
diff --git a/ofborg/tickborg/src/tasks/githubcommentposter.rs b/ofborg/tickborg/src/tasks/githubcommentposter.rs
index 70c4a118e4..2f49d7401b 100644
--- a/ofborg/tickborg/src/tasks/githubcommentposter.rs
+++ b/ofborg/tickborg/src/tasks/githubcommentposter.rs
@@ -71,7 +71,11 @@ impl worker::SimpleWorker for GitHubCommentPoster {
}
};
- let span = debug_span!("job", pr = ?pr.number);
+ let span = if pr.number == 0 {
+ debug_span!("job", push_sha = %pr.head_sha)
+ } else {
+ debug_span!("job", pr = ?pr.number)
+ };
let _enter = span.enter();
for check in checks {
@@ -111,17 +115,40 @@ fn job_to_check(job: &BuildJob, architecture: &str, timestamp: DateTime<Utc>) ->
all_attrs = vec![String::from("(unknown attributes)")];
}
+ let details_key = if job.is_push() {
+ format!(
+ "push.{}",
+ job.push
+ .as_ref()
+ .map(|p| p.branch.replace('/', "-"))
+ .unwrap_or_default()
+ )
+ } else {
+ format!("{}", job.pr.number)
+ };
+
+ let name = if job.is_push() {
+ let branch = job
+ .push
+ .as_ref()
+ .map(|p| p.branch.as_str())
+ .unwrap_or("unknown");
+ format!("{} on {architecture} (push to {branch})", all_attrs.join(", "))
+ } else {
+ format!("{} on {architecture}", all_attrs.join(", "))
+ };
+
CheckRunOptions {
- name: format!("{} on {architecture}", all_attrs.join(", ")),
+ name,
actions: None,
completed_at: None,
started_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
conclusion: None,
details_url: Some(format!(
- "https://logs.tickborg.project-tick.net/?key={}/{}.{}",
+ "https://logs.tickborg.projecttick.net/?key={}/{}.{}",
&job.repo.owner.to_lowercase(),
&job.repo.name.to_lowercase(),
- job.pr.number,
+ details_key,
)),
external_id: None,
head_sha: job.pr.head_sha.clone(),
@@ -180,17 +207,47 @@ fn result_to_check(result: &LegacyBuildResult, timestamp: DateTime<Utc>) -> Chec
String::from("No partial log is available.")
};
+ let is_push = result.push.is_some();
+
+ let details_key = if is_push {
+ format!(
+ "push.{}",
+ result
+ .push
+ .as_ref()
+ .map(|p| p.branch.replace('/', "-"))
+ .unwrap_or_default()
+ )
+ } else {
+ format!("{}", result.pr.number)
+ };
+
+ let name = if is_push {
+ let branch = result
+ .push
+ .as_ref()
+ .map(|p| p.branch.as_str())
+ .unwrap_or("unknown");
+ format!(
+ "{} on {} (push to {branch})",
+ all_attrs.join(", "),
+ result.system
+ )
+ } else {
+ format!("{} on {}", all_attrs.join(", "), result.system)
+ };
+
CheckRunOptions {
- name: format!("{} on {}", all_attrs.join(", "), result.system),
+ name,
actions: None,
completed_at: Some(timestamp.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
started_at: None,
conclusion: Some(conclusion),
details_url: Some(format!(
- "https://logs.tickborg.project-tick.net/?key={}/{}.{}&attempt_id={}",
+ "https://logs.tickborg.projecttick.net/?key={}/{}.{}&attempt_id={}",
&result.repo.owner.to_lowercase(),
&result.repo.name.to_lowercase(),
- result.pr.number,
+ details_key,
result.attempt_id,
)),
external_id: Some(result.attempt_id.clone()),
@@ -244,6 +301,7 @@ mod tests {
request_id: "bogus-request-id".to_owned(),
attrs: vec!["foo".to_owned(), "bar".to_owned()],
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -256,7 +314,7 @@ mod tests {
completed_at: None,
status: Some(CheckRunState::Queued),
conclusion: None,
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345".to_string()),
external_id: None,
head_sha: "abc123".to_string(),
output: None,
@@ -296,6 +354,7 @@ mod tests {
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: Some(vec!["bar".to_owned()]),
status: BuildStatus::Success,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -310,7 +369,7 @@ mod tests {
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -378,6 +437,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -392,7 +452,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -457,6 +517,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: None,
status: BuildStatus::TimedOut,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -471,7 +532,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -537,6 +598,7 @@ error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Success,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -551,7 +613,7 @@ error: build of '/nix/store/l1limh50lx2cx45yb2gqpv7k8xl1mik2-gdb-8.1.drv' failed
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Success),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -615,6 +677,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: None,
skipped_attrs: None,
status: BuildStatus::Failure,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -629,7 +692,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Neutral),
details_url: Some(
- "https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
+ "https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid"
.to_string()
),
external_id: Some("neatattemptid".to_string()),
@@ -682,6 +745,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -695,7 +759,7 @@ patching script interpreter paths in /nix/store/pcja75y9isdvgz5i00pkrpif9rxzxc29
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
@@ -735,6 +799,7 @@ foo
attempted_attrs: None,
skipped_attrs: Some(vec!["not-attempted".to_owned()]),
status: BuildStatus::Skipped,
+ push: None,
};
let timestamp = Utc.with_ymd_and_hms(2023, 4, 20, 13, 37, 42).unwrap();
@@ -748,7 +813,7 @@ foo
completed_at: Some("2023-04-20T13:37:42Z".to_string()),
status: Some(CheckRunState::Completed),
conclusion: Some(Conclusion::Skipped),
- details_url: Some("https://logs.tickborg.project-tick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
+ details_url: Some("https://logs.tickborg.projecttick.net/?key=project-tick/Project-Tick.2345&attempt_id=neatattemptid".to_string()),
external_id: Some("neatattemptid".to_string()),
head_sha: "abc123".to_string(),
output: Some(Output {
diff --git a/ofborg/tickborg/src/tasks/log_message_collector.rs b/ofborg/tickborg/src/tasks/log_message_collector.rs
index 2d80f72f03..302445e2ff 100644
--- a/ofborg/tickborg/src/tasks/log_message_collector.rs
+++ b/ofborg/tickborg/src/tasks/log_message_collector.rs
@@ -215,7 +215,7 @@ impl worker::SimpleWorker for LogMessageCollector {
// Make sure the log content exists by opening its handle.
// This (hopefully) prevents builds that produce no output (for any reason) from
- // having their logs.tickborg.project-tick.net link complaining about a 404.
+ // having their logs.tickborg.projecttick.net link complaining about a 404.
let _ = self.handle_for(&job.from).unwrap();
}
MsgType::Msg(ref message) => {
@@ -448,6 +448,7 @@ mod tests {
status: BuildStatus::Success,
attempted_attrs: Some(vec!["foo".to_owned()]),
skipped_attrs: Some(vec!["bar".to_owned()]),
+ push: None,
}))
})
.await
diff --git a/ofborg/tickborg/src/tasks/mod.rs b/ofborg/tickborg/src/tasks/mod.rs
index 5aab0fa631..3bf701870d 100644
--- a/ofborg/tickborg/src/tasks/mod.rs
+++ b/ofborg/tickborg/src/tasks/mod.rs
@@ -5,4 +5,5 @@ pub mod evaluationfilter;
pub mod githubcommentfilter;
pub mod githubcommentposter;
pub mod log_message_collector;
+pub mod pushfilter;
pub mod statscollector;
diff --git a/ofborg/tickborg/src/tasks/pushfilter.rs b/ofborg/tickborg/src/tasks/pushfilter.rs
new file mode 100644
index 0000000000..8cf8d7a0ef
--- /dev/null
+++ b/ofborg/tickborg/src/tasks/pushfilter.rs
@@ -0,0 +1,165 @@
+use crate::acl;
+use crate::ghevent;
+use crate::message::buildjob;
+use crate::message::{PushTrigger, Repo};
+use crate::systems;
+use crate::worker;
+
+use tracing::{debug_span, info};
+use uuid::Uuid;
+
+pub struct PushFilterWorker {
+ acl: acl::Acl,
+ /// Default projects/attrs to build when push doesn't match any known project.
+ default_attrs: Vec<String>,
+}
+
+impl PushFilterWorker {
+ pub fn new(acl: acl::Acl, default_attrs: Vec<String>) -> PushFilterWorker {
+ PushFilterWorker {
+ acl,
+ default_attrs,
+ }
+ }
+}
+
+impl worker::SimpleWorker for PushFilterWorker {
+ type J = ghevent::PushEvent;
+
+ async fn msg_to_job(
+ &mut self,
+ _: &str,
+ _: &Option<String>,
+ body: &[u8],
+ ) -> Result<Self::J, String> {
+ match serde_json::from_slice(body) {
+ Ok(event) => Ok(event),
+ Err(err) => Err(format!(
+ "Failed to deserialize push event {err:?}: {:?}",
+ std::str::from_utf8(body).unwrap_or("<job not utf8>")
+ )),
+ }
+ }
+
+ async fn consumer(&mut self, job: &ghevent::PushEvent) -> worker::Actions {
+ let branch = job.branch().unwrap_or_default();
+ let span = debug_span!("push", branch = %branch, after = %job.after);
+ let _enter = span.enter();
+
+ if !self.acl.is_repo_eligible(&job.repository.full_name) {
+ info!("Repo not authorized ({})", job.repository.full_name);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip tag events
+ if job.is_tag() {
+ info!("Skipping tag push: {}", job.git_ref);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip branch deletion events
+ if job.is_delete() {
+ info!("Skipping branch delete: {}", job.git_ref);
+ return vec![worker::Action::Ack];
+ }
+
+ // Skip zero SHA (shouldn't happen for non-delete, but just in case)
+ if job.is_zero_sha() {
+ info!("Skipping zero SHA push");
+ return vec![worker::Action::Ack];
+ }
+
+ let branch_name = branch.to_string();
+ info!(
+ "Processing push to {}:{} ({})",
+ job.repository.full_name, branch_name, job.after
+ );
+
+ // Detect which projects changed from the push event's commit info
+ let changed_files: Vec<String> = if let Some(ref head) = job.head_commit {
+ let mut files = Vec::new();
+ if let Some(ref added) = head.added {
+ files.extend(added.iter().cloned());
+ }
+ if let Some(ref removed) = head.removed {
+ files.extend(removed.iter().cloned());
+ }
+ if let Some(ref modified) = head.modified {
+ files.extend(modified.iter().cloned());
+ }
+ files
+ } else {
+ Vec::new()
+ };
+
+ let attrs = if !changed_files.is_empty() {
+ let detected = crate::buildtool::detect_changed_projects(&changed_files);
+ if detected.is_empty() {
+ info!("No known projects changed in push, using defaults");
+ self.default_attrs.clone()
+ } else {
+ info!("Detected changed projects: {:?}", detected);
+ detected
+ }
+ } else {
+ info!("No file change info in push event, using defaults");
+ self.default_attrs.clone()
+ };
+
+ if attrs.is_empty() {
+ info!("No projects to build, skipping push");
+ return vec![worker::Action::Ack];
+ }
+
+ let repo_msg = Repo {
+ clone_url: job.repository.clone_url.clone(),
+ full_name: job.repository.full_name.clone(),
+ owner: job.repository.owner.login.clone(),
+ name: job.repository.name.clone(),
+ };
+
+ let push_trigger = PushTrigger {
+ head_sha: job.after.clone(),
+ branch: branch_name,
+ before_sha: Some(job.before.clone()),
+ };
+
+ let request_id = Uuid::new_v4().to_string();
+
+ let build_job = buildjob::BuildJob::new_push(
+ repo_msg.clone(),
+ push_trigger,
+ attrs.clone(),
+ request_id,
+ );
+
+ // Schedule the build on all known architectures
+ let build_archs = systems::System::primary_systems();
+ let mut response = vec![];
+
+ info!(
+ "Scheduling push build for {:?} on {:?}",
+ attrs, build_archs
+ );
+
+ for arch in &build_archs {
+ let (exchange, routingkey) = arch.as_build_destination();
+ response.push(worker::publish_serde_action(
+ exchange, routingkey, &build_job,
+ ));
+ }
+
+ // Also publish to build-results for the comment poster to pick up
+ response.push(worker::publish_serde_action(
+ Some("build-results".to_string()),
+ None,
+ &buildjob::QueuedBuildJobs {
+ job: build_job,
+ architectures: build_archs.iter().map(|a| a.to_string()).collect(),
+ },
+ ));
+
+ response.push(worker::Action::Ack);
+ response
+ }
+}