Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flowey: use typestate pattern for gh variables #643

Open
wants to merge 33 commits into
base: main
Choose a base branch
from

Conversation

justus-camp-microsoft
Copy link
Contributor

@justus-camp-microsoft justus-camp-microsoft commented Jan 9, 2025

Instead of hardcoding variable names that may or not be present, we can use the typestate pattern to allow only valid variables at compile time. Motivated by #475, which uses the github.event.pull_request.head.ref in its implementation.

@justus-camp-microsoft justus-camp-microsoft requested review from a team as code owners January 9, 2025 23:59
@justus-camp-microsoft justus-camp-microsoft changed the title WIP: flowey: use typestate pattern for gh variables flowey: use typestate pattern for gh variables Jan 13, 2025
let parent_path = ctx
.get_gh_context_var()
.global(GhContextVar::GITHUB__WORKSPACE);
let test_pull_request_event = ctx.get_gh_context_var().event().pull_request();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presumably this will be removed before merge, yeah?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah just left in to show it working properly, will remove in a commit that also addresses whatever comments are left

@@ -685,10 +706,10 @@ pub trait NodeCtxBackend {
uses: &str,
with: BTreeMap<String, ClaimedGhParam>,
condvar: Option<String>,
outputs: BTreeMap<String, Vec<(String, bool)>>,
outputs: BTreeMap<String, Vec<(String, bool, bool)>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we do indeed need this extra bool added to these tuples... I think we should use this opportunity to introduce a proper set of struct types to use instead of these tuples, with field names that make the intent of each field clear

@@ -838,7 +838,101 @@ pub struct NodeCtx<'a> {
backend: Rc<RefCell<&'a mut dyn NodeCtxBackend>>,
}

impl NodeCtx<'_> {
#[derive(Serialize, Deserialize)]
Copy link
Contributor

@daprilik daprilik Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these new types you're adding here should be organized under a new module, to avoid polluting this already messy namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved into a new module but that also required making some fields public that weren't previously. let me know if there was a better way to go about this

}

impl<'a> GhContextVarReader<'a, ghvarstate::Root> {
pub fn global(&self, gh_var: GhContextVar) -> ReadVar<String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make a helper method to avoid duplicating the mostly-identical body between this and other methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a helper for the duplicate functionality

@@ -836,20 +836,28 @@ fn resolve_flow_as_github_yaml_steps(
output_steps.push(map.into());
}

for (rust_var, gh_var, is_secret) in rust_to_gh {
for gh_var_state in rust_to_gh {
let mut cmd = String::new();

// flowey considers all GitHub vars to be typed as raw strings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stale comment - not actually true with the introduction of is_object

@@ -7,6 +7,7 @@
//! this crate!** The crate you should be using is called `flowey`, which only
//! exports user-facing types / traits.

pub mod github_context;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this mod should be a mod under mod node, not a new top-level mod.

this should also help with some of the visibility issues you might've run into

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, this fixed pretty much all the visibility stuff I was having issues with

pub head: Head,
}

pub enum Root {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you still want to stuff these ZSTs into their own mod state inline module, just to avoid polluting this namespace with generic-sounding things like Root and Event


//! Core types and traits used to read GitHub context variables.

use crate::node::{user_facing::GhContextVar, ClaimVar, NodeCtx, ReadVar, StepCtx};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

each import on its own line please.

the guide has some suggestions on how to configure rust-analyzer to do this for you

@@ -193,8 +194,8 @@ pub enum VarClaimed {}
/// is possible to infer what order steps must be run in.
#[derive(Debug, Serialize, Deserialize)]
pub struct WriteVar<T: Serialize + DeserializeOwned, C = VarNotClaimed> {
backing_var: String,
is_secret: bool,
pub backing_var: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this pub def won't fly 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto with any other pub you've added to user_facing types

@@ -1280,7 +1266,7 @@ impl NodeCtx<'_> {

#[track_caller]
#[must_use]
fn new_maybe_secret_var<T>(
pub fn new_maybe_secret_var<T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an internal helper - def don't want this public

}

impl<'a> GhContextVarReader<'a, Root> {
pub fn global(&self, gh_var: GhContextVar) -> ReadVar<String> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that you have this read_var helper, I really think that it's not that hard to swap the current constants with standalone helpers - especially since you've already needed to touch those files as part of this global method refactoring.

the only thing you'll need to tweak is the user-defined secret var handling, but even that should be pretty straightforward, if you simply swap the current type that flow uses (i.e: the GhContextVar type we want to go away), with a far more limited newtype around a String called GhUserSecretVar, which is what gets created at the Pipeline level, and plumbed down to a new pub fn secret() method that hangs off this GhContextVarReader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not the most consistent with using mod.rs vs. name.rs + name/submod.rs... but I think in this case, we should leave this file as node.rs

impl<S> GhContextVarReader<'_, S> {
fn read_var<T: Serialize + DeserializeOwned>(
&self,
var_name: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this helper can be made marginally more ergonomic by using impl AsRef<str> here. that way - you don't need to invoke .to_string() or .into() when passing &'static str args to it

@@ -22,7 +22,6 @@ pub mod build_and_run_doc_tests;
pub mod build_and_run_nextest_unit_tests;
pub mod build_and_run_nextest_vmm_tests;
pub mod cfg_common;
pub mod cfg_gh_azure_login;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On one hand, I'd prefer if we avoided dropping github-secrets functionality on the ground here, since it seems highly probably that there will come a point in the not-so-far-away future where we'll want to run some stuff in CI that relies on private auth.

On the other hand, yes, we don't currently use this secrets stuff on github, so deleting it in the face of this refactors doesn't "break" anything, and lets us avoid the work of updating the current secret-handling APIs to work with the new github var infrastructure.

I'll let @benhillis chime in on his opinion, as I'm not very cognizant of the time tradeoffs when working on this sort sort of foundational infrastructure, and I don't have a full picture of your other priorities.

My 2c: I already sketched out in another comment how you can port the current gh secret infra to this new foundation, and can discuss it more offline if need be. It don't think it'd be that hard, and it'll make life easier for whoever is next up-to-bat in this space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline but I pushed this in a half-broken state (not ready for review) to work on a different machine. I'll reintroduce this file and the API I removed for custom secrets in a commit today.

#[derive(Clone, Debug)]
pub enum GhParam<C = VarNotClaimed> {
Static(String),
GhVar(GhContextVar),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

losing this means we are slightly more inefficient when invoking certain Github Actions from flowey, as we might end up round-tripping a github native variable through a flowey Var for "no reason".

but honestly, that doesn't seem like a big deal, given the perf hit isn't particularly noticible, and could be optimized later if need be.

so this is fine

@@ -355,6 +356,9 @@ pub struct Pipeline {
gh_bootstrap_template: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct GhUserSecretVar(pub String);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets move this to node.rs.

also, we should try to avoid making its guts totally pub. Maybe pub(crate) if need be. but in general, I don't think we want users to really poke around the internal repr of this type after creating it via a Pipeline level API

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, this needs a doc comment

@@ -54,6 +57,8 @@ pub mod user_facing {
pub use crate::flowey_request;
pub use crate::new_flow_node;
pub use crate::new_simple_flow_node;
pub use crate::node::github_context::state;
pub use crate::node::github_context::GhVarState;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be user-facing?

Comment on lines 16 to 25
#[derive(Serialize, Deserialize)]
pub struct Head {
#[serde(rename = "ref")]
pub head_ref: String,
}

#[derive(Serialize, Deserialize)]
pub struct GhContextVarReaderEventPullRequest {
pub head: Head,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these types, which directly correspond to the JSON spec defined by GitHub Actions, should be in their own mod spec, with some doc-comments pointing folks at the docs that define the schema

use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect you'll want to have a new pub mod user_facing here which will let you slice-up what public / private parts of this new API.

and then you can pub use github_context::user_facing::* under the node-level user_facing module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants