Skip to content

Rewrite it in Zig#436

Open
gaffneyc wants to merge 1 commit intomainfrom
zig
Open

Rewrite it in Zig#436
gaffneyc wants to merge 1 commit intomainfrom
zig

Conversation

@gaffneyc
Copy link
Copy Markdown
Member

@gaffneyc gaffneyc commented Mar 10, 2026

Rust is so 2024. Let's rewrite it all in Zig.

Entirely written by the ghost I have trapped in a jar. Dictated but not read.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the previous Rails implementation of Buildlight with a Zig-based server, including HTTP handlers, WebSocket broadcasting, database access/migrations, and static/template rendering. It also removes the Ruby/Node toolchain and updates deployment/CI configuration accordingly.

Changes:

  • Add Zig server implementation (HTTP routes, webhook parsers, DB models/migrations, WebSocket hub, external triggers).
  • Replace Rails views/assets pipeline with static public/ assets and simple HTML templates (embedded in release builds).
  • Remove Rails/RSpec/Node-related code and update Dockerfile, Fly.io config, and GitHub Actions CI to build/test Zig.

Reviewed changes

Copilot reviewed 134 out of 154 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
vendor/javascript/.keep Retains vendor directory placeholder.
templates/red.html Adds simple template placeholder for failing projects page.
templates/layout.html Adds main HTML layout template for the traffic light UI.
src/websocket.zig Implements WebSocket hub, client subscriptions, and broadcast helpers.
src/triggers.zig Adds outbound webhook + Particle.io trigger functions.
src/templates.zig Implements template loading (embedded vs disk) and basic string substitution renderer.
src/parsers.zig Adds webhook parsers (GitHub/Travis/CircleCI) + DB upserts + device update fanout; includes Zig tests.
src/models.zig Adds DB model types and query/update helpers for statuses/devices and color aggregation.
src/migrations/001_initial_schema.sql Adds initial SQL schema migration for statuses/devices.
src/main.zig Adds Zig application entrypoint, route registration, and graceful shutdown.
src/handlers.zig Adds HTTP handlers for health, static files, webhooks, colors, devices, API endpoints, and RYG streaming.
src/db.zig Adds DB pool initialization and embedded migration runner.
spec/support/json_helpers.rb Removes Rails/RSpec JSON helper module.
spec/spec_helper.rb Removes RSpec configuration.
spec/requests/circle_webhooks_spec.rb Removes Rails request specs (CircleCI webhooks).
spec/rails_helper.rb Removes Rails test helper.
spec/models/status_spec.rb Removes Rails model specs for Status.
spec/models/device_spec.rb Removes Rails model specs for Device.
spec/interactors/trigger_webhook_spec.rb Removes Rails interactor specs (webhook trigger).
spec/interactors/parse_travis_spec.rb Removes Rails interactor specs (Travis parsing).
spec/interactors/parse_github_spec.rb Removes Rails interactor specs (GitHub parsing).
spec/interactors/parse_circle_spec.rb Removes Rails interactor specs (CircleCI parsing).
spec/fixtures/travis.json Removes Rails test fixture.
spec/fixtures/github.json Removes Rails test fixture.
spec/fixtures/circle_pr.json Removes Rails test fixture.
spec/fixtures/circle.json Removes Rails test fixture.
spec/factories.rb Removes FactoryBot factories.
spec/controllers/webhooks_controller_spec.rb Removes Rails controller specs (webhooks).
spec/controllers/devices_controller_spec.rb Removes Rails controller specs (devices).
spec/controllers/colors_controller_spec.rb Removes Rails controller specs (colors).
spec/controllers/api/red_controller_spec.rb Removes Rails controller specs (API red).
spec/controllers/api/devices_controller_spec.rb Removes Rails controller specs (API devices).
public/websocket.js Adds vanilla JS WebSocket client to subscribe and update page state/favicon.
public/application.css Adds compiled CSS stylesheet to replace the prior Sass pipeline.
package.json Removes Node/Sass build configuration.
package-lock.json Removes npm lockfile.
log/.gitkeep Retains log directory placeholder.
lib/tasks/.gitkeep Retains legacy lib/tasks placeholder.
lib/assets/.gitkeep Retains legacy lib/assets placeholder.
fly.toml Updates Fly.io deploy config to run Zig migration command and serve /public statics from /app/public.
db/seeds.rb Removes Rails seed file.
db/schema.rb Removes Rails schema.rb (now using embedded SQL migrations).
db/migrate/20230311115915_add_workflow_to_statues.rb Removes Rails migration file.
db/migrate/20230305131208_add_status_to_devices.rb Removes Rails migration file.
db/migrate/20230304144230_make_identifier_nullable_on_devices.rb Removes Rails migration file.
db/migrate/20230304135152_add_slug_to_devices.rb Removes Rails migration file.
db/migrate/20230303181951_add_webhook_url_to_devices.rb Removes Rails migration file.
db/migrate/20161012193415_add_service_to_status.rb Removes Rails migration file.
db/migrate/20160510213407_add_name_to_devices.rb Removes Rails migration file.
db/migrate/20160510212722_add_identifier_to_devices.rb Removes Rails migration file.
db/migrate/20160510201736_create_devices.rb Removes Rails migration file.
db/migrate/20121124190606_add_user_to_statuses.rb Removes Rails migration file.
db/migrate/20121123195427_split_colors_on_statuses.rb Removes Rails migration file.
db/migrate/20121123182506_rename_status_to_color_on_statuses.rb Removes Rails migration file.
db/migrate/20121123172057_add_payload_to_statuses.rb Removes Rails migration file.
db/migrate/20121123160543_create_statuses.rb Removes Rails migration file.
config/storage.yml Removes Rails ActiveStorage config.
config/secrets.yml Removes Rails secrets.yml.
config/routes.rb Removes Rails routes.
config/puma.rb Removes Puma config (now Zig server).
config/locales/en.yml Removes Rails i18n locale file.
config/initializers/wrap_parameters.rb Removes Rails initializer.
config/initializers/session_store.rb Removes Rails initializer.
config/initializers/permissions_policy.rb Removes Rails initializer.
config/initializers/particle.rb Removes Rails initializer.
config/initializers/new_framework_defaults_8_0.rb Removes Rails defaults initializer.
config/initializers/new_framework_defaults_5_2.rb Removes Rails defaults initializer.
config/initializers/mime_types.rb Removes Rails initializer.
config/initializers/locale.rb Removes Rails initializer.
config/initializers/inflections.rb Removes Rails initializer.
config/initializers/filter_parameter_logging.rb Removes Rails initializer.
config/initializers/cookies_serializer.rb Removes Rails initializer.
config/initializers/content_security_policy.rb Removes Rails initializer.
config/initializers/backtrace_silencers.rb Removes Rails initializer.
config/initializers/assets.rb Removes Rails initializer.
config/initializers/application_controller_renderer.rb Removes Rails initializer.
config/importmap.rb Removes Rails importmap config.
config/environments/test.rb Removes Rails environment config.
config/environments/production.rb Removes Rails environment config.
config/environments/development.rb Removes Rails environment config.
config/environment.rb Removes Rails bootstrapping.
config/dockerfile.yml Removes dockerfile-rails config.
config/database.yml Removes Rails database.yml.
config/cable.yml Removes Rails ActionCable config.
config/boot.rb Removes Rails boot file.
config/application.rb Removes Rails application config.
config/application.example.yml Removes Rails example config file.
build.zig.zon Adds Zig package manifest (httpz/pg dependencies).
build.zig Adds Zig build steps for exe + tests and embed_assets option wiring.
bin/update Removes Rails update script.
bin/thrust Removes Rails thruster wrapper.
bin/setup Removes Rails setup script.
bin/rubocop Removes RuboCop wrapper script.
bin/rake Removes rake wrapper script.
bin/rails Removes rails runner script.
bin/importmap Removes importmap runner script.
bin/docker-entrypoint Removes Rails docker entrypoint.
bin/dev Removes Rails dev Procfile runner.
bin/bundle Removes bundler wrapper script.
app/views/layouts/application.html.erb Removes Rails layout view.
app/views/colors/index.html.erb Removes Rails colors index view.
app/views/api/red/show.html.erb Removes Rails API red view.
app/models/status.rb Removes Rails Status model.
app/models/device.rb Removes Rails Device model.
app/models/application_record.rb Removes Rails ApplicationRecord.
app/models/.gitkeep Leaves placeholder after removing Rails models.
app/mailers/.gitkeep Leaves placeholder after removing Rails mailers.
app/javascript/channels/index.js Removes ActionCable JS entry.
app/javascript/channels/consumer.js Removes ActionCable consumer setup.
app/javascript/channels/colors_channel.js Removes ActionCable channel client.
app/javascript/application.js Removes Rails JS entrypoint.
app/interactors/trigger_webhook.rb Removes Rails webhook trigger interactor.
app/interactors/trigger_particle.rb Removes Rails Particle trigger interactor.
app/interactors/parse_travis.rb Removes Rails Travis parser interactor.
app/interactors/parse_github.rb Removes Rails GitHub parser interactor.
app/interactors/parse_circle.rb Removes Rails CircleCI parser interactor.
app/helpers/color_helper.rb Removes Rails helper for body attrs/favicon.
app/controllers/webhooks_controller.rb Removes Rails controller (webhooks).
app/controllers/devices_controller.rb Removes Rails controller (devices).
app/controllers/colors_controller.rb Removes Rails controller (colors/ryg streaming).
app/controllers/application_controller.rb Removes Rails base controller.
app/controllers/api/red_controller.rb Removes Rails API controller.
app/controllers/api/devices_controller.rb Removes Rails API controller.
app/controllers/api/application_controller.rb Removes Rails API base controller.
app/channels/device_channel.rb Removes Rails ActionCable channel.
app/channels/colors_channel.rb Removes Rails ActionCable channel.
app/channels/application_cable/connection.rb Removes Rails ActionCable connection class.
app/channels/application_cable/channel.rb Removes Rails ActionCable channel base class.
app/assets/stylesheets/components/_message.scss Removes Sass source (messages).
app/assets/stylesheets/components/_light.scss Removes Sass source (light container).
app/assets/stylesheets/components/_bulb.scss Removes Sass source (bulb).
app/assets/stylesheets/base/_variables.scss Removes Sass source (variables).
app/assets/stylesheets/base/_reset.scss Removes Sass source (reset).
app/assets/stylesheets/base/_keyframes.scss Removes Sass source (keyframes).
app/assets/stylesheets/base/_elements.scss Removes Sass source (elements).
app/assets/stylesheets/base/_base.scss Removes Sass source (base imports).
app/assets/stylesheets/application.sass.scss Removes Sass entrypoint.
app/assets/config/manifest.js Removes Rails asset manifest.
app/assets/builds/.keep Keeps legacy builds placeholder.
app.json Removes Heroku app.json.
README.md Updates documentation for Zig development, testing, migrations, deployment, and API/WebSocket protocol.
Procfile.dev Removes Rails dev Procfile.
Procfile Removes Rails Procfile.
Gemfile.lock Removes Ruby dependency lockfile.
Gemfile Removes Ruby dependencies.
Dockerfile Replaces Rails image build with Zig multi-stage build producing a static binary + public assets.
.travis.yml Removes legacy Travis CI config.
.standard.yml Removes Ruby lint config.
.ruby-version Removes Ruby version pin.
.rspec Removes RSpec config.
.node-version Removes Node version pin.
.mise.toml Adds Zig toolchain pin via mise.
.gitignore Updates ignore rules for Zig build artifacts.
.github/workflows/ci.yml Updates CI to use Postgres 16 and build/test via Zig.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +100
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload_str, .{}) catch
return error.InvalidJson;
defer parsed.deinit();
const root = parsed.value.object;

// Ignore pull requests
if (std.mem.eql(u8, getStringOr(root, "type", ""), "pull_request")) return;
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

parseTravis assumes the parsed JSON root is an object (parsed.value.object). For non-object JSON payloads this is unsafe; validate via switch (parsed.value) before accessing object fields and return a structured error for invalid shapes.

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +154
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return error.InvalidJson;
defer parsed.deinit();
const root = parsed.value.object;

// Only handle workflow-completed
if (!std.mem.eql(u8, getStringOr(root, "type", ""), "workflow-completed")) return;

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

parseCircle assumes the parsed JSON root is an object (parsed.value.object). For non-object JSON payloads this is unsafe; validate the root value tag before accessing .object and treat mismatches as invalid payloads.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +109
.string => |channel| {
// Store a copy of the channel name
const owned = std.heap.page_allocator.dupe(u8, channel) catch return;
self.subscriptions.put(owned, {}) catch {};
},
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

In clientMessage subscribe handling, owned is allocated and then put errors are ignored. If put fails (e.g., OOM) the allocated owned slice is leaked; repeated subscribes to an existing channel may also allocate unnecessarily. Free owned on failure and avoid allocating when already subscribed.

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +320
// Broadcast device colors to WebSocket clients
if (device.slug.len > 0) {
var channel_buf: [256]u8 = undefined;
const channel = std.fmt.bufPrint(&channel_buf, "device:{s}", .{device.slug}) catch return;
hub.broadcastColors(channel, colors);
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

updateDeviceStatus only broadcasts device updates on device:{device.slug}. If someone visits /devices/:id using the UUID (which deviceShow accepts), the browser subscribes to device:{uuid} and will never receive live updates. Broadcast on both slug and id channels (or make the client subscribe to the canonical slug).

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
Like this?
<a href="https://github.com/collectiveidea/buildlight">Fork it on Github</a>.
</p>
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Link text uses "Github"; standard capitalization is "GitHub".

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +51
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return error.InvalidJson;
defer parsed.deinit();
const root = parsed.value.object;

const repo_str = try getString(root, "repository");
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

parseGithub assumes the parsed JSON root is an object (parsed.value.object). If the payload is valid JSON but not an object (e.g., []), this is unsafe and may misbehave/crash. Switch on parsed.value and return error.InvalidJson/error.InvalidField when it isn’t an object.

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +398
try writer.writeAll("{\"username\":\"");
try writer.writeAll(s.username);
try writer.writeAll("\",\"project_name\":\"");
try writer.writeAll(s.project_name);
try writer.writeAll("\"}");
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The apiRedShow JSON response is built by concatenating s.username and s.project_name directly into string literals without JSON escaping. If an attacker controls these fields, they can break the JSON structure or inject additional keys/values, which can mislead or compromise downstream consumers that trust this data. Use a JSON encoder or at minimum escape quotes, backslashes, and control characters before writing untrusted strings into JSON string values.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +186
pub fn webhookCreate(handler: *Handler, req: *httpz.Request, res: *httpz.Response) !void {
const content_type = req.header("content-type") orelse "";

// Travis CI sends form-encoded payload
if (std.mem.startsWith(u8, content_type, "application/x-www-form-urlencoded")) {
const form_data = try req.formData();
var it = form_data.iterator();
while (it.next()) |kv| {
if (std.mem.eql(u8, kv.key, "payload")) {
parsers.parseTravis(handler.pool, handler.hub, kv.value, handler.host, handler.particle_token, handler.debug, handler.allocator) catch |err| {
std.log.err("ParseTravis error: {}", .{err});
};
res.status = 200;
return;
}
}
}

// Read JSON body
const body = req.body() orelse {
res.status = 400;
return;
};

// Check for CircleCI (has Circleci-Event-Type header)
if (req.header("circleci-event-type") != null) {
parsers.parseCircle(handler.pool, handler.hub, body, handler.host, handler.particle_token, handler.debug, handler.allocator) catch |err| {
std.log.err("ParseCircle error: {}", .{err});
};
res.status = 200;
return;
}

// GitHub Actions sends JSON with "repository" containing "owner/repo"
const parsed = std.json.parseFromSlice(std.json.Value, res.arena, body, .{}) catch {
res.status = 400;
return;
};
defer parsed.deinit();

const is_github = switch (parsed.value) {
.object => |obj| if (obj.get("repository")) |repo| switch (repo) {
.string => |s| std.mem.indexOf(u8, s, "/") != null,
else => false,
} else false,
else => false,
};

if (!is_github) {
res.status = 400;
return;
}

parsers.parseGithub(handler.pool, handler.hub, body, handler.host, handler.particle_token, handler.debug, handler.allocator) catch |err| {
std.log.err("ParseGithub error: {}", .{err});
};
res.status = 200;
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The webhook endpoint webhookCreate accepts Travis, CircleCI, and GitHub payloads based only on headers/content type and never verifies a shared secret or HMAC signature. This allows anyone on the internet to POST forged payloads that create or update build statuses and trigger downstream device webhooks/Particle events, spoofing CI results. Add per-service authentication (e.g., GitHub’s X-Hub-Signature-256, secret tokens, or Basic auth) and validate it before dispatching to the parser functions.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +6
# Install Zig
RUN apt-get update && apt-get install -y curl xz-utils && \
curl -L https://ziglang.org/download/0.15.2/zig-x86_64-linux.tar.xz | tar xJ && \
mv zig-* /opt/zig
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The Dockerfile downloads the Zig toolchain via curl ... | tar from ziglang.org without any checksum or signature verification. If the download endpoint or TLS channel is compromised, an attacker could supply a malicious compiler that injects backdoors into the built buildlight binary. Pin the archive with a strong hash or use a base image or package manager that provides integrity verification for Zig.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +81
for (statuses) |s| {
try list.appendSlice(allocator, " <li>");
try list.appendSlice(allocator, s.project_name);
try list.appendSlice(allocator, "</li>\n");
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The red-projects template builds HTML by concatenating raw s.project_name into a <li> without any HTML escaping. If an attacker can control a project name (for example via a monitored CI repository), they can inject arbitrary HTML/JavaScript into the response and execute code in users’ browsers. Ensure project_name is properly HTML-escaped or rendered via a safe templating helper before being inserted into the page.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants