pub struct GitHubForge { /* private fields */ }Expand description
The GitHub adapter: one community’s App on one GitHub (github.com or a GHES instance).
Holds the App’s key (behind AppKeySigner), its webhook secret, and
the namespaces the core has bound. It holds no forge token between calls:
each operation mints an installation token scoped to the one repository
and the permissions that operation needs, and drops it on return.
Implementations§
Source§impl GitHubForge
impl GitHubForge
Sourcepub fn parse_check_trigger(
&self,
headers: &HeaderMap,
body: &[u8],
) -> Result<Vec<CheckTrigger>>
pub fn parse_check_trigger( &self, headers: &HeaderMap, body: &[u8], ) -> Result<Vec<CheckTrigger>>
Verify a webhook and, if it calls for the bridge-posted check, say on
what. Empty for a verified delivery that does not (another event, a
closed pull request, a title edit). Err for one that failed
verification, which must not be acted on.
Triggers: pull_request opened / synchronize / reopened, and
edited when the base branch changed; merge_group
checks_requested; this App’s check_run / check_suite
rerequested (one trigger per pull request the delivery names). The
repository is the delivery’s repository — the base the check is
posted on — never the fork a pull request came from.
Sourcepub async fn default_branch(&self, repo: &Resource) -> Result<String>
pub async fn default_branch(&self, repo: &Resource) -> Result<String>
The repository’s default branch — the one branch the managed
ruleset protects (~DEFAULT_BRANCH), read from GitHub now rather
than from a delivery.
Sourcepub async fn pull_request(
&self,
repo: &Resource,
number: u64,
) -> Result<PullRequestInfo>
pub async fn pull_request( &self, repo: &Resource, number: u64, ) -> Result<PullRequestInfo>
Pull request number on repo, as GitHub reports it now.
Sourcepub async fn compare_commits(
&self,
repo: &Resource,
base: &str,
head: &str,
) -> Result<Comparison>
pub async fn compare_commits( &self, repo: &Resource, base: &str, head: &str, ) -> Result<Comparison>
The commits in base...head on repo, oldest first
(GET /repos/{o}/{r}/compare/{base}...{head}, every page: GitHub
lists 100 per page and 250 in all).
Sourcepub async fn merge_base(
&self,
repo: &Resource,
a: &str,
b: &str,
) -> Result<String>
pub async fn merge_base( &self, repo: &Resource, a: &str, b: &str, ) -> Result<String>
The merge base of a and b on repo: GitHub’s
merge_base_commit for a...b, one commit listed at most. For
recomputing a merge from a fetch too shallow to search for the base.
Sourcepub async fn contents_read_token(&self, repo: &Resource) -> Result<Secret>
pub async fn contents_read_token(&self, repo: &Resource) -> Result<Secret>
A token that can read (fetch) repo and nothing else, for one fetch
of the commits under test. The caller holds it for that fetch only
and drops it; it lapses on its own within the hour either way.
Sourcepub fn clone_url(&self, repo: &Resource) -> Result<Url>
pub fn clone_url(&self, repo: &Resource) -> Result<Url>
The HTTPS clone URL of repo on this GitHub (<web>/<owner>/<repo>.git).
Sourcepub async fn start_check_run(
&self,
repo: &Resource,
head_sha: &str,
name: &str,
external_id: &str,
) -> Result<u64>
pub async fn start_check_run( &self, repo: &Resource, head_sha: &str, name: &str, external_id: &str, ) -> Result<u64>
Post the check on head_sha as in progress, and return its id.
Sourcepub async fn finish_check_run(
&self,
repo: &Resource,
id: u64,
conclusion: CheckConclusion,
title: &str,
summary: &str,
) -> Result<()>
pub async fn finish_check_run( &self, repo: &Resource, id: u64, conclusion: CheckConclusion, title: &str, summary: &str, ) -> Result<()>
Complete check run id with conclusion, a one-line title and a
Markdown summary (truncated to what GitHub accepts).
Source§impl GitHubForge
impl GitHubForge
Sourcepub fn new(
config: GitHubConfig,
signer: Arc<dyn AppKeySigner>,
webhook_secret: Secret,
) -> Result<Self>
pub fn new( config: GitHubConfig, signer: Arc<dyn AppKeySigner>, webhook_secret: Secret, ) -> Result<Self>
An adapter for config’s App, signing with signer and verifying
webhooks with webhook_secret.
Sourcepub fn with_client_secret(self, secret: Secret) -> Self
pub fn with_client_secret(self, secret: Secret) -> Self
Give the adapter the App’s OAuth client secret (from the manifest exchange). With it, the member’s user token from an account link is revoked as soon as their id is read; without it the token is only dropped and lapses on its own (eight hours for an expiring App user token), because revocation is authenticated with the client secret.
Sourcepub fn config(&self) -> &GitHubConfig
pub fn config(&self) -> &GitHubConfig
The configuration.
Sourcepub fn register_namespace(&self, ns: Namespace) -> Result<()>
pub fn register_namespace(&self, ns: Namespace) -> Result<()>
Tell the adapter about a bound namespace (from the VTC’s store, after
the admin confirmed the bind). Operations on repositories in a
namespace that was never registered are refused with
ForgeError::NotBound: the binding, not whatever the App happens
to be installed on, is what authorises the bridge to act.
Sourcepub fn unregister_namespace(&self, ns: &Resource)
pub fn unregister_namespace(&self, ns: &Resource)
Forget a namespace (unbind).
Sourcepub fn set_managed_repositories(
&self,
ns: &Resource,
ids: impl IntoIterator<Item = u64>,
)
pub fn set_managed_repositories( &self, ns: &Resource, ids: impl IntoIterator<Item = u64>, )
Tell the adapter which repositories (by forge id) it manages in
organisation ns, from the bridge’s store: at start-up and whenever
the set changes. The org ruleset lists exactly these (plus a
repository being bootstrapped); ids GitHub lists that are not here —
archived, deleted or never managed — are dropped from it. Until it
is set, the required-workflow step refuses to run rather than
guess the set from GitHub.
Sourcepub fn managed_repositories(&self, ns: &Resource) -> Option<BTreeSet<u64>>
pub fn managed_repositories(&self, ns: &Resource) -> Option<BTreeSet<u64>>
The managed set as the adapter holds it (it adds each repository it bootstraps under a required workflow, and drops each it archives).
Sourcepub fn set_bridge_checks_ready(&self, ns: &Resource, ready: bool)
pub fn set_bridge_checks_ready(&self, ns: &Resource, ready: bool)
Record whether ns’s installation carries the bridge-posted check
(from GitHubForge::detect_bridge_checks, or the bridge’s store
after a restart). Until it is known, a namespace without a required
workflow keeps the in-repo Actions workflow.
Sourcepub fn bridge_checks_ready(&self, ns: &Resource) -> Option<bool>
pub fn bridge_checks_ready(&self, ns: &Resource) -> Option<bool>
Whether ns’s installation is known to carry the bridge-posted
check (None: not known yet).
Sourcepub async fn detect_bridge_checks(&self, ns: &Resource) -> Result<bool>
pub async fn detect_bridge_checks(&self, ns: &Resource) -> Result<bool>
Read ns’s installation and record whether it grants what the
bridge-posted check needs: checks: write, pull_requests: read,
merge_queues: read and the pull_request and merge_group
subscriptions (crate::manifest::check_ready). An App registered
before these were in the manifest lacks them until its owner updates
the App’s settings and each installation approves the change; the
bridge probes again when an installation accepts new permissions.
Sourcepub async fn detect_installation(
&self,
ns: &Resource,
) -> Result<(bool, Vec<String>)>
pub async fn detect_installation( &self, ns: &Resource, ) -> Result<(bool, Vec<String>)>
GitHubForge::detect_bridge_checks, and what the installation
lacks of what the App asks for (as name:level, and event:<name>
for a missing subscription) — the same list a bind reports in
NamespaceBinding::missing_permissions, read again now (after an
owner approved an upgrade, say).
Sourcepub fn set_required_workflow(&self, ns: &Resource, available: bool)
pub fn set_required_workflow(&self, ns: &Resource, available: bool)
Record whether org rulesets — and so a required workflow — are
available in organisation ns (from GitHubForge::detect_required_workflow,
or from the bridge’s store after a restart). Until it is known the
namespace plans the owner-review fallback.
Sourcepub fn set_required_workflow_pin(&self, ns: &Resource, pin: RequiredWorkflowPin)
pub fn set_required_workflow_pin(&self, ns: &Resource, pin: RequiredWorkflowPin)
Restore the pin the required-workflow step last made in ns.
Sourcepub fn required_workflow_pin(
&self,
ns: &Resource,
) -> Option<RequiredWorkflowPin>
pub fn required_workflow_pin( &self, ns: &Resource, ) -> Option<RequiredWorkflowPin>
The pin the required-workflow step last made in ns, to persist.
Sourcepub async fn detect_required_workflow(&self, ns: &Resource) -> Result<bool>
pub async fn detect_required_workflow(&self, ns: &Resource) -> Result<bool>
Find out whether organisation ns can have a required workflow, and
record the answer.
The probe is GET /orgs/{org}/rulesets with an organization
Administration token. Org rulesets exist on GitHub Team and
Enterprise plans only; on a Free organisation, or where the owner has
not granted the App organization Administration, GitHub refuses
(403/404, or 422 when minting the token) and the answer is false.
A personal account, or manual mode, is always false. Network and
rate-limit failures are returned, not guessed at.
A true here is necessary, not sufficient: GitHub documents the
workflows rule for Enterprise Cloud. If the org ruleset is then
refused, the required-workflow step records false and asks for a
re-plan.
Sourcepub fn new_state() -> Result<String>
pub fn new_state() -> Result<String>
A fresh bind state nonce: 256 bits from the system CSPRNG,
base64url. The caller stores it with its expiry and hands it back to
Forge::complete_bind.
Sourcepub async fn fetch_web_flow_key(&self) -> Result<Vec<u8>>
pub async fn fetch_web_flow_key(&self) -> Result<Vec<u8>>
Download GitHub’s web-flow public key (<web>/web-flow.gpg) for the
platform keyring. Never called implicitly: the keyring is
configuration, and fetching it is a choice the operator makes and
can review.
Sourcepub async fn repository_by_id(
&self,
ns: &Resource,
id: u64,
) -> Result<Option<Resource>>
pub async fn repository_by_id( &self, ns: &Resource, id: u64, ) -> Result<Option<Resource>>
Where repository id is now, as namespace ns’s installation sees it
(GET /repositories/{id} with that installation’s token): None if
it cannot see it. What a transfer into ns is confirmed by — GitHub’s
word, not a webhook’s.
Source§impl GitHubForge
impl GitHubForge
Sourcepub fn parse_push(
&self,
headers: &HeaderMap,
body: &[u8],
) -> Result<Option<PushEvent>>
pub fn parse_push( &self, headers: &HeaderMap, body: &[u8], ) -> Result<Option<PushEvent>>
Verify a webhook and, if it is a push, read it. Ok(None) for a
verified delivery of any other event; Err for one that failed
verification or is malformed, which must not be acted on.
Sourcepub async fn contents_write_token(&self, repo: &Resource) -> Result<Secret>
pub async fn contents_write_token(&self, repo: &Resource) -> Result<Secret>
A token that can push to repo and nothing else, for one push of
re-signed commits. The caller holds it for that push only.
Trait Implementations§
Source§impl Debug for GitHubForge
impl Debug for GitHubForge
Source§impl Forge for GitHubForge
impl Forge for GitHubForge
Source§fn indirect_access<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
account: &'life2 ForgeAccount,
) -> Pin<Box<dyn Future<Output = Result<Option<IndirectAccess>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn indirect_access<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
account: &'life2 ForgeAccount,
) -> Pin<Box<dyn Future<Output = Result<Option<IndirectAccess>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
GitHub’s effective permission for the account
(/collaborators/{username}/permission counts teams, organisation
ownership and the base permission), then where it comes from.
Source§fn diff(&self, observed: &RepoState, desired: &Projection) -> Vec<Drift>
fn diff(&self, observed: &RepoState, desired: &Projection) -> Vec<Drift>
The default comparison, plus the owner-review guard against the projection’s owners (§9, the user’s decision on solo repositories): two or more owners must all be reviewers of a healthy guard; one owner needs no guard, and a guard left over from when there were more is a re-plan (it would lock the solo owner out).
Source§fn host(&self) -> &str
fn host(&self) -> &str
github.com, a GHES host,
codeberg.org). Every resource it accepts starts with it; a
resource on another host is refused rather than sent to the wrong
forge.Source§fn capabilities(&self, ns: &Namespace) -> Capabilities
fn capabilities(&self, ns: &Namespace) -> Capabilities
Forge::kind.Source§fn begin_bind<'life0, 'async_trait>(
&'life0 self,
req: BindRequest,
) -> Pin<Box<dyn Future<Output = Result<BindStep>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn begin_bind<'life0, 'async_trait>(
&'life0 self,
req: BindRequest,
) -> Pin<Box<dyn Future<Output = Result<BindStep>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn complete_bind<'life0, 'async_trait>(
&'life0 self,
cb: BindCallback,
) -> Pin<Box<dyn Future<Output = Result<NamespaceBinding>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn complete_bind<'life0, 'async_trait>(
&'life0 self,
cb: BindCallback,
) -> Pin<Box<dyn Future<Output = Result<NamespaceBinding>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn begin_account_link<'life0, 'life1, 'async_trait>(
&'life0 self,
member: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<LinkStep>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn begin_account_link<'life0, 'life1, 'async_trait>(
&'life0 self,
member: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<LinkStep>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
member is their DID, for
the adapter’s audit trail; nothing forge-side sees it.Source§fn complete_account_link<'life0, 'async_trait>(
&'life0 self,
cb: LinkCallback,
) -> Pin<Box<dyn Future<Output = Result<ForgeAccount>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn complete_account_link<'life0, 'async_trait>(
&'life0 self,
cb: LinkCallback,
) -> Pin<Box<dyn Future<Output = Result<ForgeAccount>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn inspect<'life0, 'life1, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
) -> Pin<Box<dyn Future<Output = Result<RepoState>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn inspect<'life0, 'life1, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
) -> Pin<Box<dyn Future<Output = Result<RepoState>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn create_repo<'life0, 'life1, 'async_trait>(
&'life0 self,
spec: &'life1 RepoSpec,
) -> Pin<Box<dyn Future<Output = Result<RepoState>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn create_repo<'life0, 'life1, 'async_trait>(
&'life0 self,
spec: &'life1 RepoSpec,
) -> Pin<Box<dyn Future<Output = Result<RepoState>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
ForgeError::AlreadyExists — adopting it is a separate, elevated
decision (§5.6), not something a retry should do silently.Source§fn archive_repo<'life0, 'life1, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn archive_repo<'life0, 'life1, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn apply_roles<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
desired: &'life2 [RoleAssignment],
unlisted: Unlisted,
) -> Pin<Box<dyn Future<Output = Result<ApplyReport>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn apply_roles<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
desired: &'life2 [RoleAssignment],
unlisted: Unlisted,
) -> Pin<Box<dyn Future<Output = Result<ApplyReport>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
desired.
Collaborators desired does not mention are handled per unlisted.Source§fn bootstrap_plan(
&self,
repo: &RepoSpec,
cfg: &VgiConfig,
) -> Result<Vec<BootstrapStep>>
fn bootstrap_plan( &self, repo: &RepoSpec, cfg: &VgiConfig, ) -> Result<Vec<BootstrapStep>>
Source§fn run_step<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
step: &'life2 BootstrapStep,
) -> Pin<Box<dyn Future<Output = Result<StepOutcome>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn run_step<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
repo: &'life1 Resource,
step: &'life2 BootstrapStep,
) -> Pin<Box<dyn Future<Output = Result<StepOutcome>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Source§fn parse_event(
&self,
headers: &HeaderMap,
body: &[u8],
) -> Result<Option<ForgeEvent>>
fn parse_event( &self, headers: &HeaderMap, body: &[u8], ) -> Result<Option<ForgeEvent>>
Ok(None) for a verified delivery
the core has no use for; Err for one that failed verification —
which must not be acted on.Source§fn normalize(&self, raw: &str) -> Result<Resource, ForgeError>
fn normalize(&self, raw: &str) -> Result<Resource, ForgeError>
owner[/repo] grammar GitHub and Forgejo share and refuses a
resource on another host.Source§fn map_role(
&self,
ns: &Namespace,
rights: EffectiveRights,
map: &RoleMap,
) -> ForgeRole
fn map_role( &self, ns: &Namespace, rights: EffectiveRights, map: &RoleMap, ) -> ForgeRole
ns.
The default asks map for a role and rounds it down onto the
namespace’s ladder.Source§fn is_protected_account(&self, ns: &Namespace, account: u64) -> bool
fn is_protected_account(&self, ns: &Namespace, account: u64) -> bool
account must never be taken off a
repository in ns, whatever a job asks: the namespace’s owner (on a
personal account, the implicit admin of every repository in it) and
the adapter’s own automation identity (a Forgejo bot, a GitHub App’s
bot user), without which nothing the bridge does would keep working. Read moreSource§impl ForgeHooks for GitHubForge
impl ForgeHooks for GitHubForge
Source§fn before_apply_roles(
&self,
repo: &Resource,
desired: &[RoleAssignment],
) -> HookDecision<Vec<RoleAssignment>>
fn before_apply_roles( &self, repo: &Resource, desired: &[RoleAssignment], ) -> HookDecision<Vec<RoleAssignment>>
In a personal-account namespace the owner is the repository’s implicit admin and GitHub refuses to add them as a collaborator, so they are dropped from the desired set before it reaches GitHub (and before the core reports their “missing” role as drift).
Source§fn before_create(&self, _spec: &RepoSpec) -> HookDecision<RepoSpec>
fn before_create(&self, _spec: &RepoSpec) -> HookDecision<RepoSpec>
Modify replaces the spec.Source§fn after_create(&self, _state: &RepoState) -> HookDecision<Vec<BootstrapStep>>
fn after_create(&self, _state: &RepoState) -> HookDecision<Vec<BootstrapStep>>
Modify adds steps for the core to
run before the bootstrap plan (Forgejo sets fast-forward-only merges
here).Source§fn after_bootstrap(
&self,
_repo: &Resource,
_outcomes: &[(String, StepOutcome)],
) -> HookDecision<Vec<BootstrapStep>>
fn after_bootstrap( &self, _repo: &Resource, _outcomes: &[(String, StepOutcome)], ) -> HookDecision<Vec<BootstrapStep>>
Modify adds follow-up steps.Source§fn on_event(&self, _event: &ForgeEvent) -> HookDecision<ForgeEvent>
fn on_event(&self, _event: &ForgeEvent) -> HookDecision<ForgeEvent>
Modify replaces it; Abort drops it.