Skip to content

Commit

Permalink
Added set_output method (#8)
Browse files Browse the repository at this point in the history
- Added method to produce an
[output](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)
- Copied from
[here](https://github.com/actions/toolkit/blob/d1df13e178816d69d96bdc5c753b36a66ad03728/packages/core/src/core.ts#L192)

## Summary

- **Documentation**
- Updated `README.md` to mark the `set_output` method as complete and
included an example of its usage.

- **Bug Fixes**
- Enhanced error handling with a new `Output` variant in the
`ActionsError` enum.

- **Chores**
  - Added `uuid` dependency with `v4` feature for better UUID handling.
  • Loading branch information
Bullrich authored May 27, 2024
1 parent 39b2a6d commit 080cc5c
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 5 deletions.
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ doctest = false

[dependencies]
json = "0.12.4"
uuid = { version = "1.8.0", features = ["v4"] }
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ A rust translation of [@actions/github](https://www.npmjs.com/package/@actions/g

Find the [documentation here](https://docs.rs/actions-github).

[![latest version](https://img.shields.io/crates/v/actions-github)](https://crates.io/crates/actions-github)
![Crates.io Total Downloads](https://img.shields.io/crates/d/actions-github)

**Work in progress**: This library is being developed.

## Work in progress

- [x] Context object
- [x] get_input method
- [ ] set_output method
- [x] set_output method
- [ ] logging methods

## Installation
Expand All @@ -29,7 +32,7 @@ println!("Event is {}", data.event_name);
Works well with [`octocrab`](https://crates.io/crates/octocrab/):

```rust
use actions_github::core::get_input;
use actions_github::core::{get_input, set_output};
use actions_github::context::get_context;
use octocrab::Octocrab;

Expand All @@ -43,4 +46,7 @@ let org = context.repo.owner;
let repo = context.repo.repo;

let pulls = octocrab::instance().pulls(owner, repo).list()

// Output how many PRs are in the repository
set_output("PRs", pulls.len().to_string);
```
39 changes: 36 additions & 3 deletions src/core.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::env;
//! Utility methods to interact with the GitHub actions ecosystem
//!
//! You can obtain injected inputs or produce an output for another step
use std::io::Write;
use std::{env, io};

use crate::error::ActionsError;
use crate::util::{issue_file_command, issue_old_command, prepare_key_value_message, EOL};

// Implemented from https://github.com/actions/toolkit/blob/main/packages/core/src/core.ts#L126
/// Obtain an input from a variable
///
/// If the input is not found, an [ActionsError] is returned
Expand All @@ -19,9 +23,33 @@ pub fn get_input(name: &str) -> Result<String, ActionsError> {
}
}

/// Produces an [output](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)
/// that can be used in another step
///
/// ```rust
/// set_output("name", "value");
/// ```
pub fn set_output(name: &str, value: &str) -> Result<(), ActionsError> {
if env::var("GITHUB_OUTPUT").is_ok() {
return match prepare_key_value_message(name, value) {
Ok(key_value_message) => match issue_file_command("OUTPUT", key_value_message) {
Ok(_) => Ok(()),
Err(err) => Err(ActionsError::Output(err)),
},
Err(err) => Err(ActionsError::Output(err)),
};
}

io::stdout()
.write_all(EOL.as_bytes())
.expect("Failed to write EOL");
issue_old_command("set-output", name, value);
Ok(())
}

#[cfg(test)]
mod test {
use crate::core::get_input;
use crate::core::{get_input, set_output};
use std::env;

#[test]
Expand All @@ -36,4 +64,9 @@ mod test {
let input = get_input("test");
assert!(input.is_err())
}

#[test]
fn writes_output() {
assert!(set_output("hi", "bye").is_ok());
}
}
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub enum ActionsError {
Context(String),
/// The input was not found
InputNotFound(String),
/// There was a problem while writing the output
Output(String),
}

impl Error for ActionsError {}
Expand All @@ -20,6 +22,7 @@ impl Display for ActionsError {
match self {
Context(msg) => write!(f, "Problem while generating the context: {}", msg),
InputNotFound(input) => write!(f, "Input required and not supplied: {}", input),
Output(msg) => write!(f, "{}", msg),
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
pub mod context;
pub mod core;
pub mod error;
mod util;
71 changes: 71 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::fs;
use std::io::Write;
use std::path::Path;
use std::{env, io};
use uuid::Uuid;

#[cfg(windows)]
pub const EOL: &str = "\r\n";
#[cfg(not(windows))]
pub const EOL: &str = "\n";

pub fn issue_file_command(command: &str, message: String) -> Result<(), String> {
let env_var = format!("GITHUB_{}", command);
let file_path = match env::var(env_var) {
Ok(path) => path,
Err(_) => {
return Err(format!(
"Unable to find environment variable for file command {}",
command
))
}
};

if !Path::new(&file_path).exists() {
return Err(format!("Missing file at path: {}", file_path));
}

let mut file = match fs::OpenOptions::new().append(true).open(&file_path) {
Ok(f) => f,
Err(_) => return Err(format!("Unable to open file at path: {}", file_path)),
};

if writeln!(file, "{}{}", message, EOL).is_err() {
return Err(format!("Unable to write to file at path: {}", file_path));
}

Ok(())
}

pub fn prepare_key_value_message(key: &str, value: &str) -> Result<String, String> {
let delimiter = format!("ghadelimiter_{}", Uuid::new_v4());

// These should realistically never happen, but just in case someone finds a
// way to exploit uuid generation let's not allow keys or values that contain
// the delimiter.
if key.contains(&delimiter) {
return Err(format!(
"Unexpected input: name should not contain the delimiter \"{}\"",
&delimiter
));
}

if value.contains(&delimiter) {
return Err(format!(
"Unexpected input: value should not contain the delimiter \"{}\"",
&delimiter
));
}

Ok(format!(
"{}<<{}{}{}{}{}",
key, delimiter, EOL, value, EOL, delimiter
))
}

pub fn issue_old_command(command: &str, name: &str, value: &str) {
let msg: String = format!("::{} name={}::{}", command, name, value);
io::stdout()
.write_all((msg.to_string() + EOL).as_bytes())
.expect("Failed to write command")
}

0 comments on commit 080cc5c

Please sign in to comment.