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
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json

name: Bug report
description: Report a reproducible problem with OpenCode V2 or one of these plugins.
title: "bug: "
Expand Down
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json

blank_issues_enabled: true
contact_links:
- name: OpenCode V2 documentation
Expand Down
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json

name: Feature request
description: Suggest an improvement to OpenCode V2 support or one of these plugins.
title: "feature: "
Expand Down
40 changes: 40 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# GitHub repository configuration

This directory contains GitHub Actions workflows, automation scripts, repository templates, and configuration used to maintain this repository.

## Workflows

| Workflow | Purpose | Trigger |
| --- | --- | --- |
| [`ci.yml`](workflows/ci.yml) | Runs typechecking, lint/check, tests, package tests, build, and Knip. | Pull requests and pushes to `main` |
| [`deploy-preview.yml`](workflows/deploy-preview.yml) | Deploys documentation previews and cleans them up when pull requests close. | CI completion and pull request closure |
| [`deploy.yml`](workflows/deploy.yml) | Deploys production documentation after a release workflow completes. | Release workflow completion |
| [`link-check.yml`](workflows/link-check.yml) | Checks documentation links. | Relevant pull requests, weekly schedule, or manual dispatch |
| [`pr-metadata.yml`](workflows/pr-metadata.yml) | Applies managed pull request labels and checks Changeset coverage. | Pull request activity |
| [`release.yml`](workflows/release.yml) | Verifies packages and runs the Changesets release/publish flow. | Pushes to `main` or manual dispatch |
| [`update-opencode-plugin.yml`](workflows/update-opencode-plugin.yml) | Checks for a newer `@opencode/plugin` and opens or updates its pull request. | Manual dispatch |
| [`workflow-security.yml`](workflows/workflow-security.yml) | Scans workflow and automation changes for security issues. | Relevant pull requests, pushes to `main`, or schedule |

## Workflow scripts

Workflow-specific TypeScript entrypoints live in [`scripts/`](scripts/) so their implementation sits beside the workflows that call them. Workflows invoke them with Bun; scripts use Node standard APIs and launch npm through the explicitly configured Node executable where needed.

| Script | Purpose | Used by |
| --- | --- | --- |
| [`create-git-tag.ts`](scripts/create-git-tag.ts) | Publishes release tags after Changesets publishing. | `release.yml` |
| [`create-signed-github-commit.ts`](scripts/create-signed-github-commit.ts) | Creates a verified GitHub commit for the plugin update. | `update-opencode-plugin.yml` |
| [`get-version-pr-title.ts`](scripts/get-version-pr-title.ts) | Produces the release pull request title. | `release.yml` |
| [`pr-metadata-helpers.ts`](scripts/pr-metadata-helpers.ts) | Pure helpers for PR labels and Changeset coverage. | `update-pr-metadata.ts` and tests |
| [`semver.ts`](scripts/semver.ts) | Parses and compares serialized semantic versions. | `update-opencode-plugin.ts` and tests |
| [`stage-packages.ts`](scripts/stage-packages.ts) | Stages workspace packages for release publishing. | `release.yml` |
| [`update-opencode-plugin.ts`](scripts/update-opencode-plugin.ts) | Checks for and reports a newer `@opencode/plugin` version. | `update-opencode-plugin.yml` |
| [`update-pr-metadata.ts`](scripts/update-pr-metadata.ts) | Reconciles PR labels and validates Changeset coverage. | `pr-metadata.yml` |

## Other GitHub files

| Path | Purpose |
| --- | --- |
| [`ISSUE_TEMPLATE/`](ISSUE_TEMPLATE/) | Bug report and feature request forms, plus shared issue-template configuration. |
| [`dependabot.yml`](dependabot.yml) | Weekly dependency update configuration for Bun and GitHub Actions. |
| [`pull_request_template.md`](pull_request_template.md) | Pull request summary, compatibility, validation, and release checklist. |
| [`zizmor.yml`](zizmor.yml) | Line-specific ignores for intentionally reviewed workflow-security findings. |
44 changes: 44 additions & 0 deletions .github/scripts/__tests__/json-value.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";

import {
parseJsonArray,
parseJsonNumber,
parseJsonObject,
parseJsonString,
} from "@/scripts/shared/json-value.ts";

describe("JSON value parsing", () => {
it("parses objects into a typed map of JSON values", () => {
const object = parseJsonObject(
JSON.parse('{"name":"plugin","count":2}'),
"test object"
);

expect(object.get("name")).toBe("plugin");
expect(object.get("count")).toBe(2);
});

it("parses strings, finite numbers, and arrays", () => {
expect(parseJsonString("plugin", "name")).toBe("plugin");
expect(parseJsonNumber(2, "count")).toBe(2);
expect(parseJsonArray(["plugin", 2], "values")).toStrictEqual([
"plugin",
2,
]);
});

it("rejects values with the wrong JSON shape", () => {
expect(() => parseJsonObject(null, "object")).toThrow(
"object must be an object"
);
expect(() => parseJsonString(2, "string")).toThrow(
"string must be a string"
);
expect(() => parseJsonNumber("2", "number")).toThrow(
"number must be a number"
);
expect(() => parseJsonArray(JSON.parse("{}"), "array")).toThrow(
"array must be an array"
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { isNewerSemVer, parseSerializedSemVer } from "../semver";
import { isNewerSemVer, parseSerializedSemVer } from "@/github/semver.ts";

describe("Semantic version comparison", () => {
it("returns true only when the candidate is newer", () => {
Expand Down
48 changes: 48 additions & 0 deletions .github/scripts/announce-preview-approval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
asNumber,
asOptionalString,
asRecord,
githubPaginate,
githubRepository,
githubRequest,
} from "@/github/github-api.ts";
import type { JsonValue } from "@/scripts/shared/json-value.ts";
import { getJsonField } from "@/scripts/shared/json-value.ts";

interface Comment {
id: number;
body: string | null;
}

const parseComment = (value: JsonValue): Comment => {
const comment = asRecord(value);
return {
body: asOptionalString(getJsonField(comment, "body")),
id: asNumber(getJsonField(comment, "id")),
};
};

const pullRequestNumber = Number(process.env.PULL_REQUEST_NUMBER);
if (!Number.isInteger(pullRequestNumber)) {
throw new TypeError("The preview pull request number is missing.");
}

const serverUrl = process.env.GITHUB_SERVER_URL;
const runId = process.env.GITHUB_RUN_ID;
if (!serverUrl || !runId) {
throw new Error("GITHUB_SERVER_URL and GITHUB_RUN_ID are required.");
}

const marker = "<!-- docs-preview-approval -->";
const runUrl = `${serverUrl}/${githubRepository.owner}/${githubRepository.repo}/actions/runs/${runId}`;
const body = `${marker}\nThe documentation preview is waiting for approval. [Open the workflow run and click **Review deployments**](${runUrl}).`;
const path = `/repos/${githubRepository.owner}/${githubRepository.repo}/issues/${pullRequestNumber}/comments`;
const comments = await githubPaginate(path, parseComment);
const existing =
comments.find((comment) => comment.body?.includes(marker)) ?? null;

await githubRequest(existing ? `${path}/${existing.id}` : path, parseComment, {
body: JSON.stringify({ body }),
headers: { "Content-Type": "application/json" },
method: existing ? "PATCH" : "POST",
});
24 changes: 13 additions & 11 deletions scripts/git-tag.ts → .github/scripts/create-git-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { execFileSync } from "node:child_process";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";

import { getNodeExecutablePath } from "@/scripts/shared/node-executable.ts";

interface PackageManifest {
name: string;
version: string;
Expand All @@ -17,25 +19,25 @@ const outputPath = process.env.CHANGESETS_OUTPUT;
if (!outputPath) {
throw new Error("CHANGESETS_OUTPUT is required by the Changesets action");
}

const gitPath = Bun.which("git");
const bunxPath = Bun.which("bunx");
if (!gitPath || !bunxPath) {
throw new Error("git and bunx are required to create release tags");
const gitExecutable = process.env.GIT_EXECUTABLE;
if (!gitExecutable || !path.isAbsolute(gitExecutable)) {
throw new Error("GIT_EXECUTABLE must be an absolute path to Git");
}

const existingTags = new Set(
execFileSync(gitPath, ["ls-remote", "--tags", "origin"], {
execFileSync(gitExecutable, ["ls-remote", "--tags", "origin"], {
encoding: "utf-8",
})
.split("\n")
.map((line) => line.match(/refs\/tags\/(?<tag>.+)$/u)?.groups?.tag)
.filter((tag): tag is string => tag !== undefined && !tag.endsWith("^{}"))
.map((line) => line.match(/refs\/tags\/(?<tag>.+)$/u)?.groups?.tag ?? "")
.filter((tag) => tag.length > 0 && !tag.endsWith("^{}"))
);

execFileSync(bunxPath, ["changeset", "git-tag"], {
stdio: "inherit",
});
execFileSync(
getNodeExecutablePath(),
[path.resolve("node_modules/@changesets/cli/bin.js"), "git-tag"],
{ stdio: "inherit" }
);

const events: ChangesetsOutputEvent[] = [];
for (const directory of readdirSync("packages", { withFileTypes: true })) {
Expand Down
Loading
Loading