1
Fork 0
ci-images/images/gitea/src/cmd/package.rs
Lucas Schwiderski 10e2740053
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.
2024-08-27 16:52:07 +02:00

286 lines
7.7 KiB
Rust

use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use cli_table::format::Justify;
use cli_table::{Cell, Style, Table};
use color_eyre::eyre::{self, Context};
use color_eyre::{Help, Report, Result};
use globwalk::{DirEntry, GlobWalkerBuilder};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use time::format_description::well_known::Iso8601;
use url::Url;
use crate::types::{Package, PackageType};
use crate::util::make_client;
use crate::Action;
#[derive(Deserialize, Serialize, Debug)]
struct Source {
access_token: String,
owner: String,
url: Url,
r#type: PackageType,
name: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct Version {
version: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct Params {
version: String,
globs: Vec<String>,
#[serde(default)]
fail_fast: bool,
#[serde(default)]
r#override: bool,
}
#[derive(Deserialize, Serialize, Debug)]
struct Config {
/// Resource configuration.
/// Passed verbatim from the definition in the pipeline.
source: Source,
/// For 'check':
/// Previous version of this resource.
/// Will be empty for the very first request.
/// For 'in':
/// The version metadata to create this as.
version: Option<Version>,
params: Option<Params>,
}
fn action_check(conf: Config) -> Result<()> {
let client = make_client(&conf.source.access_token)?;
let url = conf
.source
.url
.join(&format!("api/v1/packages/{}", conf.source.owner))
.wrap_err("Invalid URL")?;
let mut pkgs: Vec<Package> = client
.get(url)
.query(&[
("type", conf.source.r#type.to_string()),
("q", conf.source.name),
])
.send()?
.json()?;
if pkgs.is_empty() {
return io::stdout().write_all(b"[]").map_err(From::from);
}
pkgs.sort_unstable_by_key(|pkg| pkg.created_at);
let table: Vec<_> = pkgs
.iter()
.map(|pkg| {
vec![
pkg.id.cell().justify(Justify::Center),
pkg.name.clone().cell(),
pkg.owner.login.clone().cell(),
pkg.version.clone().cell(),
pkg.created_at
.format(&Iso8601::DEFAULT)
.unwrap_or_default()
.cell(),
]
})
.collect();
let table = table
.table()
.title(vec![
"#".cell().bold(true),
"Name".cell().bold(true),
"Owner".cell().bold(true),
"Version".cell().bold(true),
"Created At".cell().bold(true),
])
.bold(true);
let _ = cli_table::print_stderr(table);
let newest = pkgs
.last()
.expect("List of packages should not be empty.")
.version
.clone();
let out = if let Some(prev) = conf.version {
if prev.version == newest {
vec![prev]
} else {
vec![prev, Version { version: newest }]
}
} else {
vec![Version { version: newest }]
};
serde_json::to_writer_pretty(io::stdout(), &out)
.wrap_err("Failed to write result to stdout")?;
Ok(())
}
fn action_out(conf: Config, dir: impl AsRef<Path>) -> Result<()> {
let dir = dir
.as_ref()
.canonicalize()
.wrap_err_with(|| format!("Invalid file path '{}'", dir.as_ref().display()))?;
let Some(params) = conf.params else {
eyre::bail!("Missing params");
};
if params.globs.is_empty() {
eyre::bail!("`params.globs` must not be empty");
}
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/packages/{}/{}/{}/{}/",
conf.source.owner, conf.source.r#type, conf.source.name, params.version
))
.wrap_err("Invalid URL")?;
let walker = GlobWalkerBuilder::from_patterns(dir, &params.globs)
.max_depth(3)
.file_type(globwalk::FileType::FILE)
.build()
.wrap_err("Failed to create glob walker")?;
let handle_glob = |entry| {
let entry: DirEntry = entry?;
let name = entry.file_name();
let path = entry.path();
let url = url.join(&name.to_string_lossy()).wrap_err("Invalid url")?;
if params.r#override {
client
.delete(url.clone())
.send()
.wrap_err_with(|| format!("Failed request 'DELETE {}'", &url))?;
}
let data = fs::read(path)
.wrap_err_with(|| format!("Failed to read package file '{}'", path.display()))?;
let res = client
.put(url.clone())
.body(data)
.send()
.wrap_err_with(|| format!("Failed request 'PUT {}'", &url))?;
match res.status() {
StatusCode::CREATED => {
eprintln!(
"Uploaded file '{}', version={}, package={}",
name.to_string_lossy(),
params.version,
conf.source.name
);
}
StatusCode::BAD_REQUEST => {
eyre::bail!(
"Package, version or file name are invalid. package={}, version={}, file={}",
conf.source.name,
params.version,
name.to_string_lossy(),
);
}
StatusCode::CONFLICT => {
eyre::bail!(
"File '{}' already exists. version={}, package={}",
name.to_string_lossy(),
params.version,
conf.source.name
);
}
StatusCode::INTERNAL_SERVER_ERROR => {
eyre::bail!(
"Internal server error: {} {}",
res.status(),
res.text().unwrap_or_default()
);
}
code => {
eyre::bail!("Unexpected status code {}\ntext = {:?}", code, res.text());
}
}
Ok::<_, Report>(())
};
let mut has_entry = false;
let mut errors = Vec::new();
for entry in walker {
has_entry = true;
if let Err(err) = handle_glob(entry) {
errors.push(err);
if params.fail_fast {
break;
}
}
}
if !errors.is_empty() {
let mut err = eyre::eyre!("Failed to upload package with {} errors", errors.len());
for e in errors {
err = err.report(e);
}
return Err(err);
}
if !has_entry {
eyre::bail!("Globs didn't produce any files");
}
let out = json!({
"version": {
"version": params.version
}
});
serde_json::to_writer_pretty(io::stdout(), &out)
.wrap_err("Failed to write result to stdout")?;
Ok(())
}
pub(crate) fn run(action: &Action) -> Result<()> {
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")?
};
match action {
Action::Check => action_check(config),
Action::In { dest: _ } => Ok(()),
Action::Out { src } => action_out(config, src),
}
}