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
34 changes: 7 additions & 27 deletions .github/workflows/update-opencode-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,15 @@ jobs:
ref: main
persist-credentials: false

- name: Record main commit
id: main
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.2

- name: Prepare update branch
id: branch
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh auth setup-git
branch_sha=$(git ls-remote --heads origin refs/heads/update-opencode-plugin | cut -f1)
if [ -n "$branch_sha" ]; then
git fetch origin update-opencode-plugin
fi
git checkout -B update-opencode-plugin main
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
echo "branch_sha=$branch_sha" >> "$GITHUB_OUTPUT"

- name: Check npm for the latest version
id: latest
run: bun scripts/update-opencode-plugin.ts
Expand All @@ -58,21 +46,13 @@ jobs:
bun run changeset-add -- usage-limits patch "Update @opencode/plugin to $VERSION"
bun run changeset-add -- force-input patch "Update @opencode/plugin to $VERSION"

- name: Commit and push update
- name: Create verified update commit
if: steps.latest.outputs.changed == 'true'
env:
BRANCH_SHA: ${{ steps.branch.outputs.branch_sha }}
GH_TOKEN: ${{ github.token }}
MAIN_SHA: ${{ steps.main.outputs.sha }}
VERSION: ${{ steps.latest.outputs.version }}
run: |
gh auth setup-git
git add package.json bun.lock .changeset
git commit -m "chore(deps): update @opencode/plugin to $VERSION"
if [ -n "$BRANCH_SHA" ]; then
git push --force-with-lease="refs/heads/update-opencode-plugin:$BRANCH_SHA" origin update-opencode-plugin
else
git push --set-upstream origin update-opencode-plugin
fi
run: bun scripts/create-signed-github-commit.ts

- name: Open or update pull request
if: steps.latest.outputs.changed == 'true'
Expand Down
204 changes: 204 additions & 0 deletions scripts/create-signed-github-commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { readFile, readdir } from "node:fs/promises";

interface ApiResponse {
data?: unknown;
errors?: unknown;
message?: string;
node_id?: string;
sha?: string;
tree?: { sha: string };
verification?: { verified: boolean; reason: string };
object?: { sha: string };
}

interface CreateTreeRequest {
base_tree: string;
tree: {
content: string;
mode: "100644";
path: string;
type: "blob";
}[];
}

interface CreateCommitRequest {
message: string;
parents: string[];
tree: string;
}

type ApiRequestBody = CreateTreeRequest | CreateCommitRequest;

const isApiResponse = (value: unknown): value is ApiResponse =>
value instanceof Object && !Array.isArray(value);

const hasUpdateRefs = (response: ApiResponse | undefined): boolean => {
const { data } = response ?? {};

if (!(data instanceof Object) || Array.isArray(data)) {
return false;
}

return "updateRefs" in data && Boolean(data.updateRefs);
};

const token = process.env.GH_TOKEN;
const repository = process.env.GITHUB_REPOSITORY;
const version = process.env.VERSION;
const mainSha = process.env.MAIN_SHA;

if (!token || !repository || !version || !mainSha) {
throw new Error(
"GH_TOKEN, GITHUB_REPOSITORY, MAIN_SHA, and VERSION are required"
);
}

const request = async (
path: string,
method: "GET" | "POST" | "PATCH",
body?: ApiRequestBody
): Promise<ApiResponse> => {
const headers = new Headers({
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
});
const init: RequestInit = { headers, method };

if (body) {
headers.set("Content-Type", "application/json");
init.body = JSON.stringify(body);
}

const response = await fetch(
`https://api.github.com/repos/${repository}${path}`,
init
);

const result: unknown = await response.json();

if (!response.ok) {
const message =
isApiResponse(result) && result.message
? String(result.message)
: response.statusText;
throw new Error(`GitHub API ${method} ${path} failed: ${message}`);
}

if (!isApiResponse(result)) {
throw new Error(
`GitHub API ${method} ${path} returned an invalid response`
);
}

return result;
};

const changesetDirectoryEntries = await readdir(".changeset", {
withFileTypes: true,
});
const changesetFiles = changesetDirectoryEntries
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
.map((entry) => `.changeset/${entry.name}`);
const changedFiles = ["package.json", "bun.lock", ...changesetFiles];

const baseSha = mainSha;

const baseCommit = await request(`/git/commits/${baseSha}`, "GET");

if (!baseCommit.tree?.sha) {
throw new Error("GitHub API did not return the base tree SHA");
}

const treeEntries = await Promise.all(
changedFiles.map(async (path) => ({
content: await readFile(path, "utf-8"),
mode: "100644" as const,
path,
type: "blob" as const,
}))
);

const tree = await request("/git/trees", "POST", {
base_tree: baseCommit.tree.sha,
tree: treeEntries,
});

if (!tree.sha) {
throw new Error("GitHub API did not return a tree SHA");
}

const commit = await request("/git/commits", "POST", {
Comment thread
mynameistito marked this conversation as resolved.
message: `chore(deps): update @opencode/plugin to ${version}`,
parents: [baseSha],
tree: tree.sha,
});

if (!commit.sha || !commit.verification?.verified) {
throw new Error(
`GitHub did not verify the update commit (reason: ${commit.verification?.reason ?? "unknown"})`
);
}

const refPath = "/git/ref/heads/update-opencode-plugin";
let existingRef: ApiResponse | undefined;

try {
existingRef = await request(refPath, "GET");
} catch (error) {
if (!(error instanceof Error) || !error.message.includes("Not Found")) {
throw error;
}
}

const repositoryInfo = await request("", "GET");

if (!repositoryInfo.node_id) {
throw new Error("GitHub API did not return the repository node ID");
}

const refUpdateResponse = await fetch("https://api.github.com/graphql", {
body: JSON.stringify({
query:
"mutation($repositoryId: ID!, $refUpdates: [RefUpdate!]!) { updateRefs(input: { repositoryId: $repositoryId, refUpdates: $refUpdates }) { clientMutationId } }",
variables: {
refUpdates: [
{
afterOid: commit.sha,
beforeOid:
existingRef?.object?.sha ??
"0000000000000000000000000000000000000000",
force: true,
name: "refs/heads/update-opencode-plugin",
},
],
repositoryId: repositoryInfo.node_id,
},
}),
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "POST",
});
const refUpdateResult: unknown = await refUpdateResponse.json();

const graphQLResult = isApiResponse(refUpdateResult)
? refUpdateResult
: undefined;
const hasGraphQLErrors = Array.isArray(graphQLResult?.errors)
? graphQLResult.errors.length > 0
: false;

if (
!refUpdateResponse.ok ||
hasGraphQLErrors ||
!hasUpdateRefs(graphQLResult)
) {
throw new Error(
`GitHub API failed to update the update branch: ${JSON.stringify(graphQLResult?.errors ?? refUpdateResponse.statusText)}`
);
}

console.log(`Pushed verified update commit ${commit.sha}`);
Loading