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

Add repository secrets api #399

Merged
merged 4 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/create_repo_secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use crypto_box::{self, aead::OsRng, PublicKey};
use octocrab::{models::repos::secrets::CreateRepositorySecret, Octocrab};
use std::convert::TryInto;

#[tokio::main]
async fn main() -> octocrab::Result<()> {
let token = std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env variable is required");

let octocrab = Octocrab::builder().personal_token(token).build()?;
let repo = octocrab.repos("owner", "repo");
let secrets = repo.secrets();

let public_key = secrets.get_public_key().await?;

let crypto_pk = {
let pk_bytes = B64.decode(public_key.key).unwrap();
let pk_array: [u8; crypto_box::KEY_SIZE] = pk_bytes.try_into().unwrap();
PublicKey::from(pk_array)
};

let encrypted_value = crypto_box::seal(&mut OsRng, &crypto_pk, b"Very secret value").unwrap();

let result = secrets
.create_or_update_secret(
"TEST_SECRET_RS",
&CreateRepositorySecret {
encrypted_value: &B64.encode(encrypted_value),
key_id: &public_key.key_id,
},
)
.await?;

println!("{:?}", result);

Ok(())
}
7 changes: 7 additions & 0 deletions src/api/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod generate;
mod merges;
mod pulls;
pub mod releases;
mod secrets;
mod stargazers;
mod status;
mod tags;
Expand All @@ -27,6 +28,7 @@ pub use generate::GenerateRepositoryBuilder;
pub use merges::MergeBranchBuilder;
pub use pulls::ListPullsBuilder;
pub use releases::ReleasesHandler;
pub use secrets::RepoSecretsHandler;
pub use stargazers::ListStarGazersBuilder;
pub use status::{CreateStatusBuilder, ListStatusesBuilder};
pub use tags::ListTagsBuilder;
Expand Down Expand Up @@ -588,4 +590,9 @@ impl<'octo> RepoHandler<'octo> {
) -> MergeBranchBuilder<'octo, '_> {
MergeBranchBuilder::new(self, head, base)
}

/// Handle secrets on the repository
pub fn secrets(&self) -> RepoSecretsHandler<'_> {
RepoSecretsHandler::new(self)
}
}
169 changes: 169 additions & 0 deletions src/api/repos/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use http::StatusCode;
use snafu::GenerateImplicitData;

use super::RepoHandler;
use crate::models::repos::secrets::{CreateRepositorySecret, CreateRepositorySecretResponse};

/// A client to GitHub's repository secrets API.
///
/// Created with [`Octocrab::repos`].
pub struct RepoSecretsHandler<'octo> {
repo: &'octo RepoHandler<'octo>,
}

impl<'octo> RepoSecretsHandler<'octo> {
pub(crate) fn new(repo: &'octo RepoHandler<'octo>) -> Self {
Self { repo }
}

/// Lists all secrets available in a repository without revealing their encrypted values.
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let all_secrets = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_secrets()
/// .await?;
/// # Ok(())
/// # }
pub async fn get_secrets(
&self,
) -> crate::Result<crate::models::repos::secrets::RepositorySecrets> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets",
owner = self.repo.owner,
repo = self.repo.repo
);
self.repo.crab.get(route, None::<&()>).await
}

/// Gets your public key, which you need to encrypt secrets.
/// You need to encrypt a secret before you can create or update secrets.
/// Anyone with read access to the repository can use this endpoint.
/// If the repository is private you must use an access token with the `repo` scope.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let public_key = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_public_key()
/// .await?;
/// # Ok(())
/// # }
pub async fn get_public_key(&self) -> crate::Result<crate::models::PublicKey> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/public-key",
owner = self.repo.owner,
repo = self.repo.repo
);
self.repo.crab.get(route, None::<&()>).await
}

/// Gets a single repository secret without revealing its encrypted value.
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let secret_info = octocrab.repos("owner", "repo")
/// .secrets()
/// .get_secret("TOKEN")
/// .await?;
/// # Ok(())
/// # }
pub async fn get_secret(
&self,
secret_name: impl AsRef<str>,
) -> crate::Result<crate::models::repos::secrets::RepositorySecret> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);
self.repo.crab.get(route, None::<&()>).await
}

/// Creates or updates a repository secret with an encrypted value.
/// Encrypt your secret using [`crypto_box`](https://crates.io/crates/crypto_box).
/// You must authenticate using an access token with the `repo` scope to use this endpoint.
/// GitHub Apps must have the `secrets` repository permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// use octocrab::models::repos::secrets::{CreateRepositorySecret, CreateRepositorySecretResponse};
///
/// let result = octocrab.repos("owner", "repo")
/// .secrets()
/// .create_or_update_secret("GH_TOKEN", &CreateRepositorySecret{
/// key_id: "123456",
/// encrypted_value: "some-b64-encrypted-string",
/// })
/// .await?;
///
/// match result {
/// CreateRepositorySecretResponse::Created => println!("Created secret!"),
/// CreateRepositorySecretResponse::Updated => println!("Updated secret!"),
/// }
/// # Ok(())
/// # }
pub async fn create_or_update_secret(
&self,
secret_name: impl AsRef<str>,
secret: &CreateRepositorySecret<'_>,
) -> crate::Result<CreateRepositorySecretResponse> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);

let resp = {
let resp = self.repo.crab._put(route, Some(secret)).await?;
crate::map_github_error(resp).await?
};

match resp.status() {
StatusCode::CREATED => Ok(CreateRepositorySecretResponse::Created),
StatusCode::NO_CONTENT => Ok(CreateRepositorySecretResponse::Updated),
status_code => Err(crate::Error::Other {
source: format!(
"Unexpected status code from request: {}",
status_code.as_str()
)
.into(),
backtrace: snafu::Backtrace::generate(),
}),
}
}

/// Deletes a secret in an organization using the secret name.
/// You must authenticate using an access token with the `admin:org` scope to use this endpoint.
/// GitHub Apps must have the `secrets` organization permission to use this endpoint.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// # let octocrab = octocrab::Octocrab::default();
/// let repo = octocrab.repos("owner", "repo")
/// .secrets()
/// .delete_secret("GH_TOKEN")
/// .await?;
///
/// # Ok(())
/// # }
pub async fn delete_secret(&self, secret_name: impl AsRef<str>) -> crate::Result<()> {
let route = format!(
"/repos/{owner}/{repo}/actions/secrets/{secret_name}",
owner = self.repo.owner,
repo = self.repo.repo,
secret_name = secret_name.as_ref()
);

let resp = self.repo.crab._delete(route, None::<&()>).await?;
crate::map_github_error(resp).await?;
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/models/repos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use hyper::{body, Response};
use snafu::ResultExt;
use url::Url;

pub mod secrets;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
Expand Down
31 changes: 31 additions & 0 deletions src/models/repos/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use super::super::*;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepositorySecret {
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RepositorySecrets {
pub total_count: i32,
pub secrets: Vec<RepositorySecret>,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CreateRepositorySecret<'a> {
/// Value for your secret,
/// encrypted with LibSodium using the public key retrieved from the Get an organization public key endpoint.
pub encrypted_value: &'a str,
/// ID of the key you used to encrypt the secret.
pub key_id: &'a str,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CreateRepositorySecretResponse {
Created,
Updated,
}
Loading