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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ jobs:
- 'apps/gittensory-ui/**'
- 'apps/gittensory-miner-ui/**'
- 'apps/gittensory-extension/**'
- 'apps/gittensory-miner-extension/**'
- 'packages/gittensory-ui-kit/**'
- 'scripts/build-extension.mjs'
- 'scripts/build-miner-extension.mjs'
- 'package.json'
- 'package-lock.json'
# Backend changes that can drift the OpenAPI contract the UI type-checks against, but say
Expand Down Expand Up @@ -381,6 +383,12 @@ jobs:
- name: UI tests
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run ui:test
- name: Extension lint
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run extension:lint && npm run miner-extension:lint
- name: Extension typecheck
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run extension:typecheck && npm run miner-extension:typecheck
# `npm run ui:build` also regenerates apps/gittensory-ui/public/openapi.json (needed for a
# standalone build), but this step's trigger condition is a strict subset of "OpenAPI drift
# check" above (push || ui==true, vs. push || ui==true || uiContract==true), so whenever this
Expand All @@ -389,7 +397,7 @@ jobs:
# steps directly instead of the aggregate script, skipping that redundant regen.
- name: UI build
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build
run: npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build

# The full-suite coverage run, sharded (#ci-shard-coverage). This alone was ~9-10 of the ~11 minutes a
# typical backend PR spent in validate-code, because vitest schedules whole test FILES atomically to
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ui-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
- name: Validate frontend
env:
VITE_GITTENSORY_API_ORIGIN: https://gittensory-api.aethereal.dev
run: npm run ui:openapi:check && npm run ui:lint && npm run ui:typecheck && npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build
run: npm run ui:openapi:check && npm run ui:lint && npm run ui:typecheck && npm run extension:lint && npm run miner-extension:lint && npm run extension:typecheck && npm run miner-extension:typecheck && npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build
1 change: 1 addition & 0 deletions apps/gittensory-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
12 changes: 12 additions & 0 deletions apps/gittensory-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@jsonbored/gittensory-extension",
"version": "0.1.0",
"private": true,
"description": "Maintainer-facing browser extension overlay for GitHub pull requests and issues.",
"type": "module",
"scripts": {
"build": "node ../../scripts/build-extension.mjs",
"lint": "node --check auth.js && node --check background.js && node --check content.js && node --check options.js",
"typecheck": "npm run lint"
}
}
1 change: 1 addition & 0 deletions apps/gittensory-miner-extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
12 changes: 12 additions & 0 deletions apps/gittensory-miner-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@jsonbored/gittensory-miner-extension",
"version": "0.1.0",
"private": true,
"description": "Contributor-facing browser extension for GitHub issue opportunity signals.",
"type": "module",
"scripts": {
"build": "node ../../scripts/build-miner-extension.mjs",
"lint": "node --check background.js && node --check content.js && node --check opportunity-badge.js && node --check options.js && node --check toolbar-badge.js",
"typecheck": "npm run lint"
}
}
16 changes: 16 additions & 0 deletions package-lock.json

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

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@
"actionlint": "node scripts/actionlint.mjs",
"ui:dev": "npm run ui:preview",
"extension:build": "node scripts/build-extension.mjs",
"extension:lint": "npm --workspace @jsonbored/gittensory-extension run lint",
"extension:typecheck": "npm --workspace @jsonbored/gittensory-extension run typecheck",
"miner-extension:build": "npm --workspace @jsonbored/gittensory-miner-extension run build",
"miner-extension:lint": "npm --workspace @jsonbored/gittensory-miner-extension run lint",
"miner-extension:typecheck": "npm --workspace @jsonbored/gittensory-miner-extension run typecheck",
"ui:kit:build": "npm run build --workspace @jsonbored/gittensory-ui-kit",
"ui:build": "npm run ui:kit:build && npm run ui:openapi && npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build && npm --workspace @jsonbored/gittensory-miner-ui run build",
"ui:build": "npm run ui:kit:build && npm run ui:openapi && npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build && npm --workspace @jsonbored/gittensory-miner-ui run build",
"ui:preview": "npm run ui:build && wrangler dev --config apps/gittensory-ui/dist/server/wrangler.json --ip 127.0.0.1 --port 4173 --local",
"ui:lint": "npm run ui:kit:build && npm --workspace @jsonbored/gittensory-ui run lint && npm --workspace @jsonbored/gittensory-miner-ui run lint",
"ui:typecheck": "npm run ui:kit:build && npm --workspace @jsonbored/gittensory-ui run typecheck && npm --workspace @jsonbored/gittensory-miner-ui run typecheck",
Expand Down
118 changes: 118 additions & 0 deletions scripts/build-miner-extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const source = resolve(root, "apps/gittensory-miner-extension");
const outDir = resolve(source, "dist/package");
const crcTable = Array.from({ length: 256 }, (_, index) => {
let value = index;
for (let bit = 0; bit < 8; bit += 1) {
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
}
return value >>> 0;
});

const PACKAGE_FILES = [
"manifest.json",
"background.js",
"content.js",
"opportunity-badge.js",
"options.html",
"options.js",
"styles.css",
"toolbar-badge.js",
];

rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });

for (const file of PACKAGE_FILES) {
cpSync(resolve(source, file), resolve(outDir, file));
}

const zipPath = resolve(source, "dist/loopover-miner-extension.zip");
rmSync(zipPath, { force: true });
writeFileSync(zipPath, createStoredZip(outDir));

console.log(`wrote ${relative(root, zipPath)}`);

function createStoredZip(directory) {
const files = listFiles(directory);
const localParts = [];
const centralParts = [];
let offset = 0;

for (const file of files) {
const name = relative(directory, file).replaceAll("\\", "/");
const nameBuffer = Buffer.from(name);
const data = readFileSync(file);
const crc = crc32(data);
const localHeader = Buffer.alloc(30);
localHeader.writeUInt32LE(0x04034b50, 0);
localHeader.writeUInt16LE(10, 4);
localHeader.writeUInt16LE(0, 6);
localHeader.writeUInt16LE(0, 8);
localHeader.writeUInt16LE(0, 10);
localHeader.writeUInt16LE(33, 12);
localHeader.writeUInt32LE(crc, 14);
localHeader.writeUInt32LE(data.length, 18);
localHeader.writeUInt32LE(data.length, 22);
localHeader.writeUInt16LE(nameBuffer.length, 26);
localHeader.writeUInt16LE(0, 28);
localParts.push(localHeader, nameBuffer, data);

const centralHeader = Buffer.alloc(46);
centralHeader.writeUInt32LE(0x02014b50, 0);
centralHeader.writeUInt16LE(20, 4);
centralHeader.writeUInt16LE(10, 6);
centralHeader.writeUInt16LE(0, 8);
centralHeader.writeUInt16LE(0, 10);
centralHeader.writeUInt16LE(0, 12);
centralHeader.writeUInt16LE(33, 14);
centralHeader.writeUInt32LE(crc, 16);
centralHeader.writeUInt32LE(data.length, 20);
centralHeader.writeUInt32LE(data.length, 24);
centralHeader.writeUInt16LE(nameBuffer.length, 28);
centralHeader.writeUInt16LE(0, 30);
centralHeader.writeUInt16LE(0, 32);
centralHeader.writeUInt16LE(0, 34);
centralHeader.writeUInt16LE(0, 36);
centralHeader.writeUInt32LE(0, 38);
centralHeader.writeUInt32LE(offset, 42);
centralParts.push(centralHeader, nameBuffer);

offset += localHeader.length + nameBuffer.length + data.length;
}

const centralDirectory = Buffer.concat(centralParts);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(0, 4);
end.writeUInt16LE(0, 6);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(centralDirectory.length, 12);
end.writeUInt32LE(offset, 16);
end.writeUInt16LE(0, 20);

return Buffer.concat([...localParts, centralDirectory, end]);
}

function listFiles(directory) {
return readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const path = resolve(directory, entry.name);
return entry.isDirectory() ? listFiles(path) : [path];
})
.filter((path) => statSync(path).isFile())
.sort();
}

function crc32(buffer) {
let value = 0xffffffff;
for (const byte of buffer) {
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
}
return (value ^ 0xffffffff) >>> 0;
}
2 changes: 1 addition & 1 deletion scripts/mcp-release-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const CLIENT_VISIBLE_PATHS = [
const SUPPORTING_VISIBLE_PATHS = ["src/openapi/schemas.ts", "src/openapi/spec.ts"];

const GENERATED_OPENAPI_PATHS = ["apps/gittensory-ui/public/openapi.json", "src/openapi/spec.ts"];
const UI_ONLY_PREFIXES = ["apps/gittensory-ui/", "apps/gittensory-extension/"];
const UI_ONLY_PREFIXES = ["apps/gittensory-ui/", "apps/gittensory-extension/", "apps/gittensory-miner-extension/"];
const RELEASE_SCOPES = new Set(["release", "changelog"]);
const EXCLUDED_SCOPES = new Set(["pwa", "ui", "extension", "github-agent", "sync", "upstream"]);
const GROUP_ORDER = ["Features", "Fixes", "Security", "CI", "Build", "Docs", "Tests", "Refactors", "Dependencies", "Chores", "Reverts"];
Expand Down
37 changes: 37 additions & 0 deletions test/unit/ci-extension-packages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";

const read = (path: string) => readFileSync(path, "utf8");

describe("browser extension workspace packages (#4866)", () => {
it("declares workspace package.json files for both extensions", () => {
const maintainer = JSON.parse(read("apps/gittensory-extension/package.json"));
const miner = JSON.parse(read("apps/gittensory-miner-extension/package.json"));

expect(maintainer.name).toBe("@jsonbored/gittensory-extension");
expect(miner.name).toBe("@jsonbored/gittensory-miner-extension");
expect(maintainer.scripts.build).toContain("build-extension.mjs");
expect(miner.scripts.build).toContain("build-miner-extension.mjs");
expect(miner.scripts.lint).toContain("node --check");
expect(miner.scripts.typecheck).toBe("npm run lint");
});

it("wires extension lint/typecheck/build scripts into root package.json", () => {
const pkg = JSON.parse(read("package.json"));
expect(pkg.scripts["extension:lint"]).toContain("@jsonbored/gittensory-extension");
expect(pkg.scripts["miner-extension:build"]).toContain("@jsonbored/gittensory-miner-extension");
expect(pkg.scripts["ui:build"]).toContain("miner-extension:build");
});

it("includes both extensions in ci.yml's ui path filter and validate-code steps", () => {
const workflow = read(".github/workflows/ci.yml");
expect(workflow).toContain("apps/gittensory-miner-extension/**");
expect(workflow).toContain("scripts/build-miner-extension.mjs");
expect(workflow).toContain("name: Extension lint");
expect(workflow).toContain("npm run extension:lint && npm run miner-extension:lint");
expect(workflow).toContain("npm run extension:typecheck && npm run miner-extension:typecheck");
expect(workflow).toContain(
"npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build",
);
});
});
6 changes: 4 additions & 2 deletions test/unit/ci-ui-build-openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ describe("UI build steps skip the redundant OpenAPI regen", () => {
const stepEnd = workflow.indexOf("\n\n", stepStart);
const step = workflow.slice(stepStart, stepEnd === -1 ? undefined : stepEnd);

expect(step).toContain("run: npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build");
expect(step).toContain(
"run: npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build",
);
expect(step).not.toContain("npm run ui:build");
});

it("ui-deploy.yml's Validate frontend step runs the split commands after the openapi check", () => {
const workflow = read(".github/workflows/ui-deploy.yml");

expect(workflow).toContain(
"run: npm run ui:openapi:check && npm run ui:lint && npm run ui:typecheck && npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build",
"run: npm run ui:openapi:check && npm run ui:lint && npm run ui:typecheck && npm run extension:lint && npm run miner-extension:lint && npm run extension:typecheck && npm run miner-extension:typecheck && npm run extension:build && npm run miner-extension:build && npm --workspace @jsonbored/gittensory-ui run build",
);
expect(workflow).not.toContain("&& npm run ui:build");
});
Expand Down
Loading