opencode-github-release 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +31 -1
  2. package/dist/index.js +88 -11
  3. package/package.json +5 -1
package/README.md CHANGED
@@ -20,7 +20,7 @@ Restart OpenCode. The plugin loads automatically.
20
20
 
21
21
  ## Usage
22
22
 
23
- Ask OpenCode to create a release. The plugin provides a `create_release` tool with these modes:
23
+ Ask OpenCode to create a release. The plugin provides `create_release` and `suggest_bump` tools. When you give a specific version or bump, `create_release` is called directly. When you don't, `suggest_bump` analyzes your commits first and asks for confirmation. Modes:
24
24
 
25
25
  **Auto-bump from latest tag:**
26
26
  ```
@@ -39,6 +39,36 @@ Release version 2.0.0
39
39
  Create a patch release with notes "Fixed login bug"
40
40
  ```
41
41
 
42
+ **Auto-suggest from git history:**
43
+ ```
44
+ Create a release
45
+ ```
46
+
47
+ ## How it works
48
+
49
+ When you request a release, the plugin fetches the latest git tag from the repository and computes the next version from it. If you specify a bump (`patch`/`minor`/`major`) or an explicit version, it creates the tag and release immediately. If you don't specify either (e.g., just "create a release"), the plugin instead runs `suggest_bump` — it analyzes commits since the latest tag using conventional commit conventions: `fix` → patch, `feat` → minor, `BREAKING CHANGE` → major — and presents the suggestion for your confirmation before proceeding.
50
+
51
+ If you don't provide release notes, the plugin automatically generates them from the commit history via `gh release create --generate-notes`.
52
+
53
+ `create_release` expects conventional commit messages (`feat:`, `fix:`, `BREAKING CHANGE:`) for accurate auto-suggestion. Commits that follow this convention are classified as:
54
+
55
+ - `fix:` → patch bump
56
+ - `feat:` → minor bump
57
+ - `BREAKING CHANGE:` or `feat!:` / `fix!:` → major bump
58
+
59
+ Example output when running `suggest_bump`:
60
+
61
+ ```
62
+ Latest tag: v0.1.0
63
+ Commits: 3
64
+
65
+ [fix] a1b2c3d Fix login timeout
66
+ [feat] e4f5g6h Add user dashboard
67
+ [BREAKING] i7j8k9l Redesign auth API
68
+
69
+ Suggested bump: major -> v1.0.0
70
+ ```
71
+
42
72
  ## Prerequisites
43
73
 
44
74
  - [Git](https://git-scm.com/)
package/dist/index.js CHANGED
@@ -21,34 +21,111 @@ function bumpVersion(current, bump) {
21
21
  }
22
22
  return `v${major}.${minor}.${patch}`;
23
23
  }
24
+ function classifyCommit(subject) {
25
+ const breaking = /^BREAKING CHANGE:/im.test(subject) || /^\w+(\([^)]*\))?!:/m.test(subject);
26
+ const match = subject.match(/^(\w+)(\([^)]*\))?(!)?\s*:/);
27
+ const type = match?.[1]?.toLowerCase();
28
+ if (type === "feat") return { type: "feat", breaking };
29
+ if (type === "fix") return { type: "fix", breaking };
30
+ return { type: "other", breaking };
31
+ }
24
32
  var plugin = async ({ $ }) => {
25
33
  return {
26
34
  tool: {
35
+ suggest_bump: tool({
36
+ description: "Analyze git history since the latest tag and suggest a semantic version bump. Call this tool when the user asks to create a release but does NOT specify patch/minor/major or an explicit version string. After receiving the suggestion, present it to the user and ask for confirmation before calling create_release.",
37
+ args: {},
38
+ async execute(_args, _context) {
39
+ await $`git fetch --tags --force 2>/dev/null || true`.quiet();
40
+ const tagResult = await $`git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"`.text();
41
+ const latestTag = tagResult.trim();
42
+ const logText = await $`git log ${latestTag}..HEAD --oneline 2>/dev/null || true`.text();
43
+ if (!logText) {
44
+ return `No new commits since ${latestTag}. No release needed.`;
45
+ }
46
+ const lines = logText.split("\n");
47
+ const entries = lines.map((line) => {
48
+ const hash = line.split(/\s+/)[0];
49
+ const subject = line.slice(hash.length).trim();
50
+ const { type, breaking } = classifyCommit(subject);
51
+ return { hash, subject, type, breaking };
52
+ });
53
+ let suggestedBump = "patch";
54
+ for (const entry of entries) {
55
+ if (entry.breaking) {
56
+ suggestedBump = "major";
57
+ break;
58
+ }
59
+ if (entry.type === "feat") {
60
+ suggestedBump = "minor";
61
+ }
62
+ }
63
+ const output = entries.map((e) => {
64
+ const tag = e.breaking ? "[BREAKING]" : e.type === "feat" ? "[feat]" : e.type === "fix" ? "[fix]" : " ";
65
+ return ` ${tag} ${e.hash} ${e.subject}`;
66
+ }).join("\n");
67
+ return [
68
+ `Latest tag: ${latestTag}`,
69
+ `Commits: ${entries.length}`,
70
+ "",
71
+ output,
72
+ "",
73
+ `Suggested bump: ${suggestedBump} -> ${bumpVersion(latestTag, suggestedBump)}`
74
+ ].join("\n");
75
+ }
76
+ }),
27
77
  create_release: tool({
28
- description: 'Create a git tag and publish a GitHub release with semantic versioning. Provide either `bump` to auto-compute the next version from the latest tag, or an explicit `version` string (e.g. "2.0.0" or "v2.0.0").',
78
+ description: 'Create a git tag and publish a GitHub release with semantic versioning. Provide either `bump` to auto-compute the next version from the latest tag, or an explicit `version` string (e.g. "2.0.0" or "v2.0.0"). If the user only asks to "create a release" without specifying a bump or version, call suggest_bump first instead of this tool.',
29
79
  args: {
30
80
  bump: tool.schema.enum(["patch", "minor", "major"]).optional(),
31
81
  version: tool.schema.string().optional(),
32
- notes: tool.schema.string().optional()
82
+ notes: tool.schema.string().optional(),
83
+ force: tool.schema.boolean().optional()
33
84
  },
34
85
  async execute(args, _context) {
35
- const { bump, version, notes } = args;
86
+ const { bump, version, notes, force } = args;
36
87
  if (!bump && !version) {
37
88
  throw new Error("Provide either `bump` (patch/minor/major) or an explicit `version` string");
38
89
  }
39
- await $`git fetch --tags --force 2>/dev/null || true`;
40
- const result = await $`git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"`;
41
- const latestTag = result.stdout.toString().trim();
90
+ const status = (await $`git status --porcelain`.text()).trim();
91
+ if (status && !force) {
92
+ const count = status.split("\n").length;
93
+ return `${count} uncommitted file(s) detected. Call create_release with force: true to proceed anyway, or commit/stash first.`;
94
+ }
95
+ await $`git fetch --tags --force 2>/dev/null || true`.quiet();
96
+ const tagResult = await $`git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"`.text();
97
+ const latestTag = tagResult.trim();
42
98
  const newTag = version ? version.startsWith("v") ? version : `v${version}` : bumpVersion(latestTag, bump);
99
+ const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
100
+ let unpushedInfo = "";
101
+ if (branch !== "HEAD") {
102
+ const unpushed = (await $`git log origin/${branch}..HEAD --oneline 2>/dev/null || true`.text()).trim();
103
+ if (unpushed) {
104
+ unpushedInfo = `${unpushed.split("\n").length} unpushed commit(s) will be included in this release.`;
105
+ }
106
+ }
107
+ const hasPkg = (await $`test -f package.json && echo "yes" || echo "no"`.text()).trim() === "yes";
108
+ if (hasPkg) {
109
+ const bareVersion = newTag.replace(/^v/, "");
110
+ await $`npm version ${bareVersion} --no-git-tag-version`.quiet();
111
+ await $`git add package.json package-lock.json 2>/dev/null || true`.quiet();
112
+ await $`git commit -m ${`chore(release): bump version to ${newTag}`}`.quiet();
113
+ if (branch !== "HEAD") {
114
+ await $`git push origin ${branch}`.quiet();
115
+ }
116
+ }
43
117
  const message = notes || `Release ${newTag}`;
44
- await $`git tag -a ${newTag} -m ${message}`;
45
- await $`git push origin ${newTag}`;
118
+ await $`git tag -a ${newTag} -m ${message}`.quiet();
119
+ await $`git push origin ${newTag}`.quiet();
46
120
  if (notes) {
47
- await $`gh release create ${newTag} --title ${newTag} --notes ${notes}`;
121
+ await $`gh release create ${newTag} --title ${newTag} --notes ${notes}`.quiet();
48
122
  } else {
49
- await $`gh release create ${newTag} --title ${newTag} --generate-notes`;
123
+ await $`gh release create ${newTag} --title ${newTag} --generate-notes`.quiet();
50
124
  }
51
- return `\u2713 Created and published ${newTag} (bumped from ${latestTag})`;
125
+ let result = `Created and published ${newTag} (bumped from ${latestTag})`;
126
+ if (unpushedInfo) result += `
127
+ ${unpushedInfo}`;
128
+ return result;
52
129
  }
53
130
  })
54
131
  }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "opencode-github-release",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode plugin to create git tags and publish GitHub releases with semantic versioning.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/amestsantim/opencode-github-release"
8
+ },
5
9
  "type": "module",
6
10
  "main": "dist/index.js",
7
11
  "types": "dist/index.d.ts",