27 Commits

Author SHA1 Message Date
4553f60150 Drop the on-push-branch trigger, prep for full test 2025-07-02 15:17:33 -05:00
077ad67c99 The workdir worked! Add it to the gbp call, too 2025-07-02 15:17:33 -05:00
41277146a0 Always install build-deps, don't ask for user input 2025-07-02 15:17:33 -05:00
6c0578e466 Set workdir on the git-checkout step. Stupid 2025-07-02 15:17:33 -05:00
15d5f9872c Try relative pathing to get into the project dir 2025-07-02 15:17:33 -05:00
660ca7353d Fix: remove extra indentation in the run string
I think YAML is leaving the spaces in there and passing `  build-essential` to the shell. There is no package with a space in the name, especially not a leading space.
2025-07-02 15:17:33 -05:00
67e5e4de0f Add experimental Debian package autobuild 2025-07-02 15:17:33 -05:00
bbae6b4395 Update changelog for 2.1.0-1 release 2025-06-12 17:51:23 -05:00
2c03c5ba4d Merge tag 'v2.1.0' into deb/bookworm
Hotfix for Attachment decode error
2025-06-12 17:48:09 -05:00
136c051c82 Fix: incorrect field names for Attachment
All checks were successful
/ Compile and upload a release build (release) Successful in 37s
I think I got the names from the Go source code, but the API emits JSON
that has these names instead. The api/swagger guide even says as much.

This caused the super fun quirk that the upload actually succeedes, but
the program reports an error condition because of the deserialization
failure. Time to bump a minor revision!
2025-06-12 17:21:48 -05:00
6a9ec25d1a Update changelog for 2.0.0-1 release 2025-06-12 16:28:43 -05:00
395ce8a804 Rediff patches
Drop 0002-Use-pre-Rust-1.81-compatible-file-exists-test.patch: <REASON>
2025-06-12 16:24:48 -05:00
616be020f0 Merge tag 'v2.0.0' into deb/bookworm
Mark release for v2

Now with better error diagnostics!... better... They're still not great.
2025-06-12 16:22:33 -05:00
a0ba8e7ea8 Use pre Rust 1.81 compatible file-exists test
The function `std::fs::exists(...)` was stabilized in Rust 1.81, which
means it can't be used in the Debian Bookworm build. This patch swaps to
a compatible implementation leaning on the std::path::Path struct.

I'm both "upstreaming" a Debian-specific patch I had to make for the
package, and fixing the additional usage now in `main.rs`. There doesn't
seem to be any compelling reason to avoid using this function, so I
figure I should merge it into the base release.
2025-06-12 16:05:51 -05:00
324c7e67a7 Re-assign v1.0.0-1 to 'unstable'
I should have done this before publishing the build, but it's too late
now. Instead, I'll update it here and continue forward with gbp-dch to
help generate more correct changelogs.
2025-06-12 12:55:16 -05:00
984974c240 Update debian/gbp.conf to use new branch name
I'm switching to use nested tags for the packaging branches. I want to
have a deb/bookworm and a deb/experimental branch. The gbp-pq tool
already seems to do this, so I'll follow it's lead.
2025-06-12 12:50:48 -05:00
88cafc096f Drop unused import in api/release.rs 2025-06-08 10:43:32 -05:00
b200785e71 ... and the unit testing notes in README.md 2025-06-08 10:40:54 -05:00
8246337ae4 Delete the unit tests
They aren't useful anyway.
2025-06-08 00:06:59 -05:00
06795df3f7 Update main.rs to use new attachment iface 2025-06-07 23:57:19 -05:00
a5f6335b5f Add Attachment struct, new iface for create-rel
The Attachment struct exists, but this makes it glaringly obvious that
I've made a bad interface. The create_release_attachment should only
accept one file at a time and the loop over all files should happen in
main.rs

I've changed the signature, removed the loops, and wired in the newer
error handling routines. Needs fixing at call sites.
2025-06-07 23:50:16 -05:00
4a0addda67 Fold client-error-decode into a util function 2025-06-07 23:40:58 -05:00
0c70b584ba Interrogate create_release_attachment result 2025-06-07 23:30:56 -05:00
8a11c21b73 "Fix" the test case
I can't meaningfully unit test these things like this. I'll explore creating a tarball of a known Gitea configuration and using Docker to test against that. For now, just... kinda keep the test building.
2025-06-07 23:24:16 -05:00
d42cbbc1ec Drop unused imports 2025-06-07 23:22:48 -05:00
96e9ff4ce6 Interrogate create_release result more closely 2025-06-07 23:22:20 -05:00
6bdad44cc6 Interrogate list_releases result more closely 2025-06-07 23:15:39 -05:00
11 changed files with 168 additions and 250 deletions

View File

@@ -23,5 +23,39 @@ jobs:
target/release/gt-tool-${{ github.ref_name }}-$(arch)
env:
RELEASE_KEY_GITEA: ${{ secrets.RELEASE_KEY_GITEA }}
debian-release:
name: Build and upload the Debian 12 package
runs-on: ubuntu-latest
container:
image: debian:12
steps:
- name: Install Tools
run: >
apt-get update;
apt-get install -y --no-install-recommends
build-essential
git
git-buildpackage;
- name: Checkout Repo (can't use actions/checkout@v4, no NodeJS)
run: git clone ${{ github.event.repository.clone_url }}
- name: Switch to Debian package branch
run: git checkout deb
working-directory: ./gt-tool
- name: Install build-deps
run: apt-get build-dep -y .
working-directory: ./gt-tool
- name: Build the package
run: gbp buildpackage
working-directory: ./gt-tool
- name: Install the tool we just built
run: dpkg -i gt-tool*.deb # TODO: Pick out the exact version instead of globbing
- name: Upload the packaging parts
run: > # The file globs are like that to avoid matching the gt-tool/ folder. I don't want that uploaded.
gt-tool
-u ${{ vars.DEST_GITEA }} -r ${{ vars.DEST_REPO }}
upload-release
"${{ github.ref_name }}"
gt-tool-*
gt-tool_*
...

View File

@@ -51,14 +51,3 @@ One of these, defaults to `help`:
| upload-release | Uploads one-or-more files to an existing release, identified by it's tag name. |
| help | prints the help text (the usage summary above). |
## Unit Testing
The unit test~~s~~ require a Gitea server to execute against. This information is supplied by environment variables rather than on the command line, but it is otherwise exactly the same usage.
| Variable | Description |
|-|-|
| TEST_GITEA_SERVER | Server URL, match `-u`, `--url` |
| TEST_GITEA_REPO | Owner + repo name, match `-u` `--repo` |
| TEST_GITEA_KEY | API key, match `RELEASE_KEY_GITEA`. The use of a new variable for the API token is to help avoid accidentally touching a production environment during test execution. |
| TEST_GITEA_RELEASE_TAG | Git tag used to identify the Release. Same as `upload-release`'s positional argument `<TAG_NAME>`. |

26
debian/changelog vendored
View File

@@ -1,4 +1,28 @@
gt-tool (1.0.0-1) UNRELEASED; urgency=low
gt-tool (2.1.0-1) unstable; urgency=medium
* Fix: incorrect field names for `Attachment`
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 12 Jun 2025 17:51:12 -0500
gt-tool (2.0.0-1) unstable; urgency=medium
* Interrogate list_releases result more closely
* Interrogate create_release result more closely
* Drop unused imports
* "Fix" the test case
* Interrogate create_release_attachment result
* Fold client-error-decode into a util function
* Add `Attachment` struct, new iface for create-rel
* Update main.rs to use new attachment iface
* Delete the unit tests
* ... and the unit testing notes in README.md
* Drop unused import in api/release.rs
* Use pre Rust 1.81 compatible file-exists test
* Rediff patches
-- Robert Garrett <robertgarrett404@gmail.com> Thu, 12 Jun 2025 16:28:18 -0500
gt-tool (1.0.0-1) unstable; urgency=low
* Experimental release.

2
debian/gbp.conf vendored
View File

@@ -2,5 +2,5 @@
compression = xz
compression-level = 9
upstream-tag = v%(version)s
debian-branch = deb
debian-branch = deb/bookworm

View File

@@ -1,31 +0,0 @@
From: Robert Garrett <robertgarrett404@gmail.com>
Date: Sun, 1 Jun 2025 18:01:21 -0500
Subject: Use pre Rust 1.81 compatible file-exists test
The function `std::fs::exists(...)` was stabilized in Rust 1.81, which
means it can't be used in the Debian Bookworm build. This patch swaps to
a compatible implementation leaning on the std::path::Path struct.
---
src/api/release_attachment.rs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/api/release_attachment.rs b/src/api/release_attachment.rs
index 9af71bf..8a0c798 100644
--- a/src/api/release_attachment.rs
+++ b/src/api/release_attachment.rs
@@ -1,4 +1,4 @@
-use std::fs;
+use std::{fs, path};
pub fn check_release_match_repo() {}
pub fn get_release_attachment() {}
@@ -16,7 +16,8 @@ pub async fn create_release_attachment(
// Ensure all files exists before starting the uploads
for file in &files {
- match fs::exists(file) {
+ let path = path::Path::new(file);
+ match path.try_exists() {
Ok(true) => continue,
Ok(false) => return Err(crate::Error::NoSuchFile),
Err(e) => {

View File

@@ -1,2 +1 @@
0001-Rust-edition-downgrade-to-2021.patch
0002-Use-pre-Rust-1.81-compatible-file-exists-test.patch

View File

@@ -1,9 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::{
ApiError, Result,
Result,
structs::{
self,
release::{CreateReleaseOption, Release},
},
};
@@ -23,6 +21,7 @@ pub async fn list_releases(
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/");
let req = client.get(request_url).send().await;
let response = req.map_err(|reqwest_err| crate::Error::WrappedReqwestErr(reqwest_err))?;
if response.status().is_success() {
let release_list = response
.json::<Vec<Release>>()
.await
@@ -32,14 +31,11 @@ pub async fn list_releases(
crate::Error::WrappedReqwestErr(reqwest_err)
})?;
return Ok(release_list);
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
enum CreateResult {
Success(structs::release::Release),
ErrWithMessage(ApiError),
Empty,
} else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg));
}
panic!("Reached end of list_releases without matching a return pathway.");
}
pub async fn create_release(
@@ -49,27 +45,23 @@ pub async fn create_release(
submission: CreateReleaseOption,
) -> Result<Release> {
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases");
let req = client
let response = client
.post(request_url)
.json(&submission)
.send()
.await
.map_err(|e| crate::Error::from(e))?;
let new_release = req
.json::<CreateResult>()
if response.status().is_success() {
let new_release = response
.json::<Release>()
.await
.map_err(|e| crate::Error::from(e))?;
match new_release {
CreateResult::Success(release) => Ok(release),
CreateResult::ErrWithMessage(api_error) => {
if api_error.message == "token is required" {
Err(crate::Error::MissingAuthToken)
} else {
Err(crate::Error::ApiErrorMessage(api_error))
}
}
CreateResult::Empty => panic!("How can we have 200 OK and no release info? No. Crash"),
return Ok(new_release);
} else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg))
}
panic!("Reached end of create_release without matching a return path");
}
pub fn edit_release(id: u64) -> Result<Release> {
todo!();

View File

@@ -1,4 +1,6 @@
use std::fs;
use std::{fs, path};
use crate::structs::Attachment;
pub fn check_release_match_repo() {}
pub fn get_release_attachment() {}
@@ -10,23 +12,20 @@ pub async fn create_release_attachment(
gitea_url: &str,
repo: &str,
release_id: usize,
files: Vec<String>,
) -> crate::Result<()> {
file: String,
) -> crate::Result<Attachment> {
let request_url = format!("{gitea_url}/api/v1/repos/{repo}/releases/{release_id}/assets");
// Ensure all files exists before starting the uploads
for file in &files {
match fs::exists(file) {
Ok(true) => continue,
let path = path::Path::new(&file);
match path.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)");
},
}
}
for file in files {
println!("Uploading file {}", &file);
let data = reqwest::multipart::Part::stream(fs::read(&file).unwrap())
.file_name("attachment")
@@ -34,150 +33,24 @@ pub async fn create_release_attachment(
let form = reqwest::multipart::Form::new().part("attachment", data);
let request = client
let response = client
.post(&request_url)
.multipart(form)
.query(&[("name", file.split("/").last())])
.send()
.await?;
if response.status().is_success() {
// TODO: create a struct Attachment and return it to the caller.
let attachment_desc = response
.json::<Attachment>()
.await
.map_err(|e| crate::Error::from(e))?;
return Ok(attachment_desc);
} else if response.status().is_client_error() {
let mesg = crate::decode_client_error(response).await?;
return Err(crate::Error::ApiErrorMessage(mesg));
}
Ok(())
panic!("Reached end of release_attachment without matching a return path");
}
pub fn edit_release_attachment() {}
pub fn delete_release_attachment() {}
#[cfg(test)]
mod tests {
use reqwest::header::{self, ACCEPT};
use crate::structs::release::Release;
#[tokio::test]
async fn attach_file_exists() {
let conf = TestConfig::new();
let release_candidates =
crate::api::release::list_releases(
&conf.client,
&conf.server,
&conf.repo
)
.await
.expect("Failed to get releases. Pre-conditions unmet, aborting test!");
let release = match_release_by_tag(&conf.release_tag, release_candidates)
.expect("Failed to select matching release. Pre-conditions unmet, aborting test!");
let api_result = super::create_release_attachment(
&conf.client,
&conf.server,
&conf.repo,
release.id,
vec![String::from("Cargo.toml")],
)
.await;
}
#[tokio::test]
async fn attach_file_missing() {
let conf = TestConfig::new();
let release_candidates =
crate::api::release::list_releases(
&conf.client,
&conf.server,
&conf.repo
)
.await
.expect("Failed to get releases. Pre-conditions unmet, aborting test!");
let release = match_release_by_tag(&conf.release_tag, release_candidates)
.expect("Failed to select matching release. Pre-conditions unmet, aborting test!");
let api_result = super::create_release_attachment(
&conf.client,
&conf.server,
&conf.repo,
release.id,
vec![String::from("./this-file-doesnt-exist")],
)
.await;
let api_err = api_result.expect_err("Received Ok(()) after uploading non-existent file. That's nonsense, the API Function is wrong.");
match api_err {
crate::Error::Placeholder => panic!("Received dummy response from the API function. Finish implementing it, stupid"),
crate::Error::WrappedReqwestErr(error) => panic!("Received a reqwest::Error from the API function: {error}"),
crate::Error::MissingAuthToken => unreachable!("Missing auth token... in a unit test that already panics without the auth token..."),
crate::Error::NoSuchFile => (), // test passes
crate::Error::ApiErrorMessage(api_error) => panic!("Received an error message from the API: {api_error:?}"),
}
}
struct TestConfig {
server: String,
repo: String,
release_tag: String,
client: reqwest::Client,
}
impl TestConfig {
fn new() -> Self {
let server = std::env::var("TEST_GITEA_SERVER")
.expect("Must set server address in env var \"TEST_GITEA_SERVER\"");
let repo = std::env::var("TEST_GITEA_REPO")
.expect("Must set <user>/<repo> name in env var \"TEST_GITEA_REPO\"");
let token = format!(
"token {}",
std::env::var("TEST_GITEA_KEY")
.expect("Must set the API token in env var \"TEST_GITEA_KEY\"")
);
let release_tag = std::env::var("TEST_GITEA_RELEASE_TAG")
.expect("Must set the target release tag in env var \"TEST_GITEA_RELEASE_TAG\"");
let mut headers = reqwest::header::HeaderMap::new();
headers.append(ACCEPT, header::HeaderValue::from_static("application/json"));
headers.append("Authorization", token.parse().unwrap());
let client = reqwest::Client::builder()
.user_agent(format!(
"gt-tools-autotest-agent{}",
env!("CARGO_PKG_VERSION")
))
.default_headers(headers)
.build()
.expect("Failed to build reqwest::Client.");
return Self {
server,
repo,
release_tag,
client
};
}
}
// Testing utils
fn match_release_by_tag(tag: &String, releases: Vec<Release>) -> Option<Release> {
let mut release: Option<Release> = None;
for rel in releases {
if rel.tag_name == *tag {
// Only store the value if one hasn't been stored already
if let Some(first_release) = &release {
// if there was already a match, begin the error diagnostic creation.
let first_id = first_release.id;
let second_id = rel.id;
assert!(
first_id != second_id,
"FAILURE: Found the same release ID twice while scanning for duplicate tags. How did we get the same one twice?"
);
eprintln!("ERROR: Two releases have been found for the tag \"{tag}\".");
eprintln!("ERROR: first ID: {first_id}");
eprintln!("ERROR: second ID: {second_id}");
panic!("ERROR: Nonsense detected, I'm bailing out!");
} else {
// else, store our first (and hopefully only) match
release = Some(rel);
}
}
}
return release;
}
}

View File

@@ -10,6 +10,15 @@ pub struct ApiError {
url: String,
}
pub (crate) async fn decode_client_error(response: reqwest::Response) -> Result<ApiError> {
response
.json::<ApiError>()
.await
.map_err(|reqwest_err| {
crate::Error::WrappedReqwestErr(reqwest_err)
})
}
#[derive(Debug)]
pub enum Error {
Placeholder, // TODO: Enumerate error modes

View File

@@ -1,3 +1,6 @@
use std::path;
use gt_tool::cli::Args;
use gt_tool::structs::release::{CreateReleaseOption, Release};
@@ -78,14 +81,27 @@ async fn main() -> Result<(), gt_tool::Error> {
gt_tool::api::release::list_releases(&client, &args.gitea_url, &args.repo).await?;
if let Some(release) = match_release_by_tag(&tag_name, release_candidates) {
gt_tool::api::release_attachment::create_release_attachment(
for file in &files {
let path = path::Path::new(&file);
match path.try_exists() {
Ok(true) => continue,
Ok(false) => return Err(gt_tool::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)");
},
}
}
for file in files {
let _attach_desc = gt_tool::api::release_attachment::create_release_attachment(
&client,
&args.gitea_url,
&args.repo,
release.id,
files,
file,
)
.await?;
}
} else {
println!("ERR: Couldn't find a release matching the tag \"{tag_name}\".");
return Err(gt_tool::Error::NoSuchRelease);

View File

@@ -1,2 +1,15 @@
use serde::{Deserialize, Serialize};
pub mod release;
pub mod repo;
#[derive(Debug, Deserialize, Serialize)]
pub struct Attachment {
id: usize,
name: String,
size: i64,
download_count: i64,
created_at: String, // TODO: Date-time struct
uuid: String,
browser_download_url: String,
}