12 Commits

Author SHA1 Message Date
a0f67464b5 Simplify build-and-release workflow by switching to akkuman/gitea-release-action and enabling checksum generation.
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 7s
2025-10-24 23:41:29 +02:00
ff5699e627 Update workflow to specify branches for push and pull_request events 2025-10-24 23:35:28 +02:00
dc0040c2d1 Add workflow to check Go code on push and pull requests 2025-10-24 23:34:15 +02:00
9950282d6e Update build-and-release workflow to use Gitea-hosted setup-go action
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 7s
2025-10-24 23:33:31 +02:00
7c8e8eb236 Clean code by commenting out some variable, and allowing some dead code. 2025-10-24 23:24:47 +02:00
442b5d25b4 Release v0.4.0
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 1m40s
2025-10-24 18:08:01 +02:00
4763d48290 Add PATCH support in Gitea client and update issue handling
Extend Gitea client with a `patch` method for executing PATCH requests. Introduce `IssueWriteSetBody` struct for serializing issue update payloads. Update Gitea action to append related issues and send PATCH requests to update issue descriptions.
2025-10-24 18:04:28 +02:00
1af6596b51 Enhance issue handling by adding utils module
Introduce `append_related_issues` function and `IssueRelated` enum to manage related issue linking. Update Gitea action to utilize the new functionality for appending related issues in work package content. Add corresponding tests.
2025-10-24 17:32:10 +02:00
c5f4f9fcf9 Release v0.3.0
Some checks failed
Release binary for cl-cli / build-and-release (push) Failing after 1m27s
2024-11-17 21:25:32 +01:00
9f544c66c2 Add "open" crate and automate browser opening for work packages
Added the "open" crate to Cargo.toml and corresponding dependencies to Cargo.lock. Updated the code to automatically open the newly created work package in the browser and handle potential failure cases gracefully.
2024-11-17 21:24:41 +01:00
7c1cdb64ec fix multiple gitlab token possible 2024-11-14 15:23:25 +01:00
4bb787488d example configuation file 2024-11-14 15:22:14 +01:00
19 changed files with 357 additions and 57 deletions

3
.changes/v0.3.0.md Normal file
View File

@@ -0,0 +1,3 @@
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created

3
.changes/v0.4.0.md Normal file
View File

@@ -0,0 +1,3 @@
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"

View File

@@ -20,17 +20,14 @@ jobs:
id: read_release
with:
path: .changes/${{ github.ref_name }}.md
- name: Setup go for using go gitea actions
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Use Go Action to release
id: use-go-action
uses: https://gitea.com/actions/release-action@main
- name: Release
uses: akkuman/gitea-release-action@v1
#env:
# NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
files: |-
target/release/cl-cli
config.toml.dist
api_key: '${{secrets.RELEASE_TOKEN}}'
body: |
${{ steps.read_release.outputs.content }}
md5sum: true
sha256sum: true
body_path: .changes/${{ github.ref_name }}.md

View File

@@ -0,0 +1,20 @@
name: Check go code
on:
push:
branches: *
pull_request:
branches: *
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: https://github.com/actions/checkout@v4
- name: Install rust toolchain
uses: https://github.com/dtolnay/rust-toolchain@stable
- name: Build binaries
run: cargo build
- name: Run tests
run: cargo test

View File

@@ -6,6 +6,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v0.4.0 - 2025-10-24
### Added
* [planning - i2work] Met à jour le body du ticket gitea avec un lien vers le work package, dans une section "related issues"
## v0.3.0 - 2024-11-17
### Added
* Open the newly create work package after the wp has been created
## v0.2.0 - 2024-11-14
### Added
* Add an option `--assign-to-me` to automatically assign the openproject's user to the newly created work package

37
Cargo.lock generated
View File

@@ -111,6 +111,7 @@ dependencies = [
"clap",
"gitlab",
"log",
"open",
"reqwest",
"serde",
"simple-home-dir",
@@ -738,6 +739,15 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-terminal"
version = "0.4.5"
@@ -750,6 +760,16 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -909,6 +929,17 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "open"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.62"
@@ -959,6 +990,12 @@ version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
[[package]]
name = "pathdiff"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
[[package]]
name = "percent-encoding"
version = "2.2.0"

View File

@@ -15,3 +15,4 @@ url = "2.3.1"
tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"] }
simple-home-dir = "0.2.1"
log = "0.4.17"
open = "5.3.1"

View File

@@ -1,6 +1,11 @@
[gitlab]
[[gitlab]]
# generate from https://gitlab.com/-/user_settings/personal_access_tokens
token = "glpat-example"
domain = "gitlab.com"
[[gitea]]
token = "abcdexempletoken"
domain = "gitea.champs-libres.be"
[openproject]
# generate api token from https://champs-libres.openproject.com/my/access_token

View File

@@ -7,7 +7,7 @@ pub struct GeneralError {
}
impl From<InvalidHeaderValue> for GeneralError {
fn from(value: InvalidHeaderValue) -> Self {
fn from(_value: InvalidHeaderValue) -> Self {
GeneralError {
description: "Unable to convert the token into header value".to_string(),
}

View File

@@ -2,9 +2,10 @@ use crate::cli::Issue2Work;
use crate::config::Config;
use crate::error::GeneralError;
use crate::gitea::client::has_client_for_url;
use crate::gitea::issue::{issue_html_url_to_api, Issue};
use crate::gitea::issue::{issue_html_url_to_api, Issue, IssueWriteSetBody};
use crate::openproject::user::{GetMe, User};
use crate::openproject::work::WorkPackageWriter;
use crate::planning::utils::{append_related_issues, IssueRelated};
use crate::planning::Issue2WorkActionTrait;
use url::Url;
@@ -32,11 +33,31 @@ impl Issue2WorkActionTrait for GiteaAction {
.create_work_package(&work_package, &args.project_id)
.await?;
println!(
"new work package created: {:?}, edit at {}/projects/{}/work_packages/{}",
work_package.subject, config.openproject.base_url, args.project_id, work_package.id
let url_wp = format!(
"{}/projects/{}/work_packages/{}",
config.openproject.base_url, args.project_id, work_package.id
);
let content = append_related_issues(
&IssueRelated::OpenProjectIssue(url_wp.to_string()),
&issue.body,
);
let _u: Issue = gitea_client
.patch(
issue_html_url_to_api(url)?,
&IssueWriteSetBody { body: content },
)
.await?;
println!(
"new work package created: {:?}, edit at {}",
work_package.subject, url_wp
);
if let Err(e) = open::that(url_wp) {
println!("failed to open work package in browser: {}", e);
};
Ok(())
}
@@ -47,7 +68,10 @@ impl Issue2WorkActionTrait for GiteaAction {
fn create_work_package_from_issue(issue: &Issue, assignee: Option<User>) -> WorkPackageWriter {
WorkPackageWriter {
subject: format!("{} ({}/{})", issue.title, issue.repository.full_name, issue.number),
subject: format!(
"{} ({}/{})",
issue.title, issue.repository.full_name, issue.number
),
work_type: "TASK".into(),
description: crate::openproject::work::DescriptionWriter {
format: "markdown".into(),

View File

@@ -3,12 +3,13 @@ use crate::error::GeneralError;
use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION};
use reqwest::{ClientBuilder, StatusCode};
use serde::de::DeserializeOwned;
use serde::Serialize;
use url::Url;
#[derive(Debug)]
pub struct Client {
token: String,
base_uri: String,
// base_uri: String,
}
fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool {
@@ -28,6 +29,7 @@ pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool {
false
}
#[allow(dead_code)]
fn client_for_url(url: &Url, config: &Config) -> Result<Client, GeneralError> {
for c in &config.gitea {
if is_client_for_url(url, c) {
@@ -45,10 +47,10 @@ impl Client {
Self::new(&config.token, &config.domain)
}
pub fn new(token: &String, domain: &String) -> Self {
pub fn new(token: &String, _domain: &String) -> Self {
Client {
token: token.clone(),
base_uri: format!("https://{}", domain.clone()),
// base_uri: format!("https://{}", domain.clone()),
}
}
@@ -79,6 +81,38 @@ impl Client {
}),
}
}
pub async fn patch<T: DeserializeOwned, B: Serialize>(
&self,
url: Url,
body: &B,
) -> Result<T, GeneralError> {
let mut headers = HeaderMap::new();
headers.append(AUTHORIZATION, format!("token {}", self.token).parse()?);
headers.append(ACCEPT, "application/json".parse()?);
let client = ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap();
let response = client.patch(url.clone()).json(body).send().await?;
match response.status() {
StatusCode::OK | StatusCode::CREATED => {
let result: T = response.json().await?;
Ok(result)
}
_ => Err(GeneralError {
description: format!(
"Could not call PATCH on {:?}, error code {}",
url,
response.status()
),
}),
}
}
}
#[cfg(test)]

View File

@@ -1,12 +1,11 @@
use crate::error::GeneralError;
use crate::gitea::client::Client;
use crate::gitea::repository::Repository;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Deserialize)]
pub struct Issue {
pub id: u64,
// pub id: u64,
pub number: u64,
pub title: String,
pub body: String,
@@ -14,14 +13,9 @@ pub struct Issue {
pub html_url: String,
}
pub trait IssueClient {
fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option<Issue>;
}
impl IssueClient for Client {
fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option<Issue> {
todo!()
}
#[derive(Debug, Serialize)]
pub struct IssueWriteSetBody {
pub body: String,
}
pub fn issue_html_url_to_api(url: &Url) -> Result<Url, GeneralError> {

View File

@@ -2,8 +2,8 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Repository {
id: u64,
name: String,
owner: String,
// id: u64,
// name: String,
// owner: String,
pub full_name: String,
}

View File

@@ -3,10 +3,6 @@ use crate::error::GeneralError;
use gitlab::{AsyncGitlab, GitlabBuilder};
use url::Url;
pub struct ClientProvider {}
impl ClientProvider {}
fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool {
if url.domain() == Some(config.domain.as_str()) {
return true;

View File

@@ -1,13 +1,13 @@
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct HalEntity {
#[serde(rename = "_type")]
pub d_type: String,
}
// #[derive(Deserialize, Debug)]
// pub struct HalEntity {
// // #[serde(rename = "_type")]
// // pub d_type: String,
// }
#[derive(Deserialize, Debug, Clone)]
pub struct Link {
pub href: String,
pub title: Option<String>,
// pub title: Option<String>,
}

View File

@@ -1,11 +1,11 @@
use crate::openproject::client::{handle_response_status, Client, OpenProjectError};
use crate::openproject::hal::HalEntity;
// use crate::openproject::hal::HalEntity;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct User {
pub href: String,
pub title: String,
// pub title: String,
}
#[derive(Deserialize, Debug)]
pub struct Links {
@@ -13,12 +13,12 @@ pub struct Links {
}
#[derive(Deserialize, Debug)]
pub struct Root {
#[serde(rename = "instanceName")]
pub instance_name: String,
// #[serde(rename = "instanceName")]
// pub instance_name: String,
#[serde(rename = "_links")]
pub links: Links,
#[serde(flatten)]
pub hal_entity: HalEntity,
// #[serde(flatten)]
// pub hal_entity: HalEntity,
}
pub trait RootClient {

View File

@@ -10,13 +10,10 @@ pub struct UserLink {
}
#[derive(Deserialize, Debug, Clone)]
pub struct User {
#[serde(rename = "_type")]
#[allow(unused_variables)]
pub d_type: String,
#[allow(unused_variables)]
pub id: u64,
#[allow(unused_variables)]
pub name: String,
// #[serde(rename = "_type")]
// pub d_type: String,
// pub id: u64,
// pub name: String,
#[serde(rename = "_links")]
pub d_links: UserLink,
}

View File

@@ -4,6 +4,7 @@ use crate::error::GeneralError;
use url::Url;
pub(crate) mod issue2work;
pub mod utils;
pub trait Issue2WorkActionTrait {
async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>;

180
src/planning/utils.rs Normal file
View File

@@ -0,0 +1,180 @@
pub enum IssueRelated {
OpenProjectIssue(String),
}
pub fn append_related_issues(issue: &IssueRelated, content: &String) -> String {
let mut splitted = content.lines();
let mut new_content: Vec<String> = Vec::new();
let mut found = false;
let mut iteration_started = false;
while let Some(line) = splitted.next() {
if line.contains("## Related issues") {
found = true;
new_content.push(line.parse().unwrap());
// we go to the end of the section
while let Some(line) = splitted.next() {
if line.starts_with("-") || line.starts_with("*") {
iteration_started = true;
new_content.push(line.parse().unwrap());
} else if (line.trim().is_empty()) && iteration_started {
new_content.append(&mut add_related_issues_section(issue));
iteration_started = false;
found = true;
new_content.push(line.parse().unwrap());
break;
} else if line.starts_with("#") {
new_content.push("new title found".to_string());
found = true;
new_content.append(&mut add_related_issues_section(issue));
new_content.push("".to_string());
new_content.push(line.parse().unwrap());
break;
} else {
new_content.push(line.parse().unwrap());
}
}
} else {
new_content.push(line.parse().unwrap());
}
}
if !found || iteration_started {
if !found {
new_content.append(&mut add_related_issues_title());
}
new_content.append(&mut add_related_issues_section(issue));
}
new_content.join(&"\n")
}
fn add_related_issues_title() -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push("".to_string());
previous.push("## Related issues".to_string());
previous.push("".to_string());
previous
}
fn add_related_issues_section(issue: &IssueRelated) -> Vec<String> {
let mut previous: Vec<String> = Vec::new();
previous.push(convert_issue_link_items(issue));
previous
}
fn convert_issue_link_items(issue: &IssueRelated) -> String {
match issue {
IssueRelated::OpenProjectIssue(issue_url) => format!("- [{}]({})", issue_url, issue_url),
}
}
#[cfg(test)]
mod tests {
use crate::planning::utils::{append_related_issues, IssueRelated};
#[test]
pub fn test_append_related_issues_content_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(&issue, &("".to_string()))
);
}
#[test]
pub fn test_append_related_issues_content_not_empty() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&"Something happens.\n\
\n\
## Some title\n\
\n\
Some content"
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)"#
.to_string()
),
);
}
#[test]
pub fn test_append_related_issues_existing_section_related_issues_not_last() {
let issue = IssueRelated::OpenProjectIssue("https://example/wp/1".to_string());
assert_eq!(
r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
- [https://example/wp/1](https://example/wp/1)
## Other content
Some other content"#,
append_related_issues(
&issue,
&r#"Something happens.
## Some title
Some content
## Related issues
- [https://example/wp/2](https://example/wp/2)
## Other content
Some other content"#
.to_string()
),
);
}
}