feat(hive-forge): add artifact-get verb to download CI run artifacts
Forgejo 15 has no REST endpoint to download an Actions artifact — the only path is the web UI download route, which is keyed by the run's global id rather than the per-repo run number shown in run-page URLs. The REST artifacts list route keys off the run number instead, so the two can't be chained directly. artifact-get takes the run number (what pr-status surfaces as a CI context target_url), translates it to the global run id via the REST runs list by matching each run's html_url tail, then GETs the web download route with the agent's forge token. Saves the artifact zip to a path (default /tmp/forge-artifact-<name>.zip) or streams to stdout with -o -. The artifact name is percent-encoded into the path. The encoder that list already used for query-string filters is promoted to a shared verbs::pct_encode helper so both call sites stay in sync. Lets an agent pull a CI-built artifact (e.g. a paper PDF) into /shared without host access.
This commit is contained in:
parent
801b886a4c
commit
6490fc422e
6 changed files with 184 additions and 41 deletions
|
|
@ -99,23 +99,23 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
if let Some(u) = args.assignee.as_deref()
|
||||
&& !u.is_empty()
|
||||
{
|
||||
write!(path, "&assigned_by={}", pct_encode(u)).unwrap();
|
||||
write!(path, "&assigned_by={}", super::pct_encode(u)).unwrap();
|
||||
}
|
||||
if let Some(u) = args.author.as_deref()
|
||||
&& !u.is_empty()
|
||||
{
|
||||
write!(path, "&created_by={}", pct_encode(u)).unwrap();
|
||||
write!(path, "&created_by={}", super::pct_encode(u)).unwrap();
|
||||
}
|
||||
if let Some(u) = args.mention.as_deref()
|
||||
&& !u.is_empty()
|
||||
{
|
||||
write!(path, "&mentioned_by={}", pct_encode(u)).unwrap();
|
||||
write!(path, "&mentioned_by={}", super::pct_encode(u)).unwrap();
|
||||
}
|
||||
if !args.labels.is_empty() {
|
||||
// Encode each label individually so a comma INSIDE a label
|
||||
// (rare but legal) gets escaped while the field separator
|
||||
// stays a literal comma the forge will parse as N labels.
|
||||
let encoded: Vec<String> = args.labels.iter().map(|l| pct_encode(l)).collect();
|
||||
let encoded: Vec<String> = args.labels.iter().map(|l| super::pct_encode(l)).collect();
|
||||
write!(path, "&labels={}", encoded.join(",")).unwrap();
|
||||
}
|
||||
let resp = client.get_json(&path)?;
|
||||
|
|
@ -134,23 +134,6 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Minimal RFC 3986 unreserved-set percent encoder. Covers the
|
||||
/// subset of characters that show up in usernames + label names
|
||||
/// (spaces in labels are the realistic non-ASCII case) without
|
||||
/// pulling in a fresh workspace dep. Username regex is gitea-style
|
||||
/// `[a-zA-Z0-9_-]` so the no-op fast path covers all of them.
|
||||
fn pct_encode(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
|
||||
out.push(b as char);
|
||||
} else {
|
||||
write!(out, "%{b:02X}").unwrap();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Render one issue/PR as a single `#NNN [author] title` line.
|
||||
/// Defensive: missing fields drop to placeholders so a partial
|
||||
/// response from a future API change still produces readable output
|
||||
|
|
@ -192,24 +175,4 @@ mod tests {
|
|||
assert_eq!(State::Closed.api_value(), "closed");
|
||||
assert_eq!(State::All.api_value(), "all");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pct_encode_passes_unreserved_through() {
|
||||
// Usernames + plain label names round-trip verbatim — no
|
||||
// performance regression on the common case.
|
||||
assert_eq!(pct_encode("damocles"), "damocles");
|
||||
assert_eq!(pct_encode("area-ops"), "area-ops");
|
||||
assert_eq!(pct_encode("area_ops"), "area_ops");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pct_encode_escapes_spaces_and_specials() {
|
||||
// Forgejo labels can contain spaces ("good first issue" is a
|
||||
// canonical example); the comma separator gets encoded too
|
||||
// when it appears INSIDE a label name (we re-add the joined
|
||||
// form unencoded above as the field separator).
|
||||
assert_eq!(pct_encode("good first issue"), "good%20first%20issue");
|
||||
assert_eq!(pct_encode("a&b"), "a%26b");
|
||||
assert_eq!(pct_encode("a/b"), "a%2Fb");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue