From 0b40c16db4fcfffff66f665308a2d42e4e1d2772 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 23 Nov 2023 11:39:12 +0100 Subject: [PATCH] gitea: Implement commit statuses For now this is just a simple implementation only supporting `put` steps. `in` is a noop, so `no_get: true` is recommended. --- images/gitea/shims/status/check | 2 + images/gitea/shims/status/in | 2 + images/gitea/shims/status/out | 2 + images/gitea/src/cmd/package.rs | 23 +---- images/gitea/src/cmd/pr.rs | 22 +---- images/gitea/src/cmd/status.rs | 154 ++++++++++++++++++++++++++++++++ images/gitea/src/main.rs | 7 ++ images/gitea/src/util.rs | 19 ++++ 8 files changed, 194 insertions(+), 37 deletions(-) create mode 100755 images/gitea/shims/status/check create mode 100755 images/gitea/shims/status/in create mode 100755 images/gitea/shims/status/out create mode 100644 images/gitea/src/cmd/status.rs create mode 100644 images/gitea/src/util.rs diff --git a/images/gitea/shims/status/check b/images/gitea/shims/status/check new file mode 100755 index 0000000..05c370e --- /dev/null +++ b/images/gitea/shims/status/check @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/gitea status check "$@" diff --git a/images/gitea/shims/status/in b/images/gitea/shims/status/in new file mode 100755 index 0000000..c652bb8 --- /dev/null +++ b/images/gitea/shims/status/in @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/gitea status in "$@" diff --git a/images/gitea/shims/status/out b/images/gitea/shims/status/out new file mode 100755 index 0000000..af0e7e5 --- /dev/null +++ b/images/gitea/shims/status/out @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/gitea status out "$@" diff --git a/images/gitea/src/cmd/package.rs b/images/gitea/src/cmd/package.rs index ec43d59..d82d765 100644 --- a/images/gitea/src/cmd/package.rs +++ b/images/gitea/src/cmd/package.rs @@ -7,8 +7,6 @@ use cli_table::{Cell, Style, Table}; use color_eyre::eyre::{self, Context}; use color_eyre::{Help, Report, Result}; use globwalk::{DirEntry, GlobWalkerBuilder}; -use reqwest::blocking::Client; -use reqwest::header::HeaderMap; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -16,7 +14,8 @@ use time::format_description::well_known::Iso8601; use url::Url; use crate::types::{Package, PackageType}; -use crate::{Action, USER_AGENT}; +use crate::util::make_client; +use crate::Action; #[derive(Deserialize, Serialize, Debug)] struct Source { @@ -56,22 +55,8 @@ struct Config { params: Option, } -fn make_client(src: &Source) -> Result { - let mut headers = HeaderMap::new(); - headers.insert( - "Authorization", - format!("token {}", src.access_token).try_into()?, - ); - - Client::builder() - .default_headers(headers) - .user_agent(USER_AGENT) - .build() - .map_err(From::from) -} - fn action_check(conf: Config) -> Result<()> { - let client = make_client(&conf.source)?; + let client = make_client(&conf.source.access_token)?; let url = conf .source .url @@ -158,7 +143,7 @@ fn action_out(conf: Config, dir: impl AsRef) -> Result<()> { eyre::bail!("`params.globs` must not be empty"); } - let client = make_client(&conf.source)?; + let client = make_client(&conf.source.access_token)?; let url = conf .source .url diff --git a/images/gitea/src/cmd/pr.rs b/images/gitea/src/cmd/pr.rs index eafbcff..a20f39a 100644 --- a/images/gitea/src/cmd/pr.rs +++ b/images/gitea/src/cmd/pr.rs @@ -6,15 +6,15 @@ use cli_table::format::Justify; use cli_table::{print_stderr, Cell, Style, Table}; use color_eyre::eyre::{self, Context}; use color_eyre::Result; -use reqwest::blocking::{Client, Response}; -use reqwest::header::HeaderMap; +use reqwest::blocking::Response; use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; use url::Url; use crate::types::PullRequest; -use crate::{Action, USER_AGENT}; +use crate::util::make_client; +use crate::Action; #[derive(Deserialize, Serialize, Debug)] struct Source { @@ -59,22 +59,8 @@ struct Config { version: Option, } -fn make_client(src: &Source) -> Result { - let mut headers = HeaderMap::new(); - headers.insert( - "Authorization", - format!("token {}", src.access_token).try_into()?, - ); - - Client::builder() - .default_headers(headers) - .user_agent(USER_AGENT) - .build() - .map_err(From::from) -} - fn fetch(src: &Source) -> Result { - let client = make_client(&src).wrap_err("Failed to create HTTP client")?; + let client = make_client(&src.access_token).wrap_err("Failed to create HTTP client")?; let url = src .url .join(&format!("api/v1/repos/{}/{}/pulls", src.owner, src.repo)) diff --git a/images/gitea/src/cmd/status.rs b/images/gitea/src/cmd/status.rs new file mode 100644 index 0000000..5533bae --- /dev/null +++ b/images/gitea/src/cmd/status.rs @@ -0,0 +1,154 @@ +use color_eyre::eyre; +use color_eyre::eyre::WrapErr; +use color_eyre::Help; +use color_eyre::Result; +use color_eyre::SectionExt; +use reqwest::StatusCode; +use serde::Deserialize; +use serde::Serialize; +use serde_json::json; +use std::io; +use std::io::Read; +use std::path::Path; +use url::Url; + +use crate::util::make_client; +use crate::Action; + +#[derive(Deserialize, Serialize, Debug)] +struct Source { + access_token: String, + owner: String, + url: Url, + repo: String, + sha: String, + context: String, + description: Option, + target_url: Option, +} + +#[derive(Deserialize, Serialize, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +enum State { + Pending, + Success, + Error, + Failure, + Warning, +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Pending => write!(f, "pending"), + State::Success => write!(f, "success"), + State::Error => write!(f, "error"), + State::Failure => write!(f, "failure"), + State::Warning => write!(f, "warning"), + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +struct Params { + state: State, + description: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +struct Config { + /// Resource configuration. + /// Passed verbatim from the definition in the pipeline. + source: Source, + params: Params, +} + +fn action_out(conf: Config, _dir: impl AsRef) -> Result<()> { + let params = conf.params; + let description = params.description.or(conf.source.description); + + let client = make_client(&conf.source.access_token)?; + let url = conf + .source + .url + .join(&format!( + // The trailing slash is required, to make sure this entire URL is used + // when `join`ing the file name later. Otherwise, the last component + // would be considered a "file" part itself, and replaced by a future `join`. + "api/v1/repos/{}/{}/statuses/{}", + conf.source.owner, conf.source.repo, conf.source.sha + )) + .wrap_err("Invalid URL")?; + + let body = json!({ + "context": conf.source.context, + "description": description, + "state": params.state, + "target_url": conf.source.target_url, + }); + + let res = client + .post(url.clone()) + .json(&body) + .send() + .wrap_err_with(|| format!("Failed to send request 'POST {}'", url))?; + + match res.status() { + StatusCode::CREATED => { + eprintln!( + "Created status '{}' on commit '{}'", + params.state, conf.source.sha + ); + } + StatusCode::BAD_REQUEST => { + eyre::bail!( + "Invalid request: {:?}. state={}, context={}, description={:?}, target_url={:?}", + res.text(), + params.state, + conf.source.context, + description, + conf.source.target_url + ) + } + code => { + eyre::bail!("Unexpected status code {}\ntext = {:?}", code, res.text()); + } + } + + println!("{{ \"version\": {{}} }}"); + Ok(()) +} + +pub(crate) fn run(action: &Action) -> Result<()> { + // TODO: Gitea does actually support fetching statuses, making `check` and `in` viable, + // theoretically. But it also doesn't make much sense to implement them. + match action { + Action::Check => { + // Dummy implemented that always reports nothing. + println!("[]"); + Ok(()) + } + Action::In { dest: _ } => { + println!("{{}}"); + Ok(()) + } + Action::Out { src } => { + let config: Config = { + let mut buf = String::new(); + io::stdin() + .read_to_string(&mut buf) + .wrap_err("Failed to read from stdin")?; + + if buf.is_empty() { + eyre::bail!("No data received on stdin"); + } + + serde_json::from_str(&buf) + .wrap_err("Failed to parse stdin") + .with_section(|| buf.header("JSON"))? + }; + + action_out(config, src) + } + } +} diff --git a/images/gitea/src/main.rs b/images/gitea/src/main.rs index 4e6e13f..23a608d 100644 --- a/images/gitea/src/main.rs +++ b/images/gitea/src/main.rs @@ -23,6 +23,10 @@ enum Commands { #[command(subcommand)] action: Action, }, + Status { + #[command(subcommand)] + action: Action, + }, } #[derive(Clone, Debug, Subcommand)] @@ -33,9 +37,11 @@ pub(crate) enum Action { } mod types; +mod util; mod cmd { pub mod package; pub mod pr; + pub mod status; } fn main() -> Result<()> { @@ -45,5 +51,6 @@ fn main() -> Result<()> { match &cli.command { Commands::Pr { action } => cmd::pr::run(action), Commands::Package { action } => cmd::package::run(action), + Commands::Status { action } => cmd::status::run(action), } } diff --git a/images/gitea/src/util.rs b/images/gitea/src/util.rs new file mode 100644 index 0000000..825c978 --- /dev/null +++ b/images/gitea/src/util.rs @@ -0,0 +1,19 @@ +use color_eyre::Result; +use reqwest::blocking::Client; +use reqwest::header::HeaderMap; + +use crate::USER_AGENT; + +pub(crate) fn make_client(access_token: impl AsRef) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + "Authorization", + format!("token {}", access_token.as_ref()).try_into()?, + ); + + Client::builder() + .default_headers(headers) + .user_agent(USER_AGENT) + .build() + .map_err(From::from) +}