Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b92a09a
The window cuts a long time left to its cell and draws nothing under …
AbrahamGeorge8547 Oct 30, 2026
6785c78
Merge ui-time-left-overlap into dev: no text under a long time left (…
AbrahamGeorge8547 Oct 30, 2026
2f976ee
AGENTS.md: the MCP server is named loadtable
AbrahamGeorge8547 Oct 30, 2026
0687d3e
loadtable gc: what each checkout's build output takes on every machin…
AbrahamGeorge8547 Oct 30, 2026
4670e2c
Merge gc into dev: loadtable gc frees merged, gone and old build outp…
AbrahamGeorge8547 Oct 30, 2026
ed3d7c7
The window's Disk tab: gc per machine and checkout, tick rows and fre…
AbrahamGeorge8547 Oct 30, 2026
3471dc5
Merge gc-ui into dev: the window's Disk tab and gc --free (R-840)
AbrahamGeorge8547 Oct 30, 2026
7a1e9b1
Release on merge to main: an untagged version is built, tagged and dr…
AbrahamGeorge8547 Oct 30, 2026
0f07eeb
Merge release-on-main into dev: merging to main releases an untagged …
AbrahamGeorge8547 Oct 30, 2026
c7129b7
Homebrew dirs on macOS job PATHs; sandbox tests skip inside a sandbox…
AbrahamGeorge8547 Oct 30, 2026
1aafc21
Merge small-fixes into dev: Homebrew PATH on macOS, sandbox tests ins…
AbrahamGeorge8547 Oct 30, 2026
078cd98
Disk tab: totals after freeing are what is left; an idle Free button …
AbrahamGeorge8547 Oct 30, 2026
8ac8222
Merge disk-after-free into dev: totals after freeing, greyed idle Fre…
AbrahamGeorge8547 Oct 30, 2026
be7adae
Version 0.1.4 and its release notes: gc and the Disk tab, plus what 0…
AbrahamGeorge8547 Oct 30, 2026
f9dd767
Merge release-0.1.4 into dev: version 0.1.4 and its release notes
AbrahamGeorge8547 Oct 30, 2026
e26effd
Drop a borrow clippy 1.99 flags in gc's unknown-checkout name
AbrahamGeorge8547 Oct 30, 2026
e0f840c
Merge clippy-1.99 into dev: CI's newer clippy passes
AbrahamGeorge8547 Oct 30, 2026
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
51 changes: 40 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
name: Release builds

# Pushing a vX.Y.Z tag builds the binaries for every published target and drafts the GitHub release
# with them. Publishing that draft (by hand, after testing) triggers publish-r2.yml.
# A merge to main whose workspace version has no vX.Y.Z tag yet builds the binaries for every
# published target, tags that commit and drafts the GitHub release with them; when the tag exists
# the run stops after the version check. Pushing a vX.Y.Z tag by hand does the same for that tag.
# Publishing the draft (by hand, after testing) triggers publish-r2.yml.
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:

Expand All @@ -22,29 +25,50 @@ jobs:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.version }}
# true when this run builds and drafts a release
release: ${{ steps.version.outputs.release }}
steps:
- uses: actions/checkout@v4
- id: version
name: Validate release version
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
python3 - <<'PY'
# on main, a version already tagged is already released (or being released): stop here
tagged=false
if [ "$GITHUB_REF_TYPE" = branch ]; then
version=$(python3 -c "import tomllib; print(tomllib.load(open('Cargo.toml', 'rb'))['workspace']['package']['version'])")
if gh api "repos/$GH_REPO/git/ref/tags/v$version" >/dev/null 2>&1; then
tagged=true
echo "v$version is already tagged; nothing to release."
fi
fi
TAGGED=$tagged python3 - <<'PY'
import os, re, tomllib
with open('Cargo.toml', 'rb') as f:
version = tomllib.load(f)['workspace']['package']['version']
if not re.fullmatch(r'\d+\.\d+\.\d+', version):
raise SystemExit('Only stable X.Y.Z versions are released')
if os.environ['GITHUB_REF_TYPE'] == 'tag' and os.environ['GITHUB_REF_NAME'] != f'v{version}':
raise SystemExit(f'Tag {os.environ["GITHUB_REF_NAME"]} does not match the workspace version {version}')
notes = f'docs/releases/v{version}.md'
if os.environ['GITHUB_REF_TYPE'] == 'tag' and not (os.path.isfile(notes) and os.path.getsize(notes)):
raise SystemExit(f'No release notes for v{version}: add {notes}')
ref_type, ref_name = os.environ['GITHUB_REF_TYPE'], os.environ['GITHUB_REF_NAME']
pushed_main = os.environ['GITHUB_EVENT_NAME'] == 'push' and ref_type == 'branch' and ref_name == 'main'
release = ref_type == 'tag' or (pushed_main and os.environ['TAGGED'] != 'true')
Comment on lines +53 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add requirement tracking and tests for automatic releases

The release workflow now automatically tags and drafts an untagged workspace version on a push to main, but this changed behavior has neither a requirement row nor a requirement-named test. Repository-wide searches for the release workflow and its new gating logic found only the workflow and release documentation; the added R-838–R-844 rows cover other changes. AGENTS.md requires both tracking and a named test for new or changed behavior. Add coverage for an untagged main push, an already-tagged version, and a manual branch run before relying on this release path.

AGENTS.md reference: AGENTS.md:L32-L34

Useful? React with 👍 / 👎.

if release:
if not re.fullmatch(r'\d+\.\d+\.\d+', version):
raise SystemExit('Only stable X.Y.Z versions are released')
if ref_type == 'tag' and ref_name != f'v{version}':
raise SystemExit(f'Tag {ref_name} does not match the workspace version {version}')
notes = f'docs/releases/v{version}.md'
if not (os.path.isfile(notes) and os.path.getsize(notes)):
raise SystemExit(f'No release notes for v{version}: add {notes} and merge it to main')
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'version={version}\n')
f.write(f'release={str(release).lower()}\n')
PY

build:
needs: version
# a manual run on a branch builds without tagging or drafting, to try the build
if: needs.version.outputs.release == 'true' || github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -113,7 +137,7 @@ jobs:
if-no-files-found: error

draft-release:
if: github.ref_type == 'tag'
if: needs.version.outputs.release == 'true'
needs: [version, build]
runs-on: ubuntu-24.04
permissions:
Expand All @@ -133,6 +157,11 @@ jobs:
TAG: v${{ needs.version.outputs.version }}
run: |
(cd artifacts && sha256sum *.tar.gz > SHA256SUMS && cat SHA256SUMS)
# a merge to main tags the commit it built; a pushed tag is already there
if [ "$GITHUB_REF_TYPE" = branch ]; then
gh api "repos/$GH_REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$GITHUB_SHA" >/dev/null
echo "Tagged $GITHUB_SHA as $TAG"
fi
if gh release view "$TAG" >/dev/null 2>&1; then
test "$(gh release view "$TAG" --json isDraft --jq .isDraft)" = true || {
echo 'Refusing to replace the binaries of a published release'; exit 1;
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ are gone from the machines: never run `tasks2`, `tasks2d` or the retired v1 `tas
## Builds and tests go through LoadTable itself

Run every cargo build, test and clippy through the installed `loadtable`
(`~/.local/bin/loadtable`; Claude sessions can use the LoadTable MCP tools, server `tasks2`),
(`~/.local/bin/loadtable`; Claude sessions can use the LoadTable MCP tools, server `loadtable`, tools `mcp__loadtable__*`),
never bare `cargo` in a shell:

```sh
Expand Down
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "0.1.3"
version = "0.1.4"
edition = "2024"
rust-version = "1.92"
license = "MIT"
Expand Down
19 changes: 19 additions & 0 deletions crates/tasks-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,25 @@ pub enum Command {
/// Build caches: the compiler wrapper in use, the cache root and its disk against the
/// reserve. Reads the machine's files; needs no daemon.
Cache,
/// What each checkout's build output takes here and on the peers, and freeing it. Only
/// build output and peers' copies are deleted, never a working tree, and never anything
/// a queued or running job uses.
Gc {
/// Free the checkouts in these states, comma separated: merged, gone, old, idle,
/// active. Without it gc only reports.
#[arg(long, value_name = "STATES", value_delimiter = ',')]
delete: Vec<String>,
/// Free these checkouts too, whatever their state: a path, or the name the report
/// shows (`(unknown checkout …)` included). Repeat it for more.
#[arg(long, value_name = "CHECKOUT")]
free: Vec<String>,
/// Days without a job after which a checkout counts as idle (default 14).
#[arg(long, value_name = "DAYS")]
idle_days: Option<u32>,
/// The daemon's reply as JSON.
#[arg(long)]
json: bool,
},
/// This machine's identity.
Identity {
/// What to do.
Expand Down
62 changes: 60 additions & 2 deletions crates/tasks-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use std::time::Duration;
use tasks_core::plan::Plan;
use tasks_core::{DeviceId, JobId, JobSpec, Kind, Owner, Phase, Placement, Priority};
use tasks_daemon::api::{
ErrorCode, HistoryView, JobView, LogSearchQuery, LogView, MAX_CONTEXT, MAX_TAIL_LINES, Request,
Response, StatusSnapshot, WaiterView,
ErrorCode, GcState, HistoryView, JobView, LogSearchQuery, LogView, MAX_CONTEXT, MAX_TAIL_LINES,
Request, Response, StatusSnapshot, WaiterView,
};
use tasks_policy::PolicyFs;
use tasks_policy::submit_rules::{self, PlacementClass};
Expand Down Expand Up @@ -96,6 +96,12 @@ fn dispatch(ctx: &mut Ctx<'_>, cmd: &Command) -> Result<i32, CliError> {
Command::LogSearch(a) => log_search(ctx, a),
Command::Get { id, json } => get(ctx, *id, *json),
Command::Metrics { id, json } => metrics(ctx, *id, *json),
Command::Gc {
delete,
free,
idle_days,
json,
} => gc(ctx, delete, free, *idle_days, *json),
Command::Cancel { id, who, reason } => cancel(ctx, *id, who.as_deref(), reason.as_deref()),
Command::Prioritize {
id,
Expand Down Expand Up @@ -611,6 +617,58 @@ fn metrics(ctx: &mut Ctx<'_>, id: i64, json: bool) -> Result<i32, CliError> {
Ok(EXIT_OK)
}

/// `gc`: what each checkout keeps on every machine's build cache, and with `--delete` frees the
/// rows in those states (R-839), with `--free` the checkouts named (R-840).
fn gc(
ctx: &mut Ctx<'_>,
delete: &[String],
free: &[String],
idle_days: Option<u32>,
json: bool,
) -> Result<i32, CliError> {
let mut states = Vec::new();
for word in delete.iter().filter(|w| !w.trim().is_empty()) {
match GcState::parse(word) {
Some(GcState::InUse) => {
return Err(CliError::Usage(
"gc never deletes a checkout a job uses; leave in_use out of --delete".into(),
));
}
Some(s) => states.push(s),
None => {
return Err(CliError::Usage(format!(
"--delete takes merged, gone, old, idle or active, not {word:?}"
)));
}
}
}
// a path on disk goes as its full form; a report name like `(unknown checkout …)` as is
let checkouts = free
.iter()
.map(|c| match std::fs::canonicalize(c) {
Ok(p) if !c.starts_with('(') => p.display().to_string(),
_ => c.clone(),
})
.collect();
let resp = ctx.client.call(&Request::Gc {
delete: states,
checkouts,
idle_days,
})?;
let Response::Gc(v) = resp else {
return Err(CliError::Failed(format!(
"unexpected reply to gc: {resp:?}"
)));
};
if json {
writeln!(ctx.out, "{}", pretty(&v)?)?;
} else {
ctx.out
.write_all(render::gc(&v, (ctx.now_ms)()).as_bytes())?;
}
Ok(EXIT_OK)
}

/// `wait`: blocks until every job is over, sending only read-only requests (R-720). As each job
/// ends it prints the job's last log lines and its result line; the exit code is the first
/// failed job's, as `run` would give it (R-721). `--timeout` gives up with 124 and touches
Expand Down
Loading
Loading