Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

A free workspace keeps up to 2 GB of Actions artifacts and cache together, across its repositories. The only caps were per repository (10 GB of cache) and per run (10 GB of artifacts, kept up to 90 days), so a free workspace with many repositories or runs could keep any amount in R2 at g1t's cost. Reserving an artifact or a cache entry now adds up what the workspace's ready and pending entries hold, from the indexes already there, and past FREE_ACTIONS_STORAGE_BYTES (2 GiB, a wrangler var; 0 is no cap) asks billing whether it has the plan. Without it the upload is refused with a message saying what it holds and how to make room. Billing is asked only past the cap, and when it does not answer the upload goes ahead. Nothing already kept is touched. The Actions guide says so.

syntaqxcommitted Parentf1e1810Browse files
4 files+74−00/4 viewed
+2−0
782782 | --- | --- |
783783 | One entry | Up to 2 GiB, compressed. A larger one is not saved, and the job goes on. |
784784 | A repository's entries | Up to 10 GiB together. Saving past it removes the entries restored longest ago. |
785+| A free workspace | Its cache and [artifacts](#artifacts), in every repository, up to 2 GiB together. Past it, a save is refused (the job goes on) until old entries expire or artifacts are deleted; on the g1t plan there is no such cap. |
785786 | How long | Until it has not been restored for 7 days, and at most 28 days after it was saved. |
786787 | Which branch | An entry belongs to the branch, tag or pull request whose run saved it. A run restores from its own, then from the branch its pull request merges into, then from the default branch. |
787788 | Keys | Written once on each branch: saving under a key that exists there does nothing. On each branch in turn, a restore finds its `key` exactly, else the newest entry whose key starts with one of its `restore-keys`. |
821822 | --- | --- |
822823 | One artifact | Up to 5 GiB, zipped. |
823824 | A run's artifacts | Up to 10 GiB together. |
825+| A free workspace | Its artifacts and [cache](#the-cache), in every repository, up to 2 GiB together. Past it, an upload fails and says so, until some expire or are deleted; on the g1t plan there is no such cap. |
824826 | How long | The repository's setting: 14 days unless someone with the Maintain role changes it under **Settings → Repository → Artifacts**, from 1 to 90 days. `retention-days` asks for fewer days, never more. |
825827 | Names | One artifact per name in a run. Uploading a name again fails, unless the upload says `overwrite: true`, which replaces it. A name is up to 256 characters, none of `" : < > \| * ? \ /`. |
826828 | `path` | Files, folders and globs, `**` included; a line starting with `!` leaves matching paths out. Files and folders whose names start with `.` are left out unless `include-hidden-files: true`. |
+3−0
263263 format!("It is {} MB; a cache entry is at most {} MB.", a.size / 1_048_576, CACHE_MAX_ENTRY_BYTES / 1_048_576),
264264 ));
265265 }
266+ if let Some(why) = self.storage_refused(&job.namespace, a.size).await? {
267+ return Ok(fail(FailureCode::Limit, why));
268+ }
266269 let now = now_ms();
267270 let at = rfc3339(now);
268271 let version = a.version.clone().unwrap_or_default();
+66−0
104104 }
105105 }
106106
107+/// What a free workspace is told when an upload would take its artifacts
108+/// and cache past `cap`, holding `held` and adding `adding`. None while it
109+/// fits, or with no cap.
110+pub fn storage_refusal(held: u64, adding: u64, cap: u64) -> Option<String> {
111+ (cap > 0 && held.saturating_add(adding) > cap).then(|| {
112+ format!(
113+ "This workspace's artifacts and cache hold {} of the {} a workspace on the free plan keeps, so this upload is not kept. Delete artifacts you no longer need, let old cache entries expire, or start the g1t plan, which has no such cap.",
114+ shown_bytes(held),
115+ shown_bytes(cap)
116+ )
117+ })
118+}
119+
120+impl crate::Actions {
121+ /// Why an upload of `adding` bytes to the workspace's artifacts or
122+ /// cache is refused: only past FREE_ACTIONS_STORAGE_BYTES, and only
123+ /// without the plan. Billing is asked only once the workspace is past
124+ /// the cap, and when it does not answer the upload goes ahead.
125+ pub(crate) async fn storage_refused(&self, namespace: &str, adding: u64) -> worker::Result<Option<String>> {
126+ let cap = self.limits.free_actions_storage_bytes;
127+ if cap == 0 {
128+ return Ok(None);
129+ }
130+ #[derive(serde::Deserialize)]
131+ struct Held {
132+ bytes: Option<f64>,
133+ }
134+ let held = self
135+ .db
136+ .prepare(
137+ "SELECT SUM(size) AS bytes FROM (
138+ SELECT size FROM cache_entries WHERE namespace = ?1 AND status IN ('ready', 'pending')
139+ UNION ALL SELECT size FROM artifacts WHERE namespace = ?1 AND status IN ('ready', 'pending')
140+ )",
141+ )
142+ .bind(&[namespace.into()])?
143+ .first::<Held>(None)
144+ .await?
145+ .and_then(|held| held.bytes)
146+ .unwrap_or(0.0) as u64;
147+ let Some(why) = storage_refusal(held, adding, cap) else {
148+ return Ok(None);
149+ };
150+ Ok(match self.standing(namespace).await {
151+ Some(standing) if !standing.has_plan => Some(why),
152+ _ => None,
153+ })
154+ }
155+}
156+
157+#[cfg(test)]
158+mod storage_tests {
159+ use super::storage_refusal;
160+
161+ const GB: u64 = 1024 * 1024 * 1024;
162+
163+ #[test]
164+ fn a_free_workspace_keeps_two_gigabytes_of_artifacts_and_cache() {
165+ assert_eq!(storage_refusal(GB, GB, 2 * GB), None);
166+ let refused = storage_refusal(2 * GB - 10, 100, 2 * GB).unwrap();
167+ assert!(refused.contains("of the 2 GB"), "{refused}");
168+ assert!(refused.contains("start the g1t plan"), "{refused}");
169+ assert_eq!(storage_refusal(100 * GB, GB, 0), None);
170+ }
171+}
172+
107173 #[cfg(test)]
108174 mod room_tests {
109175 use super::self_hosted_room;
+3−0
191191 format!("A run's artifacts are at most {} MB together, and this run's hold {} MB.", RUN_ARTIFACTS_MAX_BYTES / 1_048_576, held / 1_048_576),
192192 ));
193193 }
194+ if let Some(why) = self.storage_refused(&job.namespace, a.size).await? {
195+ return Ok(fail(FailureCode::Limit, why));
196+ }
194197 let existing = self
195198 .artifact_rows("artifacts.run_id = ? AND artifacts.name = ? AND artifacts.status <> 'expired'", &[job.run_id.as_str().into(), a.name.as_str().into()])
196199 .await?;

Comments

Sign in to comment.