Skip to content

Commit a5dc219

Browse files
authored
Merge pull request #207 from LeXwDeX/dev
fix(release): promote GraphAgent versioning
2 parents 04d83a9 + d482e4b commit a5dc219

3 files changed

Lines changed: 232 additions & 33 deletions

File tree

.github/workflows/release-fork.yml

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
# LeXwDeX/opencode fork. Stripped-down version of upstream
66
# publish.yml — removes repo guard, Blacksmith runners, code
77
# signing, npm publish, and Tauri desktop.
8-
# Trigger : Manual workflow_dispatch (PRIMARY) — pick branch + version
8+
# Trigger : Manual workflow_dispatch (PRIMARY) — pick main or dev
99
# Push to main/dev ONLY registers the workflow in the Actions UI
1010
# (the build job is gated on `github.event_name == 'workflow_dispatch'`)
11-
# Inputs : version — release version string (e.g. "1.14.30-fork.2")
12-
# Leave blank for auto timestamped version.
13-
# create_release — when true, create a GitHub Release with the
11+
# Version : Independent GraphAgent SemVer derived only from graphagent-v* tags.
12+
# dev builds the next stable version as X.Y.Z-dev.N; main publishes
13+
# X.Y.Z and marks it Latest. OpenCode package versions are ignored.
14+
# Inputs : create_release — when true, create a GitHub Release with the
1415
# built binaries + SHA256SUMS attached. Builds
15-
# from any branch other than `main` are marked
16-
# prerelease (test build); `main` = formal release.
16+
# from `dev` are marked prerelease (test build);
17+
# `main` builds are formal and become Latest.
1718
# platforms — comma-separated subset of linux,macos,windows
1819
# to build. Leave blank to build all three.
1920
# Jobs : package-templates — package latest reference templates from the
@@ -31,15 +32,11 @@ on:
3132
# Manual trigger — the primary way to build releases
3233
workflow_dispatch:
3334
inputs:
34-
version:
35-
description: "Release version (e.g. 1.14.30-fork.2). Leave blank for auto."
36-
required: false
37-
type: string
3835
create_release:
3936
description: "Create a GitHub Release with the built binaries"
4037
required: false
4138
type: boolean
42-
default: false
39+
default: true
4340
platforms:
4441
description: "Platforms to build, comma-separated (linux,macos,windows). Leave blank for all."
4542
required: false
@@ -58,7 +55,36 @@ permissions:
5855
env:
5956
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: false
6057

58+
concurrency:
59+
group: release-fork
60+
cancel-in-progress: false
61+
6162
jobs:
63+
version:
64+
name: Resolve GraphAgent Version
65+
if: github.event_name == 'workflow_dispatch'
66+
runs-on: ubuntu-latest
67+
outputs:
68+
channel: ${{ steps.release-version.outputs.channel }}
69+
version: ${{ steps.release-version.outputs.version }}
70+
tag: ${{ steps.release-version.outputs.tag }}
71+
prerelease: ${{ steps.release-version.outputs.prerelease }}
72+
latest: ${{ steps.release-version.outputs.latest }}
73+
steps:
74+
- name: Checkout Repository
75+
uses: actions/checkout@v4
76+
with:
77+
fetch-depth: 0
78+
79+
- name: Setup Bun
80+
uses: ./.github/actions/setup-bun
81+
with:
82+
save-cache: false
83+
84+
- name: Resolve Version
85+
id: release-version
86+
run: bun run ./packages/opencode/script/release-version.ts
87+
6288
# Package the latest reference templates from the dedicated config repo
6389
# (LeXwDeX/opencode-dag-config) into a release asset. dev/main do not manage
6490
# these templates anymore — the config repo is the single source of truth.
@@ -98,7 +124,7 @@ jobs:
98124

99125
build-cli:
100126
name: Build CLI (${{ matrix.name }})
101-
needs: package-templates
127+
needs: [version, package-templates]
102128
# Skip the actual build when triggered by push (registration-only). Per-
103129
# matrix-entry platform filtering can't live here — `matrix` isn't in
104130
# scope for a job-level `if:` — so it's applied to each step below instead.
@@ -182,8 +208,8 @@ jobs:
182208
if: inputs.platforms == '' || contains(inputs.platforms, matrix.name)
183209
run: ./packages/opencode/script/build.ts --single --skip-install
184210
env:
185-
OPENCODE_CHANNEL: latest
186-
OPENCODE_VERSION: ${{ inputs.version || '' }}
211+
OPENCODE_CHANNEL: ${{ needs.version.outputs.channel == 'main' && 'latest' || 'dev' }}
212+
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
187213

188214
# Bundle ripgrep into each dist/opencode-*/bin so air-gapped users do not
189215
# hit the runtime download in packages/core/src/ripgrep/binary.ts.
@@ -221,7 +247,7 @@ jobs:
221247
222248
release:
223249
name: Create GitHub Release
224-
needs: [build-cli, package-templates]
250+
needs: [version, build-cli, package-templates]
225251
if: inputs.create_release
226252
runs-on: ubuntu-latest
227253
steps:
@@ -234,16 +260,6 @@ jobs:
234260
path: release-assets
235261
merge-multiple: true
236262

237-
- name: Determine Version Tag
238-
id: tag
239-
run: |
240-
VER="${{ inputs.version }}"
241-
if [ -z "$VER" ]; then
242-
VER="0.0.0-$(date +%Y%m%d%H%M)"
243-
fi
244-
echo "version=$VER" >> "$GITHUB_OUTPUT"
245-
echo "tag=v$VER" >> "$GITHUB_OUTPUT"
246-
247263
- name: Generate SHA256SUMS
248264
working-directory: release-assets
249265
run: |
@@ -254,17 +270,18 @@ jobs:
254270
- name: Create GitHub Release
255271
env:
256272
GH_TOKEN: ${{ github.token }}
257-
# Releases built from any branch other than `main` (e.g. `dev`) are
258-
# test builds and get marked --prerelease; only `main` produces a
259-
# formal release.
273+
# dev releases are prereleases and can never become Latest. main
274+
# releases are formal and are explicitly promoted to Latest.
260275
run: |
261276
EXTRA_FLAGS=()
262-
if [ "${{ github.ref_name }}" != "main" ]; then
263-
EXTRA_FLAGS+=(--prerelease)
277+
if [ "${{ needs.version.outputs.channel }}" = "main" ]; then
278+
EXTRA_FLAGS+=(--latest)
279+
else
280+
EXTRA_FLAGS+=(--prerelease --latest=false)
264281
fi
265-
gh release create "${{ steps.tag.outputs.tag }}" \
266-
--title "opencode ${{ steps.tag.outputs.version }}" \
267-
--notes "Fork release from branch ${{ github.ref_name }}" \
282+
gh release create "${{ needs.version.outputs.tag }}" \
283+
--title "OpenCode GraphAgent v${{ needs.version.outputs.version }}" \
284+
--notes "GraphAgent release from branch ${{ github.ref_name }}" \
268285
--target "${{ github.sha }}" \
269286
"${EXTRA_FLAGS[@]}" \
270287
release-assets/*
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const tagPrefix = "graphagent-v"
2+
const stableTag = /^graphagent-v(\d+)\.(\d+)\.(\d+)$/
3+
const devTag = /^graphagent-v(\d+)\.(\d+)\.(\d+)-dev\.(\d+)$/
4+
5+
type Version = readonly [major: number, minor: number, patch: number]
6+
7+
export function resolveReleaseVersion(input: { branch: string; tags: string[] }) {
8+
const channel = input.branch
9+
if (channel !== "main" && channel !== "dev") {
10+
throw new Error(`GraphAgent releases require main or dev, received: ${channel || "(empty)"}`)
11+
}
12+
13+
const latest = input.tags
14+
.flatMap((tag) => {
15+
const version = parseStableTag(tag)
16+
return version ? [version] : []
17+
})
18+
.toSorted(compareVersion)
19+
.at(-1)
20+
const target = nextVersion(latest)
21+
const base = target.join(".")
22+
23+
if (channel === "main") {
24+
return {
25+
channel,
26+
version: base,
27+
tag: `${tagPrefix}${base}`,
28+
prerelease: false,
29+
latest: true,
30+
}
31+
}
32+
33+
const sequence =
34+
Math.max(
35+
0,
36+
...input.tags.flatMap((tag) => {
37+
const parsed = parseDevTag(tag)
38+
return parsed && sameVersion(parsed.version, target) ? [parsed.sequence] : []
39+
}),
40+
) + 1
41+
const version = `${base}-dev.${sequence}`
42+
return {
43+
channel,
44+
version,
45+
tag: `${tagPrefix}${version}`,
46+
prerelease: true,
47+
latest: false,
48+
}
49+
}
50+
51+
function parseStableTag(tag: string): Version | undefined {
52+
const match = stableTag.exec(tag)
53+
if (!match) return undefined
54+
return [Number(match[1]), Number(match[2]), Number(match[3])]
55+
}
56+
57+
function parseDevTag(tag: string) {
58+
const match = devTag.exec(tag)
59+
if (!match) return undefined
60+
return {
61+
version: [Number(match[1]), Number(match[2]), Number(match[3])] as Version,
62+
sequence: Number(match[4]),
63+
}
64+
}
65+
66+
function nextVersion(version: Version | undefined): Version {
67+
if (!version) return [1, 0, 0]
68+
return [version[0], version[1], version[2] + 1]
69+
}
70+
71+
function compareVersion(left: Version, right: Version) {
72+
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2]
73+
}
74+
75+
function sameVersion(left: Version, right: Version) {
76+
return left[0] === right[0] && left[1] === right[1] && left[2] === right[2]
77+
}
78+
79+
async function readTags() {
80+
const child = Bun.spawn(["git", "tag", "--list", `${tagPrefix}*`], {
81+
stdout: "pipe",
82+
stderr: "pipe",
83+
})
84+
const [stdout, stderr, exitCode] = await Promise.all([
85+
new Response(child.stdout).text(),
86+
new Response(child.stderr).text(),
87+
child.exited,
88+
])
89+
if (exitCode !== 0) throw new Error(`Unable to list GraphAgent tags: ${stderr.trim()}`)
90+
return stdout.split(/\r?\n/).filter(Boolean)
91+
}
92+
93+
async function main() {
94+
const output = process.env.GITHUB_OUTPUT
95+
if (!output) throw new Error("GITHUB_OUTPUT is required")
96+
97+
const release = resolveReleaseVersion({
98+
branch: process.env.GITHUB_REF_NAME ?? "",
99+
tags: await readTags(),
100+
})
101+
const { appendFile } = await import("node:fs/promises")
102+
await appendFile(
103+
output,
104+
[
105+
`channel=${release.channel}`,
106+
`version=${release.version}`,
107+
`tag=${release.tag}`,
108+
`prerelease=${release.prerelease}`,
109+
`latest=${release.latest}`,
110+
"",
111+
].join("\n"),
112+
)
113+
console.log(`GraphAgent release: ${release.tag} (channel=${release.channel}, latest=${release.latest})`)
114+
}
115+
116+
if (import.meta.main) await main()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { resolveReleaseVersion } from "../script/release-version"
3+
4+
describe("GraphAgent release versions", () => {
5+
test("starts the independent main release line at 1.0.0", () => {
6+
expect(resolveReleaseVersion({ branch: "main", tags: [] })).toEqual({
7+
channel: "main",
8+
version: "1.0.0",
9+
tag: "graphagent-v1.0.0",
10+
prerelease: false,
11+
latest: true,
12+
})
13+
})
14+
15+
test("starts dev at 1.0.0-dev.1 and ignores inherited OpenCode tags", () => {
16+
expect(
17+
resolveReleaseVersion({
18+
branch: "dev",
19+
tags: ["v1.17.11-main.10", "v0.0.0-202608082302"],
20+
}),
21+
).toEqual({
22+
channel: "dev",
23+
version: "1.0.0-dev.1",
24+
tag: "graphagent-v1.0.0-dev.1",
25+
prerelease: true,
26+
latest: false,
27+
})
28+
})
29+
30+
test("increments the dev sequence within the pending stable version", () => {
31+
expect(
32+
resolveReleaseVersion({
33+
branch: "dev",
34+
tags: ["graphagent-v1.0.0-dev.1", "graphagent-v1.0.0-dev.3"],
35+
}).version,
36+
).toBe("1.0.0-dev.4")
37+
})
38+
39+
test("promotes the pending dev version on main and moves dev to the next patch", () => {
40+
expect(resolveReleaseVersion({ branch: "main", tags: ["graphagent-v1.0.0-dev.4"] }).version).toBe("1.0.0")
41+
expect(resolveReleaseVersion({ branch: "dev", tags: ["graphagent-v1.0.0"] }).version).toBe("1.0.1-dev.1")
42+
expect(
43+
resolveReleaseVersion({
44+
branch: "main",
45+
tags: ["graphagent-v1.0.0", "graphagent-v1.0.1-dev.2"],
46+
}).version,
47+
).toBe("1.0.1")
48+
})
49+
50+
test("rejects releases from feature branches", () => {
51+
expect(() => resolveReleaseVersion({ branch: "feat/example", tags: [] })).toThrow(
52+
"GraphAgent releases require main or dev",
53+
)
54+
})
55+
56+
test("wires one resolved version into both the build and GitHub Release", async () => {
57+
const workflow = await Bun.file(new URL("../../../.github/workflows/release-fork.yml", import.meta.url)).text()
58+
59+
expect(workflow).not.toContain("inputs.version")
60+
expect(workflow).not.toContain("0.0.0-")
61+
expect(workflow).toContain("OPENCODE_CHANNEL: ${{ needs.version.outputs.channel == 'main' && 'latest' || 'dev' }}")
62+
expect(workflow).toContain("OPENCODE_VERSION: ${{ needs.version.outputs.version }}")
63+
expect(workflow).toContain('gh release create "${{ needs.version.outputs.tag }}"')
64+
expect(workflow).toContain("--prerelease --latest=false")
65+
})
66+
})

0 commit comments

Comments
 (0)