4 Commits

Author SHA1 Message Date
ef46e79674 Improved desc. in README, explain why tool exists 2025-11-14 17:13:42 -06:00
c5c5598fb7 Mark v3.0.1 patch
All checks were successful
/ Compile and upload a release build (release) Successful in 54s
Barely anything has changed, but the package *is* different. v3 is from
months back and is missing information that Crates.io kinda wants.
2025-11-13 16:55:12 -06:00
9e47cb72d5 Remove the comments tracking Debian-specific deps
The Debian 12 dependency versions can go away since I'm no longer
targetting it. The Debian Sid versions haven't been checked in months,
and I'm not actually targetting it *either* (never have been). They can
both go away.
2025-11-13 16:40:01 -06:00
ff2286f44b Add metadata required for publishing to crates.io
I'm not sure they're required. I sure hope not because I don't have a
homepage, and the guide says not to reuse the repo URL there.
2025-11-13 16:36:54 -06:00
8 changed files with 14 additions and 236 deletions

View File

@@ -1,7 +1,12 @@
[package]
name = "gt-tool"
version = "4.0.0-dev"
version = "3.0.1"
edition = "2024"
license = "GPL-3.0-only"
description = "CLI tools for interacting with the Gitea API. Mainly for attaching files to releases."
# homepage = "" I have no website for a project home page :(
repository = "https://git.gelvin.dev/robert/gt-tool"
readme = "README.md"
[dependencies]
clap = { version = "4.5.23", features = ["derive", "env"] }
@@ -11,13 +16,3 @@ reqwest = { version = "0.12.15", features = ["json", "stream", "multipart"] }
serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] }
toml = "0.8.19"
# Packages available in Debian (Sid)
# clap = "4.5.23"
# reqwest = "0.12.15"
# tokio = "1.43.1"
# Debian (Bookworm)
# clap = "4.0.32"
# reqwest = "0.11.13"
# tokio = "1.24.2"

View File

@@ -1,6 +1,12 @@
# gt-tool
CLI tools for interacting with the Gitea API. Use interactively to talk to your Gitea instance, or automatically via a CI/CD pipeline.
A CLI tool for interacting with the Gitea API. List existing releases, create new ones, and attach release-items (files) to them. It can be used interactively as one publishes their work, or automatically as part of a CI/CD pipeline.
## Why this thing exists
This program exists because I find GitHub Actions to be an insufficient system. There is no official "release-attachment" workflow. I have to either make my own or find someone else's. Since there is no standard Workflow for this task, the [Gitea version](https://gitea.com/actions/gitea-release-action) has a different API. Since I can only use branches, tags, or exact commit hashes, my dependency resolution is often manual or happens in surprising ways (e.g.: force pushing a new version tag). Adjacent to this is the issue that lots of dependencies are "assumed" and cannot be named -- like how the Ubuntu base image contains NodeJS, even if I never ask to install it. Finally, I can't easily execute or re-execute the workflows. There is no "run Action" button and I can't run any of it from my dev machine.
So I figured I'll make my own as a learning exercise and try to address as many of those problems as I can. The key is recognizing that a CI/CD platform needs a package manager. I like Debian, so I will stand on its package manager, although it might be better to pick NixOS or Gentoo. A GH Workflow which depends on this tool needs only to record it's name -- declarative build dependencies! This feature extends to the entire Debian container, automatically giving me full-system dependency knowledge. Updating the build environment is exactly like updating a normal Linux host because it *is* a normal Linux host. Lastly, it's also *a normal CLI program* so you can use it from your own dev workstation. There is no need for a "run Action" button.
## Usage
@@ -126,11 +132,3 @@ Similar to how unspecified project settings will fall back to those in the "`[al
| token | Gitea auth token, exactly the same as `$RELEASE_KEY_GITEA` |
Additional keys are quietly ignored. The config loading is done by querying a HashMap, so anything not touched doesn't get inspected. The only requirements are that the file is valid TOML, and that these keys are all strings.h
# Errata
- Gitea's package *uploading* API endpoints are not documented with Swagger.
* https://github.com/go-gitea/gitea/issues/30597
* It seems this is intentional because of which side defines the protocol (the consumer, not Gitea).
* This is an open issue (as of 12-Sep-2025) that doesn't have much attention.
* HTTP-POST (`curl`) is sufficient for upload. Use that either directly or as reference to make custom code.

View File

@@ -1,42 +1,4 @@
use std::{fs, path::Path};
use crate::structs::package::{Package, PackageType};
/// Gets all packages of an owner
///
/// https://github.com/go-gitea/gitea/blob/main/routers/api/v1/packages/package.go#L22
///
/// Matches the route `GET /packages/{owner}`
pub async fn list_packages(
client: &reqwest::Client,
gitea_url: &str,
owner: &str,
pkg_type: Option<PackageType>,
) -> crate::Result<Vec<Package>> {
let request_url = format!("{gitea_url}/api/v1/packages/{owner}");
// add the query parameter only when a package type filter has been set.
let request_url = if let Some(pkg_type) = pkg_type {
let pkg_type = pkg_type.to_string();
format!("{request_url}?type={pkg_type}")
} else {
request_url
};
let req = client.get(request_url).send().await;
let response = req.map_err(crate::Error::WrappedReqwestErr)?;
if response.status().is_success() {
let release_list = response
.json::<Vec<Package>>()
.await
.map_err(crate::Error::WrappedReqwestErr)?;
return Ok(release_list);
} else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg));
}
panic!("Reached the end of `api::list_packages()` without matching a return pathway.");
}
pub fn list_packages() {}
pub fn get_packages() {}
pub fn delete_package() {}
pub fn list_package_files() {}
@@ -44,73 +6,3 @@ pub fn get_latest_package_version() {}
pub fn link_package() {}
pub fn unlink_package() {}
pub fn search_packages() {}
/// Upload a Debian package and link it to it's source repository
fn upload_debian(
client: &reqwest::Client,
gitea_url: &str,
repo: &str,
file: &Path,
owner: &str,
distribution: &str,
component: &str,
) -> crate::Result<()> {
let request_url = format!("{gitea_url}/api/packages/{owner}/debian/pool/{distribution}/{component}/upload");
match file.try_exists() {
Ok(true) => (),
Ok(false) => return Err(crate::Error::NoSuchFile),
Err(e) => {
eprintln!("Uh oh! The file-exists check couldn't be done: {e}");
panic!(
"TODO: Deal with scenario where the file's existence cannot be checked (e.g.: no permission)"
);
}
};
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
.file_name("deb-pkg")
.mime_str("text/plain")?;
Ok(())
}
/// Uploads a package file to the Gitea registry.
///
/// This route is not documented in Swagger. The reason seems to be that some
/// package managers have their own upload protocol (e.g. `docker push`). Gitea
/// implements it but does not define it, so it doesn't include the API docs.
pub fn upload_package(
client: &reqwest::Client,
gitea_url: &str,
file: String, // TODO: Use a path buffer of some flavor.
) {
let request_url = format!("{gitea_url}/{}", route_for_upload(PackageType::Debian));
}
fn route_for_upload(kind: PackageType) -> &'static str {
match kind {
PackageType::Alpine => "https://gitea.example.com/api/packages/{owner}/alpine/{branch}/{repository}",
PackageType::Arch => "api/packages/{owner}/arch/{repository}",
PackageType::Cargo => todo!(),
PackageType::Chef => todo!(),
PackageType::Composer => todo!(),
PackageType::Conan => todo!(),
PackageType::Conda => todo!(),
PackageType::Container => todo!(),
PackageType::Cran => todo!(),
PackageType::Debian => "https://gitea.example.com/api/packages/{owner}/debian/pool/{distribution}/{component}/upload",
PackageType::Generic => todo!(),
PackageType::Go => todo!(),
PackageType::Helm => todo!(),
PackageType::Maven => todo!(),
PackageType::Npm => todo!(),
PackageType::Nuget => todo!(),
PackageType::Pub => todo!(),
PackageType::PyPi => todo!(),
PackageType::Rpm => todo!(),
PackageType::RubyGems => todo!(),
PackageType::Swift => todo!(),
PackageType::Vagrant => todo!(),
}
}

View File

@@ -44,5 +44,4 @@ pub enum Commands {
#[arg()]
files: Vec<String>,
},
ListPackages,
}

View File

@@ -134,16 +134,6 @@ async fn main() -> Result<(), gt_tool::Error> {
return Err(gt_tool::Error::NoSuchRelease);
}
}
gt_tool::cli::Commands::ListPackages => {
let packages = gt_tool::api::packages::list_packages(&client, &gitea_url, &owner, None).await?;
itertools::Itertools::intersperse(
packages
.iter()
.rev()
.map(|pkg| pkg.colorized()),
String::from("")
).for_each(|package| println!("{package}"));
}
}
Ok(())

View File

@@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize};
pub mod package;
pub mod release;
pub mod repo;

View File

@@ -1,94 +0,0 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use crate::structs::release::Author;
/// Represents the package as described by JSON
///
/// https://github.com/go-gitea/gitea/blob/main/modules/structs/package.go
#[derive(Debug, Deserialize, Serialize)]
pub struct Package {
created_at: String, // TODO: Datetime struct
creator: Author,
html_url: String,
id: u64,
name: String,
owner: Author,
#[serde(skip)]
repository: (), // TODO: Create a `struct Repository`
#[serde(rename = "type")] // field is "type" in JSON & Go, but that's a
pkg_type: PackageType, // keyword in Rust so we need to serde-rename it.
version: String,
}
impl Package {
pub fn colorized(&self) -> String {
let name = "Name:".green().bold();
let version = &self.version;
let pkg_type = "Type:".green();
format!(
"{name} {} {version}
{pkg_type} {}",
self.name,
self.version,
)
}
}
/// A marker for the kind of package being handled.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PackageType {
Alpine,
Arch,
Cargo,
Chef,
Composer,
Conan,
Conda,
Container,
Cran,
Debian,
Generic,
Go,
Helm,
Maven,
Npm,
Nuget,
Pub,
PyPi,
Rpm,
RubyGems,
Swift,
Vagrant,
}
impl ToString for PackageType {
fn to_string(&self) -> String {
match self {
PackageType::Alpine => "alpine".into(),
PackageType::Arch => "arch".into(),
PackageType::Cargo => "cargo".into(),
PackageType::Chef => "chef".into(),
PackageType::Composer => "composer".into(),
PackageType::Conan => "conan".into(),
PackageType::Conda => "conda".into(),
PackageType::Container => "container".into(),
PackageType::Cran => "cran".into(),
PackageType::Debian => "debian".into(),
PackageType::Generic => "generic".into(),
PackageType::Go => "go".into(),
PackageType::Helm => "helm".into(),
PackageType::Maven => "maven".into(),
PackageType::Npm => "npm".into(),
PackageType::Nuget => "nuget".into(),
PackageType::Pub => "pub".into(),
PackageType::PyPi => "pypi".into(),
PackageType::Rpm => "rpm".into(),
PackageType::RubyGems => "rubygems".into(),
PackageType::Swift => "swift".into(),
PackageType::Vagrant => "vagrant".into(),
}
}
}

View File

@@ -50,7 +50,6 @@ impl Release {
}
}
// TODO: Rename to "User" to match Gitea's code
#[derive(Debug, Deserialize, Serialize)]
pub struct Author {
id: usize,