1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! A client for the GitHub Actions **Runner Scale Set** APIs.
//!
//! Scale sets let you autoscale self-hosted runners on any infrastructure,
//! without Kubernetes or actions-runner-controller. This crate covers the
//! whole API: scale sets, just-in-time (JIT) runner configs, and the message
//! sessions that tell you how many runners are needed. It follows the
//! protocol of GitHub's Go client,
//! [`actions/scaleset`](https://github.com/actions/scaleset).
//!
//! # Overview
//!
//! 1. Build a [`Client`] with a GitHub App ([`Credentials::github_app`],
//! preferred) or a personal access token.
//! 2. Create a scale set with [`Client::create_runner_scale_set`]. Its name
//! is the label workflows target with `runs-on`.
//! 3. Run a [`Listener`](listener::Listener) with your
//! [`Scaler`](listener::Scaler). It opens the message session, acquires
//! jobs, acknowledges messages and recovers from failures.
//! 4. In the scaler, keep
//! [`total_assigned_jobs`](ScaleSetStatistics::total_assigned_jobs)
//! runners available: start each with a config from
//! [`Client::generate_jit_config`].
//!
//! ```no_run
//! use std::collections::HashSet;
//!
//! use scaleset::listener::{Event, Listener, Scaler};
//! use scaleset::{
//! BoxError, Client, Credentials, ExposeSecret, JitRunnerSetting, RunnerScaleSetSpec,
//! ScaleSetId,
//! };
//!
//! /// Keeps one runner for every job assigned to the scale set.
//! struct Autoscaler {
//! client: Client,
//! scale_set_id: ScaleSetId,
//! /// Runners started and not finished yet.
//! runners: HashSet<String>,
//! }
//!
//! impl Scaler for Autoscaler {
//! type Error = BoxError;
//!
//! async fn scale(&mut self, event: Event<'_>) -> Result<(), BoxError> {
//! // Runners are ephemeral: each one leaves when its job completes.
//! if let Some(message) = event.message() {
//! for job in message.jobs_completed() {
//! self.runners.remove(&job.runner_name);
//! }
//! }
//! let wanted = event.statistics().total_assigned_jobs as usize;
//! while self.runners.len() < wanted {
//! let setting = JitRunnerSetting::unique("runner");
//! let jit = self.client.generate_jit_config(self.scale_set_id, &setting).await?;
//! // Start a runner (process, container, VM) with
//! // ACTIONS_RUNNER_INPUT_JITCONFIG set to this value.
//! let _config = jit.encoded_jit_config.expose_secret();
//! self.runners.insert(setting.name);
//! }
//! Ok(())
//! }
//! }
//!
//! # async fn run() -> Result<(), BoxError> {
//! let pem = std::fs::read_to_string("app.pem")?;
//! let client = Client::builder(
//! "https://github.com/my-org",
//! Credentials::github_app("Iv1.abc123", 12345, &pem)?,
//! )
//! .build()?;
//!
//! let scale_set = client
//! .create_runner_scale_set(&RunnerScaleSetSpec::new("my-scale-set"))
//! .await?;
//!
//! let mut scaler = Autoscaler { client: client.clone(), scale_set_id: scale_set.id, runners: HashSet::new() };
//! Listener::new(client.clone(), scale_set.id, 10)
//! .run(&mut scaler, async { let _ = tokio::signal::ctrl_c().await; })
//! .await?;
//!
//! client.delete_runner_scale_set(scale_set.id).await?;
//! # Ok(()) }
//! ```
//!
//! # Layers
//!
//! | Layer | Use it to |
//! | --- | --- |
//! | [`Listener`](listener::Listener) + [`Scaler`](listener::Scaler) | run an autoscaler; the usual choice |
//! | [`Subscription`](listener::Subscription) | drive the poll / acknowledge loop yourself |
//! | [`MessageSession`] | call the message queue endpoints directly |
//! | [`Client`] | manage scale sets, runners and JIT configs |
//!
//! # Transport and retries
//!
//! The HTTP client is yours ([`ClientBuilder::http_client`], with defaults
//! from [`http_client_builder`]); retries belong to the SDK
//! ([`RetryPolicy`]). Transport errors, 5xx other than 501, and 429 are
//! retried four times with jittered exponential backoff by default.
//! Rate-limited responses come back as a [`RateLimitError`] (see
//! [`Error::rate_limit`]) and are retried according to [`RateLimitPolicy`].
//! [`Client::with_retry_policy`] changes the policy for some calls only.
//!
//! # Testing
//!
//! With the `testing` feature, [`testing::Emulator`] runs a stateful
//! emulator of GitHub and the Actions service in your test, so an
//! autoscaler can be tested end to end without network access.
//!
//! # Logging
//!
//! The crate logs through [`tracing`]; each HTTP request runs in a
//! `scaleset.request` span. Install a subscriber to see token refreshes,
//! polls and retries.
//!
//! # Features
//!
//! * `rustls` (default): TLS through rustls.
//! * `native-tls`: TLS through the platform's library.
//!
//! Enable one of the two; without TLS the client cannot connect.
//! * `testing`: [`testing::Emulator`] and the `scaleset-emulator` binary.
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use MessageSession;
pub use *;
/// Re-exported so callers can build an HTTP client without adding the
/// dependency themselves.
pub use reqwest;
/// Re-exported for [`SecretString`] fields such as
/// [`JitRunnerConfig::encoded_jit_config`].
pub use ;
/// Compiles the README's examples as doctests.
;