Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 14 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# CI: the offline verifier every push has to clear.
#
# Deliberately not running ziglint — it is a `zig build lint` step that shells
# out to a binary this workflow would have to build from source on every run,
# and it is advisory. `zig fmt --check` and the unit suite are the floors.
#
# The online suite (`zig build online`) is not here on purpose: it needs a real
# LINEAR_API_KEY and mutates a live workspace.
name: ci
Expand Down Expand Up @@ -37,6 +33,20 @@ jobs:
- name: Version manifests agree
run: ./scripts/check-versions.sh

lint:
# ziglint runs through the flake dev shell, which is the only place the
# binary exists — `zig build lint` shells out to it and dies with
# FileNotFound otherwise. The tree is at zero findings; this is the floor
# that keeps it there.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix develop -c zig build lint

cross-build:
# The npm dist targets are cross-compiled and never exercised by `zig build
# test`, so a target-specific break (libc, tcsetattr, file modes) only shows
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Notable changes per release. Versions before 0.3.0 are recorded in the
attestations automatically.
- `flake.nix` provides a dev shell pinned to Zig 0.16.0 with `ziglint` and
`jq`, so `zig build lint` works instead of failing with `FileNotFound`.
- The 14 findings that surfaced the first time ziglint actually ran are fixed
(11 `deinit` bodies now poison the struct with `self.* = undefined`, plus one
`@This()` binding and two error-literal returns), and CI runs `zig build
lint` through the flake so the tree stays at zero.

## 0.3.0

Expand Down
3 changes: 1 addition & 2 deletions src/commands/bulk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ pub const Targets = struct {
pub fn deinit(self: *Targets) void {
self.allocator.free(self.items);
if (self.storage) |buf| self.allocator.free(buf);
self.items = &.{};
self.storage = null;
self.* = undefined;
}
};

Expand Down
1 change: 1 addition & 0 deletions src/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ pub const Config = struct {
self.allocator.free(entry.value_ptr.*);
}
self.team_cache.deinit();
self.* = undefined;
}

pub fn resolveApiKey(self: *Config, override_key: ?[]const u8) ![]const u8 {
Expand Down
11 changes: 8 additions & 3 deletions src/graphql_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub const GraphqlClient = struct {

pub fn deinit(self: *GraphqlClient) void {
shared_client.release(self.io);
self.* = undefined;
}

pub const Request = struct {
Expand Down Expand Up @@ -149,6 +150,10 @@ pub const GraphqlClient = struct {

pub fn deinit(self: *Response) void {
self.parsed.deinit();
// Every parsed field is a slice into `parsed`'s arena, so any read
// after this point was already use-after-free. Poisoning makes it
// crash instead of returning plausible bytes.
self.* = undefined;
}
};

Expand All @@ -171,19 +176,19 @@ pub const GraphqlClient = struct {
while (true) : (attempt += 1) {
response_writer.clearRetainingCapacity();

if (std.Io.Clock.real.now(self.io).toMilliseconds() >= deadline_ms) return Error.RequestTimedOut;
if (std.Io.Clock.real.now(self.io).toMilliseconds() >= deadline_ms) return error.RequestTimedOut;

const attempt_result = try performRequest(self, payload_bytes, &response_writer.writer);
rate_limit = attempt_result.rate_limit;
const status_code: u16 = attempt_result.status;

const after_ms: i64 = std.Io.Clock.real.now(self.io).toMilliseconds();
if (after_ms >= deadline_ms) return Error.RequestTimedOut;
if (after_ms >= deadline_ms) return error.RequestTimedOut;

const can_retry = shouldRetry(status_code) and attempt + 1 < max_attempts;
if (can_retry) {
const remaining_ms = deadline_ms - after_ms;
const delay_ms = computeDelayMs(attempt, rate_limit, remaining_ms, &random) orelse return Error.RequestTimedOut;
const delay_ms = computeDelayMs(attempt, rate_limit, remaining_ms, &random) orelse return error.RequestTimedOut;
logRetry(self.io, status_code, attempt + 2, max_attempts, delay_ms);
try self.io.sleep(.fromMilliseconds(@intCast(delay_ms)), .awake);
continue;
Expand Down
7 changes: 6 additions & 1 deletion src/tests/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9532,6 +9532,7 @@ const FakeProcess = struct {
self.calls.deinit(self.allocator);
for (self.inputs.items) |input| self.allocator.free(input);
self.inputs.deinit(self.allocator);
self.* = undefined;
}

fn runner(self: *FakeProcess) git.Runner {
Expand Down Expand Up @@ -12036,10 +12037,12 @@ test "bulk execute records every outcome and keeps running" {
const allocator = std.testing.allocator;

const Recorder = struct {
const Self = @This();

seen: *std.ArrayListUnmanaged([]const u8),
allocator: std.mem.Allocator,

fn call(self: @This(), index: usize, target: []const u8) !bulk.Outcome {
fn call(self: Self, index: usize, target: []const u8) !bulk.Outcome {
_ = index;
try self.seen.append(self.allocator, target);
return if (std.mem.eql(u8, target, "bad")) .failed else .succeeded;
Expand Down Expand Up @@ -12501,6 +12504,7 @@ const ChainResult = struct {
fn deinit(self: *ChainResult) void {
self.cfg.deinit();
self.diagnostics.deinit();
self.* = undefined;
}

fn stderrText(self: *ChainResult) []const u8 {
Expand Down Expand Up @@ -13224,6 +13228,7 @@ const ScratchConfigFixture = struct {
self.tmp.cleanup();
restoreEnv(env_name_z, self.saved_key_env, self.allocator);
restoreEnv(config_env_name_z, self.saved_config_env, self.allocator);
self.* = undefined;
}

fn readConfig(self: *ScratchConfigFixture) ![]u8 {
Expand Down
4 changes: 3 additions & 1 deletion src/tests/mock_graphql.zig
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub const MockServer = struct {
entry.value_ptr.*.deinit();
}
self.fixtures.deinit();
self.* = undefined;
}

pub fn set(self: *MockServer, operation: []const u8, payload: []const u8) !void {
Expand Down Expand Up @@ -244,6 +245,7 @@ pub const GraphqlClient = struct {

pub fn deinit(self: *Response) void {
self.parsed.deinit();
self.* = undefined;
}
};

Expand All @@ -257,7 +259,7 @@ pub const GraphqlClient = struct {
}

pub fn deinit(self: *GraphqlClient) void {
_ = self;
self.* = undefined;
}

pub fn send(self: *GraphqlClient, allocator: Allocator, req: Request) !Response {
Expand Down
1 change: 1 addition & 0 deletions src/tests/online.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const Env = struct {
if (self.issue_id) |value| allocator.free(value);
if (self.project_id) |value| allocator.free(value);
if (self.milestone_id) |value| allocator.free(value);
self.* = undefined;
}
};

Expand Down
Loading