Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline
bcb37c0d91 fix: config ordering issue 2026-04-24 22:59:33 -04:00
14 changed files with 123 additions and 324 deletions

View File

@@ -451,7 +451,6 @@
"@typescript/native-preview": "catalog:",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"prettier": "3.6.2",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-V1Rt2k7ujkqGw4pDkn++WALTy1fAugvoKLhKvwFKkss=",
"aarch64-linux": "sha256-ho0AuGbJ1qw9Hvb3EbGC8f0lWqqgUslvda/wTe32MFo=",
"aarch64-darwin": "sha256-hdUyNmp+snwtnBckHXsPMgNFUYS1sYDdngkk+AXVqzc=",
"x86_64-darwin": "sha256-P57LpQNF8fplFKQBBIukhOKbIugbViyBUIUjClXohuk="
"x86_64-linux": "sha256-+G3/s18NZO1Dpc5TsZyix2Npodzei25Svw3nTjfzXW8=",
"aarch64-linux": "sha256-39HPencmRYRbyCk/cZIdPFk6ocY1AMlyuN9j25zAKzI=",
"aarch64-darwin": "sha256-043korPEjSHKiZ3P+EfWyOfKpgOC7CBpviccviaDa0o=",
"x86_64-darwin": "sha256-vsZ7e//rL9e7Cl5kl/Xplvi1fqayljxTLwRSbxvCxeM="
}
}

View File

@@ -27,16 +27,12 @@ Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, L
async function getCosts(workspaceID: string, year: number, month: number) {
"use server"
return withActor(async () => {
// Fetch a UTC buffer so client-side local dates at month boundaries are not dropped.
const startDate = new Date(Date.UTC(year, month, 1))
startDate.setUTCDate(startDate.getUTCDate() - 1)
const endDate = new Date(Date.UTC(year, month + 1, 1))
endDate.setUTCDate(endDate.getUTCDate() + 1)
const hourBucket = sql<number>`FLOOR(UNIX_TIMESTAMP(${UsageTable.timeCreated}) / 3600)`
const startDate = new Date(year, month, 1)
const endDate = new Date(year, month + 1, 1)
const usageData = await Database.use((tx) =>
tx
.select({
hourBucket: hourBucket.as("hour_bucket"),
date: sql<string>`DATE(${UsageTable.timeCreated})`,
model: UsageTable.model,
totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID,
@@ -51,7 +47,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
),
)
.groupBy(
hourBucket,
sql`DATE(${UsageTable.timeCreated})`,
UsageTable.model,
UsageTable.keyID,
sql`JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')`,
@@ -59,7 +55,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
.then((x) =>
x.map((r) => ({
...r,
hourBucket: Number(r.hourBucket),
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
plan: r.plan as "sub" | "lite" | "byok" | null,
})),
@@ -130,13 +125,15 @@ function getModelColor(model: string): string {
}
function formatDateLabel(dateStr: string): string {
const [year, month, day] = dateStr.split("-").map(Number)
const date = new Date(year, month - 1, day)
return `${date.toLocaleDateString(undefined, { month: "short" })} ${day.toString().padStart(2, "0")}`
}
function formatDateKey(date: Date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`
const date = new Date()
const [y, m, d] = dateStr.split("-").map(Number)
date.setFullYear(y)
date.setMonth(m - 1)
date.setDate(d)
date.setHours(0, 0, 0, 0)
const month = date.toLocaleDateString(undefined, { month: "short" })
const day = date.getUTCDate().toString().padStart(2, "0")
return `${month} ${day}`
}
function addOpacityToColor(color: string, opacity: number): string {
@@ -182,24 +179,17 @@ export function GraphSection() {
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
const getModels = createMemo(() => {
if (!store.data?.usage) return []
return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
})
const getDates = createMemo(() => {
const daysInMonth = new Date(store.year, store.month + 1, 0).getDate()
const dates = Array.from({ length: daysInMonth }, (_, i) => formatDateKey(new Date(store.year, store.month, i + 1)))
const dateSet = new Set(dates)
return {
dates,
dateSet,
}
})
const getUsageForMonth = createMemo(() => {
if (!store.data?.usage) return []
const dateSet = getDates().dateSet
return store.data.usage.filter((row) => dateSet.has(formatDateKey(new Date(row.hourBucket * 3600 * 1000))))
})
const getModels = createMemo(() => {
return Array.from(new Set(getUsageForMonth().map((row) => row.model))).sort()
return Array.from({ length: daysInMonth }, (_, i) => {
const date = new Date(store.year, store.month, i + 1)
return date.toISOString().split("T")[0]
})
})
const getKeyName = (keyID: string | null): string => {
@@ -215,9 +205,9 @@ export function GraphSection() {
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
const chartConfig = createMemo((): ChartConfiguration | null => {
const dates = getDates().dates
const usage = getUsageForMonth()
if (usage.length === 0) return null
const data = store.data
const dates = getDates()
if (!data?.usage?.length) return null
store.colorScheme
const styles = getComputedStyle(document.documentElement)
@@ -239,12 +229,11 @@ export function GraphSection() {
dailyDataLite.set(dateKey, new Map())
}
usage
data.usage
.filter((row) => (store.key ? row.keyId === store.key : true))
.forEach((row) => {
const dateKey = formatDateKey(new Date(row.hourBucket * 3600 * 1000))
const targetMap = row.plan === "sub" ? dailyDataSub : row.plan === "lite" ? dailyDataLite : dailyDataRegular
const dayMap = targetMap.get(dateKey)
const dayMap = targetMap.get(row.date)
if (!dayMap) return
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
})
@@ -297,8 +286,6 @@ export function GraphSection() {
}),
]
if (datasets.length === 0) return null
return {
type: "bar",
data: {

View File

@@ -69,7 +69,6 @@
"@typescript/native-preview": "catalog:",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"prettier": "3.6.2",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",

View File

@@ -14,7 +14,6 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".cc": "cpp",
".c++": "cpp",
".cs": "csharp",
".csx": "csharp",
".css": "css",
".d": "d",
".pas": "pascal",

View File

@@ -703,10 +703,31 @@ export const Zls: Info = {
export const CSharp: Info = {
id: "csharp",
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
extensions: [".cs", ".csx"],
extensions: [".cs"],
async spawn(root) {
const bin = await getRoslynLanguageServer()
if (!bin) return
let bin = which("roslyn-language-server")
if (!bin) {
if (!which("dotnet")) {
log.error(".NET SDK is required to install roslyn-language-server")
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing roslyn-language-server via dotnet tool")
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install roslyn-language-server")
return
}
bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed roslyn-language-server`, { bin })
}
return {
process: spawn(bin, ["--stdio", "--autoLoadProjects"], {
@@ -716,135 +737,6 @@ export const CSharp: Info = {
},
}
export const Razor: Info = {
id: "razor",
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
extensions: [".razor", ".cshtml"],
async spawn(root) {
const bin = await getRoslynLanguageServer()
if (!bin) return
const razor = await findVscodeRazorExtension()
if (!razor) {
log.info("VS Code C# extension with Razor support not found, skipping Razor LSP")
return
}
log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension })
return {
process: spawn(
bin,
[
"--stdio",
"--autoLoadProjects",
`--razorSourceGenerator=${razor.compiler}`,
`--razorDesignTimePath=${razor.targets}`,
"--extension",
razor.extension,
],
{
cwd: root,
},
),
}
},
}
let roslynLanguageServerInstall: Promise<string | undefined> | undefined
async function getRoslynLanguageServer() {
const existing = which("roslyn-language-server")
if (existing) return existing
const global = await roslynLanguageServerGlobalPath()
if (global) return global
roslynLanguageServerInstall ||= installRoslynLanguageServer().finally(() => {
roslynLanguageServerInstall = undefined
})
return roslynLanguageServerInstall
}
async function installRoslynLanguageServer() {
if (!which("dotnet")) {
log.error(".NET SDK is required to install roslyn-language-server")
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing roslyn-language-server via dotnet tool")
const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], {
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install roslyn-language-server")
return
}
const resolved = which("roslyn-language-server")
if (resolved) {
log.info(`installed roslyn-language-server`, { bin: resolved })
return resolved
}
const global = await roslynLanguageServerGlobalPath()
if (global) {
log.info(`installed roslyn-language-server`, { bin: global })
return global
}
log.error("Installed roslyn-language-server but could not resolve executable")
}
async function roslynLanguageServerGlobalPath() {
const bin = path.join(
process.env.DOTNET_CLI_HOME ?? os.homedir(),
".dotnet",
"tools",
"roslyn-language-server" + (process.platform === "win32" ? ".cmd" : ""),
)
return (await pathExists(bin)) ? bin : undefined
}
async function findVscodeRazorExtension() {
const roots = [
process.env.VSCODE_EXTENSIONS,
path.join(os.homedir(), ".vscode", "extensions"),
path.join(os.homedir(), ".vscode-insiders", "extensions"),
path.join(os.homedir(), ".vscode-server", "extensions"),
path.join(os.homedir(), ".vscode-server-insiders", "extensions"),
].filter((item) => item !== undefined)
for (const root of [...new Set(roots)]) {
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
const candidates = await Promise.all(
entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith("ms-dotnettools.csharp-"))
.map(async (entry) => ({
path: path.join(root, entry.name, ".razorExtension"),
modified: (await fs.stat(path.join(root, entry.name)).catch(() => undefined))?.mtimeMs ?? 0,
})),
)
for (const entry of candidates.sort((a, b) => b.modified - a.modified).map((candidate) => candidate.path)) {
const result = {
compiler: path.join(entry, "Microsoft.CodeAnalysis.Razor.Compiler.dll"),
targets: path.join(entry, "Targets", "Microsoft.NET.Sdk.Razor.DesignTime.targets"),
extension: path.join(entry, "Microsoft.VisualStudioCode.RazorExtension.dll"),
}
if (
(await pathExists(result.compiler)) &&
(await pathExists(result.targets)) &&
(await pathExists(result.extension))
) {
return result
}
}
}
}
export const FSharp: Info = {
id: "fsharp",
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),

View File

@@ -288,18 +288,8 @@ function expand(pattern: string): string {
}
export function fromConfig(permission: ConfigPermission.Info) {
// Sort top-level keys so wildcard permissions (`*`, `mcp_*`) come before
// specific ones. Combined with `findLast` in evaluate(), this gives the
// intuitive semantic "specific tool rules override the `*` fallback"
// regardless of the user's JSON key order. Sub-pattern order inside a
// single permission key is preserved — only top-level keys are sorted.
const entries = Object.entries(permission).sort(([a], [b]) => {
const aWild = a.includes("*")
const bWild = b.includes("*")
return aWild === bWild ? 0 : aWild ? -1 : 1
})
const ruleset: Ruleset = []
for (const [key, value] of entries) {
for (const [key, value] of Object.entries(permission)) {
if (typeof value === "string") {
ruleset.push({ permission: key, action: value, pattern: "*" })
continue

View File

@@ -787,7 +787,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const shellName = (
process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh)
).toLowerCase()
const cwd = ctx.directory
const invocations: Record<string, { args: string[] }> = {
nu: { args: ["-c", input.command] },
fish: { args: ["-c", input.command] },
@@ -796,13 +795,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
"-l",
"-c",
`
__oc_cwd=$PWD
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
cd -- "$1"
cd "$__oc_cwd"
eval ${JSON.stringify(input.command)}
`,
"opencode",
cwd,
],
},
bash: {
@@ -810,13 +808,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
"-l",
"-c",
`
__oc_cwd=$PWD
shopt -s expand_aliases
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
cd -- "$1"
cd "$__oc_cwd"
eval ${JSON.stringify(input.command)}
`,
"opencode",
cwd,
],
},
cmd: { args: ["/c", input.command] },
@@ -826,6 +823,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
const args = (invocations[shellName] ?? invocations[""]).args
const cwd = ctx.directory
const shellEnv = yield* plugin.trigger(
"shell.env",
{ cwd, sessionID: input.sessionID, callID: part.callID },

View File

@@ -1501,10 +1501,8 @@ test("permission config canonicalises known keys first, preserves rest-key inser
// todowrite, external_directory are declared in `config/permission.ts`),
// followed by rest keys in the user's insertion order.
//
// Rule precedence is NOT affected by this reordering: `Permission.fromConfig`
// sorts wildcards before specifics before iterating. See the
// "fromConfig - specific key beats wildcard regardless of JSON key order"
// test in test/permission/next.test.ts for the behavioural guarantee.
// Permission.fromConfig preserves this effective order, and permission
// evaluation uses last-match-wins precedence.
await using tmp = await tmpdir({
init: async (dir) => {
await Filesystem.write(

View File

@@ -130,49 +130,45 @@ test("fromConfig - does not expand tilde in middle of path", () => {
// Top-level wildcard-vs-specific precedence semantics.
//
// fromConfig sorts top-level keys so wildcard permissions (containing "*")
// come before specific permissions. Combined with `findLast` in evaluate(),
// this gives the intuitive semantic "specific tool rules override the `*`
// fallback", regardless of the order the user wrote the keys in their JSON.
// fromConfig preserves top-level key order. Combined with `findLast` in
// evaluate(), later matching rules win.
//
// Sub-pattern order inside a single permission key (e.g. `bash: { "*": "allow", "rm": "deny" }`)
// still depends on insertion order — only top-level keys are sorted.
// also depends on insertion order.
test("fromConfig - specific key beats wildcard regardless of JSON key order", () => {
test("fromConfig - top-level key order controls wildcard precedence", () => {
const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" })
const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" })
// Both orderings produce the same ruleset
expect(wildcardFirst).toEqual(specificFirst)
// And both evaluate bash → allow (bash rule wins over * fallback)
expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow")
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("allow")
expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny")
})
test("fromConfig - regression: trailing wildcard overrides earlier specific rule", () => {
const ruleset = Permission.fromConfig({ bash: "ask", "*": "allow" })
expect(Permission.evaluate("bash", "glab", ruleset).action).toBe("allow")
})
test("fromConfig - wildcard acts as fallback for permissions with no specific rule", () => {
const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask" })
const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" })
expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask")
expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow")
})
test("fromConfig - top-level ordering: wildcards first, specifics after", () => {
test("fromConfig - preserves top-level ordering", () => {
const ruleset = Permission.fromConfig({
bash: "allow",
"*": "ask",
edit: "deny",
"mcp_*": "allow",
})
// wildcards (* and mcp_*) come before specifics (bash, edit)
const permissions = ruleset.map((r) => r.permission)
expect(permissions.slice(0, 2).sort()).toEqual(["*", "mcp_*"])
expect(permissions.slice(2)).toEqual(["bash", "edit"])
expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"])
})
test("fromConfig - sub-pattern insertion order inside a tool key is preserved (only top-level sorts)", () => {
test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => {
// Sub-patterns within a single tool key use the documented "`*` first,
// specific patterns after" convention (findLast picks specifics). The
// top-level sort must not touch sub-pattern ordering.
// top-level order must not affect sub-pattern ordering.
const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } })
expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"])
// * fallback for unknown commands

View File

@@ -1078,30 +1078,6 @@ unix("shell completes a fast command on the preferred shell", () =>
),
)
unix("shell commands can change directory after startup", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { prompt, run, chat } = yield* boot()
const parent = path.dirname(dir)
const result = yield* prompt.shell({
sessionID: chat.id,
agent: "build",
command: "cd .. && pwd",
})
expect(result.info.role).toBe("assistant")
const tool = completedTool(result.parts)
if (!tool) return
expect(tool.state.output).toContain(parent)
expect(tool.state.metadata.output).toContain(parent)
yield* run.assertNotBusy(chat.id)
}),
{ git: true, config: cfg },
),
)
unix("shell lists files from the project directory", () =>
provideTmpdirInstance(
(dir) =>

View File

@@ -16,7 +16,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
| astro | .astro | Auto-installs for Astro projects |
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| csharp | .cs, .csx | `.NET SDK` installed |
| csharp | .cs | `.NET SDK` installed |
| clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` command available |
| dart | .dart | `dart` command available |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
@@ -36,7 +36,6 @@ OpenCode comes with several built-in LSP servers for popular languages:
| php intelephense | .php | Auto-installs for PHP projects |
| prisma | .prisma | `prisma` command available |
| pyright | .py, .pyi | `pyright` dependency installed |
| razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| rust | .rs | `rust-analyzer` command available |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |

View File

@@ -57,19 +57,6 @@ function lines(prs: PR[]) {
return prs.map((x) => `- #${x.number}: ${x.title}`).join("\n") || "(none)"
}
function group(title: string) {
if (process.env.GITHUB_ACTIONS !== "true") {
console.log(title)
return { [Symbol.dispose]() {} }
}
console.log(`::group::${title}`)
return {
[Symbol.dispose]() {
console.log("::endgroup::")
},
}
}
async function typecheck() {
console.log(" Running typecheck...")
@@ -94,39 +81,6 @@ async function build() {
}
}
async function validate() {
if (!(await typecheck())) return false
if (!(await build())) return false
return true
}
async function commitSmokeChanges() {
const out = await $`git status --porcelain`.text()
if (!out.trim()) {
console.log("Smoke check passed")
return true
}
try {
await $`git add -A`
await $`git commit -m "Fix beta integration"`
} catch (err) {
console.log(`Failed to commit smoke fixes: ${err}`)
return false
}
if (!(await validate())) return false
const left = await $`git status --porcelain`.text()
if (!left.trim()) {
console.log("Smoke check passed")
return true
}
console.log(`Smoke check left uncommitted changes:\n${left}`)
return false
}
async function install() {
console.log(" Regenerating bun.lock...")
@@ -189,15 +143,11 @@ async function fix(pr: PR, files: string[], prs: PR[], applied: number[], idx: n
}
async function smoke(prs: PR[], applied: number[]) {
console.log("\nRunning final smoke check...")
if (await validate()) return commitSmokeChanges()
console.log("\nTrying to fix final smoke check with opencode...")
console.log("\nRunning final smoke check with opencode...")
const done = lines(prs.filter((x) => applied.includes(x.number)))
const prompt = [
"The beta merge batch is complete, but the deterministic final smoke check failed.",
"The beta merge batch is complete.",
`Merged PRs on HEAD:\n${done}`,
"Run `bun typecheck` at the repo root.",
"Run `./script/build.ts --single` in `packages/opencode`.",
@@ -212,8 +162,38 @@ async function smoke(prs: PR[], applied: number[]) {
return false
}
if (!(await validate())) return false
return commitSmokeChanges()
if (!(await typecheck())) {
return false
}
if (!(await build())) {
return false
}
const out = await $`git status --porcelain`.text()
if (!out.trim()) {
console.log("Smoke check passed")
return true
}
try {
await $`git add -A`
await $`git commit -m "Fix beta integration"`
} catch (err) {
console.log(`Failed to commit smoke fixes: ${err}`)
return false
}
if (!(await typecheck())) {
return false
}
if (!(await build())) {
return false
}
console.log("Smoke check passed")
return true
}
async function main() {
@@ -240,8 +220,8 @@ async function main() {
const failed: FailedPR[] = []
for (const [idx, pr] of prs.entries()) {
console.log()
using _ = group(`Processing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`)
console.log(`\nProcessing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`)
console.log(" Fetching PR head...")
try {
await $`git fetch origin pull/${pr.number}/head:pr/${pr.number}`
@@ -314,13 +294,17 @@ async function main() {
throw new Error(`${failed.length} PR(s) failed to merge`)
}
if (applied.length > 0) {
console.log("\nSkipping final smoke check")
}
console.log("\nChecking if beta branch has changes...")
await $`git fetch origin beta`
const localTree = (await $`git rev-parse beta^{tree}`.text()).trim()
const localTree = await $`git rev-parse beta^{tree}`.text()
const remoteTrees = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n")
const matchIdx = remoteTrees.indexOf(localTree)
const matchIdx = remoteTrees.indexOf(localTree.trim())
if (matchIdx !== -1) {
if (matchIdx !== 0) {
console.log(`Beta branch contains this sync, but additional commits exist after it. Leaving beta branch as is.`)
@@ -330,25 +314,7 @@ async function main() {
return
}
if (!(await smoke(prs, applied))) throw new Error("Final smoke check failed")
await $`git fetch origin beta`
const validatedTree = (await $`git rev-parse beta^{tree}`.text()).trim()
const remoteTreesAfterSmoke = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n")
const matchIdxAfterSmoke = remoteTreesAfterSmoke.indexOf(validatedTree)
if (matchIdxAfterSmoke !== -1) {
if (matchIdxAfterSmoke !== 0) {
console.log(
`Beta branch contains this validated sync, but additional commits exist after it. Leaving beta branch as is.`,
)
} else {
console.log("Validated beta branch now matches remote contents, no push needed")
}
return
}
console.log("Force pushing validated beta branch...")
console.log("Force pushing beta branch...")
await $`git push origin beta --force --no-verify`
console.log("Successfully synced beta branch")

View File

@@ -37,7 +37,7 @@ async function close(num: number) {
const patch = await fetch(base, {
method: "PATCH",
headers,
body: JSON.stringify({ state: "closed", state_reason: "not_planned" }),
body: JSON.stringify({ state: "closed", state_reason: "completed" }),
})
if (!patch.ok) throw new Error(`Failed to close #${num}: ${patch.status} ${patch.statusText}`)