hive-github-notify: pick up a rotated PAT without a restart
The poller read `<state>/github-token` once at start, so after a PAT rotation it kept polling with the revoked token until the unit restarted. It now re-reads the file on every tick, as `hive-forge-notify` does: a different non-empty value rebuilds the GitHub source, clears the own-login cache, and logs one info line. A missing or empty file keeps the token in use. A 401 from the notifications endpoint is now logged at warn, once per token value; other non-2xx statuses stay at debug. Part of #5017 (slice G).
This commit is contained in:
parent
fa56931a99
commit
cf763da178
5 changed files with 230 additions and 16 deletions
|
|
@ -7,6 +7,8 @@
|
|||
//! tolerant parse in the shared library is the right common layer, not
|
||||
//! the client.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use hive_forge_notify::notify::UNREAD_FETCH_LIMIT;
|
||||
use hive_forge_notify::source::Source;
|
||||
use tracing::{debug, warn};
|
||||
|
|
@ -15,7 +17,7 @@ use tracing::{debug, warn};
|
|||
/// github.com only — the same constraint the `gh` wrapper and the git
|
||||
/// credential helper already carry — so this is a constant rather than
|
||||
/// another operator-entered URL.
|
||||
const API_BASE: &str = "https://api.github.com";
|
||||
pub const API_BASE: &str = "https://api.github.com";
|
||||
|
||||
/// Value of the `X-GitHub-Api-Version` header. GitHub dates its REST
|
||||
/// versions; pinning one means a future default bump cannot silently
|
||||
|
|
@ -27,13 +29,20 @@ const USER_AGENT: &str = "hyperhive-forge-notify";
|
|||
|
||||
/// github.com, via a personal access token.
|
||||
pub struct GithubSource {
|
||||
/// [`API_BASE`] everywhere but the tests, which point it at a local stub.
|
||||
api_base: String,
|
||||
token: String,
|
||||
/// Set once a 401 has been logged at `warn`. A source holds one token,
|
||||
/// so this caps the warning at one per token value.
|
||||
warned_unauthorized: AtomicBool,
|
||||
}
|
||||
|
||||
impl GithubSource {
|
||||
pub fn new(token: &str) -> Self {
|
||||
pub fn new(api_base: &str, token: &str) -> Self {
|
||||
Self {
|
||||
api_base: api_base.to_owned(),
|
||||
token: token.to_owned(),
|
||||
warned_unauthorized: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +71,7 @@ impl Source for GithubSource {
|
|||
}
|
||||
|
||||
async fn own_login(&self, client: &reqwest::Client) -> String {
|
||||
let url = format!("{API_BASE}/user");
|
||||
let url = format!("{}/user", self.api_base);
|
||||
let Ok(resp) = self.authorize(client.get(&url)).send().await else {
|
||||
return String::new();
|
||||
};
|
||||
|
|
@ -80,7 +89,10 @@ impl Source for GithubSource {
|
|||
&self,
|
||||
client: &reqwest::Client,
|
||||
) -> Option<(Vec<serde_json::Value>, Option<u64>)> {
|
||||
let url = format!("{API_BASE}/notifications?all=false&per_page={UNREAD_FETCH_LIMIT}");
|
||||
let url = format!(
|
||||
"{}/notifications?all=false&per_page={UNREAD_FETCH_LIMIT}",
|
||||
self.api_base
|
||||
);
|
||||
let resp = match self.authorize(client.get(&url)).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
|
|
@ -101,7 +113,17 @@ impl Source for GithubSource {
|
|||
// 403 with a rate-limit body and 401 on a revoked PAT are
|
||||
// both "stay quiet and retry", not fatal: the unit must not
|
||||
// restart-loop on a credential the operator fixes out of band.
|
||||
debug!("forge_notify: github poll status {status}");
|
||||
// A 401 is the one an operator must act on, so it is a `warn`.
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED
|
||||
&& !self.warned_unauthorized.swap(true, Ordering::Relaxed)
|
||||
{
|
||||
warn!(
|
||||
"forge_notify: github refused the PAT (401) — no notifications \
|
||||
until github-token holds a working one"
|
||||
);
|
||||
} else {
|
||||
debug!("forge_notify: github poll status {status}");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let raw = resp.text().await.ok()?;
|
||||
|
|
@ -116,7 +138,7 @@ impl Source for GithubSource {
|
|||
}
|
||||
|
||||
async fn mark_read(&self, client: &reqwest::Client, id: &str) {
|
||||
let url = format!("{API_BASE}/notifications/threads/{id}");
|
||||
let url = format!("{}/notifications/threads/{id}", self.api_base);
|
||||
match self.authorize(client.patch(&url)).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
debug!(%id, "forge_notify: marked read (github)");
|
||||
|
|
|
|||
Loading…
Reference in a new issue