Skip to content

Commit f4c9576

Browse files
authored
feat: add OpenCode release automation (#5)
* docs: add comprehensive getting started guide Add GETTING_STARTED.md with step-by-step walkthrough: - Repository structure setup - Configuration examples - First plugin creation - Usage in Claude Code - Troubleshooting common issues - Links from main README and action docs * fix: move GETTING_STARTED.md to docs/ to avoid component scanning * fix: remove docs to unblock CI (will add back separately) * fix: remove getting started links * feat: add OpenCode release automation Add automated OpenCode-compatible plugin release generation to the agentic marketplace workflow. Changes: - Add opencode-release.js script for release automation - Detect changed plugins from marketplace.json - Generate changelogs from conventional commits - Package plugins as zips with CHANGELOG.md and INSTALL.md - Create GitHub releases with date-based tags (plugin-name-vYYYY.MM.DD) - Extend generate action with create-opencode-release input - Update reusable workflow to pass through release parameters - Bundle opencode-release.js in build process Release workflow triggers on main branch when create-opencode-release is enabled. Skips plugins with no changes since last release.
1 parent 068756f commit f4c9576

5 files changed

Lines changed: 661 additions & 3 deletions

File tree

.github/workflows/agentic-marketplace.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ on:
1414
description: 'Preview changes without committing'
1515
default: false
1616
type: boolean
17+
create-opencode-release:
18+
description: 'Create OpenCode-compatible plugin releases'
19+
default: false
20+
type: boolean
21+
release-version:
22+
description: 'Version for releases (default: YYYY.MM.DD)'
23+
default: ''
24+
type: string
1725
secrets:
1826
token:
1927
description: 'GitHub token for PR creation'
@@ -68,3 +76,5 @@ jobs:
6876
config-path: ${{ inputs.config-path }}
6977
github-token: ${{ secrets.token }}
7078
auto-merge: ${{ inputs.auto-merge }}
79+
create-opencode-release: ${{ inputs.create-opencode-release }}
80+
release-version: ${{ inputs.release-version }}

agentic-marketplace/generate/action.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ inputs:
1818
description: 'Preview changes without committing'
1919
required: false
2020
default: 'false'
21+
create-opencode-release:
22+
description: 'Create OpenCode-compatible plugin releases'
23+
required: false
24+
default: 'false'
25+
release-version:
26+
description: 'Version for releases (default: YYYY.MM.DD)'
27+
required: false
28+
default: ''
2129

2230
outputs:
2331
pr-number:
@@ -26,6 +34,9 @@ outputs:
2634
pr-url:
2735
description: 'Pull request URL if created'
2836
value: ${{ steps.create-pr.outputs.pull-request-url }}
37+
releases-created:
38+
description: 'Number of OpenCode releases created'
39+
value: ${{ steps.opencode-release.outputs.count }}
2940

3041
runs:
3142
using: 'composite'
@@ -96,6 +107,29 @@ runs:
96107
echo "No PR created (no changes detected)"
97108
fi
98109
110+
- name: Create OpenCode Releases
111+
id: opencode-release
112+
if: ${{ inputs.create-opencode-release == 'true' && inputs.dry-run != 'true' }}
113+
shell: bash
114+
env:
115+
GITHUB_TOKEN: ${{ inputs.github-token }}
116+
RELEASE_VERSION: ${{ inputs.release-version }}
117+
run: |
118+
set -e
119+
120+
# Use bundled script from this action
121+
SCRIPT_PATH="${GITHUB_ACTION_PATH}/../../scripts/dist/opencode-release.cjs"
122+
123+
if [ ! -f "$SCRIPT_PATH" ]; then
124+
echo "ERROR: Bundled script not found at $SCRIPT_PATH" >&2
125+
exit 1
126+
fi
127+
128+
echo "Creating OpenCode releases..."
129+
node "$SCRIPT_PATH" opencode-release
130+
131+
echo "✓ Release creation complete"
132+
99133
branding:
100134
icon: 'file-text'
101135
color: 'purple'

scripts/build.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ if (!fs.existsSync(distDir)) {
1313
fs.mkdirSync(distDir, { recursive: true });
1414
}
1515

16+
// Bundle discover-components.js
1617
esbuild.buildSync({
1718
entryPoints: [path.join(__dirname, 'src/discover-components.js')],
1819
bundle: true,
@@ -28,7 +29,26 @@ esbuild.buildSync({
2829
});
2930

3031
// Make the output executable
31-
const outputPath = path.join(__dirname, 'dist/discover-components.cjs');
32-
fs.chmodSync(outputPath, 0o755);
33-
32+
const discoverPath = path.join(__dirname, 'dist/discover-components.cjs');
33+
fs.chmodSync(discoverPath, 0o755);
3434
console.log('✓ Bundled discover-components.cjs');
35+
36+
// Bundle opencode-release.js
37+
esbuild.buildSync({
38+
entryPoints: [path.join(__dirname, 'src/opencode-release.js')],
39+
bundle: true,
40+
platform: 'node',
41+
target: 'node20',
42+
outfile: path.join(__dirname, 'dist/opencode-release.cjs'),
43+
banner: {
44+
js: '#!/usr/bin/env node\n'
45+
},
46+
external: [], // Bundle all dependencies
47+
minify: false, // Keep readable for debugging
48+
sourcemap: false
49+
});
50+
51+
// Make the output executable
52+
const releasePath = path.join(__dirname, 'dist/opencode-release.cjs');
53+
fs.chmodSync(releasePath, 0o755);
54+
console.log('✓ Bundled opencode-release.cjs');

scripts/dist/opencode-release.cjs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env node
2+
3+
4+
// scripts/src/opencode-release.js
5+
var fs = require("fs");
6+
var path = require("path");
7+
var { execSync } = require("child_process");
8+
function getLatestTag(pluginName = null) {
9+
try {
10+
const pattern = pluginName ? `${pluginName}-v*` : "*";
11+
const tags = execSync(`git tag -l '${pattern}' --sort=-version:refname`, { encoding: "utf8" }).trim().split("\n").filter(Boolean);
12+
return tags[0] || null;
13+
} catch (err) {
14+
return null;
15+
}
16+
}
17+
function getChangedFiles(since, pathFilter = "") {
18+
try {
19+
const cmd = pathFilter ? `git diff --name-only ${since}...HEAD -- ${pathFilter}` : `git diff --name-only ${since}...HEAD`;
20+
const output = execSync(cmd, { encoding: "utf8" }).trim();
21+
return output ? output.split("\n") : [];
22+
} catch (err) {
23+
console.error(`Error getting changed files: ${err.message}`);
24+
return [];
25+
}
26+
}
27+
function parseConventionalCommits(since, pathFilter = "") {
28+
try {
29+
const cmd = pathFilter ? `git log ${since}...HEAD --pretty=format:"%s" -- ${pathFilter}` : `git log ${since}...HEAD --pretty=format:"%s"`;
30+
const output = execSync(cmd, { encoding: "utf8" }).trim();
31+
const commits = output ? output.split("\n") : [];
32+
const categories = {
33+
features: [],
34+
fixes: [],
35+
other: []
36+
};
37+
commits.forEach((commit) => {
38+
if (commit.startsWith("feat:") || commit.startsWith("feat(")) {
39+
categories.features.push(commit.replace(/^feat(\([^)]+\))?:\s*/, ""));
40+
} else if (commit.startsWith("fix:") || commit.startsWith("fix(")) {
41+
categories.fixes.push(commit.replace(/^fix(\([^)]+\))?:\s*/, ""));
42+
} else if (!commit.startsWith("chore:") && !commit.startsWith("chore(")) {
43+
categories.other.push(commit.replace(/^[a-z]+(\([^)]+\))?:\s*/, ""));
44+
}
45+
});
46+
return categories;
47+
} catch (err) {
48+
console.error(`Error parsing commits: ${err.message}`);
49+
return { features: [], fixes: [], other: [] };
50+
}
51+
}
52+
function generateChangelog(pluginName, version, commits) {
53+
const lines = [
54+
`# Changelog - ${pluginName} v${version}`,
55+
""
56+
];
57+
if (commits.features.length > 0) {
58+
lines.push("## Features");
59+
commits.features.forEach((feat) => lines.push(`- ${feat}`));
60+
lines.push("");
61+
}
62+
if (commits.fixes.length > 0) {
63+
lines.push("## Bug Fixes");
64+
commits.fixes.forEach((fix) => lines.push(`- ${fix}`));
65+
lines.push("");
66+
}
67+
if (commits.other.length > 0) {
68+
lines.push("## Other Changes");
69+
commits.other.forEach((change) => lines.push(`- ${change}`));
70+
lines.push("");
71+
}
72+
return lines.join("\n");
73+
}
74+
function generateInstallGuide(pluginName, version) {
75+
return `# Installation - ${pluginName} v${version}
76+
77+
## Quick Install
78+
79+
\`\`\`bash
80+
# Download and extract to OpenCode plugins directory
81+
curl -L https://github.com/bitcomplete/bc-llm-skills/releases/download/${pluginName}-v${version}/${pluginName}.zip -o ${pluginName}.zip
82+
unzip ${pluginName}.zip -d ~/.config/opencode/plugins/
83+
rm ${pluginName}.zip
84+
\`\`\`
85+
86+
## Manual Install
87+
88+
1. Download \`${pluginName}.zip\` from this release
89+
2. Extract to \`~/.config/opencode/plugins/${pluginName}/\`
90+
3. Restart OpenCode
91+
92+
## Verify Installation
93+
94+
After restarting OpenCode, the plugin commands should appear in autocomplete.
95+
96+
## Platform Notes
97+
98+
**Linux/macOS**: Default location is \`~/.config/opencode/plugins/\`
99+
100+
**Windows**: Use \`%USERPROFILE%\\.config\\opencode\\plugins\\\`
101+
102+
## Compatibility
103+
104+
This plugin works with both OpenCode and Claude Code. Paths are auto-detected at runtime.
105+
`;
106+
}
107+
function createPluginZip(pluginPath, outputPath, changelog, installGuide) {
108+
try {
109+
const tempDir = path.join(process.cwd(), ".tmp-release");
110+
const pluginName = path.basename(pluginPath);
111+
const stagingDir = path.join(tempDir, pluginName);
112+
if (fs.existsSync(tempDir)) {
113+
fs.rmSync(tempDir, { recursive: true });
114+
}
115+
fs.mkdirSync(stagingDir, { recursive: true });
116+
execSync(`cp -R "${pluginPath}"/* "${stagingDir}/"`, { stdio: "inherit" });
117+
fs.writeFileSync(path.join(stagingDir, "CHANGELOG.md"), changelog);
118+
fs.writeFileSync(path.join(stagingDir, "INSTALL.md"), installGuide);
119+
const zipFile = path.basename(outputPath);
120+
execSync(`cd "${tempDir}" && zip -r "${zipFile}" "${pluginName}"`, { stdio: "inherit" });
121+
execSync(`mv "${tempDir}/${zipFile}" "${outputPath}"`, { stdio: "inherit" });
122+
fs.rmSync(tempDir, { recursive: true });
123+
console.log(`\u2713 Created ${outputPath}`);
124+
} catch (err) {
125+
console.error(`Error creating zip: ${err.message}`);
126+
throw err;
127+
}
128+
}
129+
function createGitHubRelease(pluginName, version, zipPath, changelog) {
130+
try {
131+
const tag = `${pluginName}-v${version}`;
132+
const title = `${pluginName} v${version}`;
133+
const notes = `OpenCode-compatible release of ${pluginName}.
134+
135+
${changelog}
136+
137+
## Installation
138+
139+
Download \`${pluginName}.zip\` and extract to \`~/.config/opencode/plugins/\`
140+
141+
See INSTALL.md in the zip for detailed instructions.
142+
`;
143+
const notesFile = path.join(process.cwd(), `.tmp-notes-${pluginName}.md`);
144+
fs.writeFileSync(notesFile, notes);
145+
execSync(
146+
`gh release create "${tag}" "${zipPath}" --title "${title}" --notes-file "${notesFile}"`,
147+
{ stdio: "inherit" }
148+
);
149+
fs.unlinkSync(notesFile);
150+
console.log(`\u2713 Created release: ${tag}`);
151+
} catch (err) {
152+
console.error(`Error creating GitHub release: ${err.message}`);
153+
throw err;
154+
}
155+
}
156+
function detectChangedPlugins(marketplacePath) {
157+
if (!fs.existsSync(marketplacePath)) {
158+
console.error(`Marketplace file not found: ${marketplacePath}`);
159+
return [];
160+
}
161+
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
162+
const changedPlugins = [];
163+
for (const plugin of marketplace.plugins) {
164+
const pluginName = plugin.name;
165+
const pluginPath = path.dirname(plugin.source);
166+
const lastTag = getLatestTag(pluginName) || getLatestTag();
167+
if (!lastTag) {
168+
console.log(`No previous releases found for ${pluginName}, skipping`);
169+
continue;
170+
}
171+
const changedFiles = getChangedFiles(lastTag, pluginPath);
172+
if (changedFiles.length > 0) {
173+
console.log(`\u2713 Changes detected in ${pluginName} (${changedFiles.length} files)`);
174+
changedPlugins.push({
175+
name: pluginName,
176+
path: pluginPath,
177+
lastTag,
178+
changedFiles
179+
});
180+
}
181+
}
182+
return changedPlugins;
183+
}
184+
function main() {
185+
const command = process.argv[2];
186+
if (command !== "opencode-release") {
187+
console.log("Usage: node opencode-release.js opencode-release");
188+
process.exit(1);
189+
}
190+
const marketplacePath = path.join(".claude-plugin", "marketplace.json");
191+
const releasesDir = path.join(process.cwd(), ".releases");
192+
const version = process.env.RELEASE_VERSION || (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, ".");
193+
console.log("Detecting changed plugins...");
194+
const changedPlugins = detectChangedPlugins(marketplacePath);
195+
if (changedPlugins.length === 0) {
196+
console.log("No plugins with changes detected");
197+
process.exit(0);
198+
}
199+
if (!fs.existsSync(releasesDir)) {
200+
fs.mkdirSync(releasesDir, { recursive: true });
201+
}
202+
let releasesCreated = 0;
203+
for (const plugin of changedPlugins) {
204+
console.log(`
205+
Creating release for ${plugin.name}...`);
206+
const commits = parseConventionalCommits(plugin.lastTag, plugin.path);
207+
const changelog = generateChangelog(plugin.name, version, commits);
208+
const installGuide = generateInstallGuide(plugin.name, version);
209+
const zipPath = path.join(releasesDir, `${plugin.name}.zip`);
210+
createPluginZip(plugin.path, zipPath, changelog, installGuide);
211+
createGitHubRelease(plugin.name, version, zipPath, changelog);
212+
releasesCreated++;
213+
}
214+
console.log(`
215+
\u2713 Created ${releasesCreated} OpenCode release(s)`);
216+
if (process.env.GITHUB_OUTPUT) {
217+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `count=${releasesCreated}
218+
`);
219+
}
220+
}
221+
if (require.main === module) {
222+
main();
223+
}
224+
module.exports = {
225+
getLatestTag,
226+
getChangedFiles,
227+
parseConventionalCommits,
228+
generateChangelog,
229+
generateInstallGuide,
230+
createPluginZip,
231+
createGitHubRelease,
232+
detectChangedPlugins
233+
};

0 commit comments

Comments
 (0)