Here’s a complete guide with code samples showing how to parse JSONC (JSON withComments) in JavaScript/Node.js, Pythonand Go, along with notes on available tooling support in each ecosystem.
๐งฉ What is JSONC?
JSONC (JSON with Comments) is an extension of JSON (JavaScript Object Notation) that allows comments (using // or /* */), making it more human-readable — useful for configuration files.
However, standard JSON parsers do not accept comments, so special handling is needed.
๐ 1. JavaScript / Node.js
✅ Tooling Support:
jsonc-parser(from Microsoft) — widely used, supports parsing and formatting.- VS Code uses
jsonc-parserinternally for.jsoncfiles.
๐ป Install:
npm install jsonc-parser
๐ง Example Code:
// parse-jsonc.js
import { parse } from 'jsonc-parser';
const jsoncText = `
{
// This is a comment
"name": "ChatGPT",
"version": 5,
/* multi-line comment */
"features": ["AI", "Chat", "Knowledge"]
}
`;
const data = parse(jsoncText);
console.log(data);
๐งพ Output:
{
"name": "ChatGPT",
"version": 5,
"features": ["AI", "Chat", "Knowledge"]
}
✅ Note:
You can also strip comments manually using libraries like strip-json-comments:
import fs from 'fs';
import stripJsonComments from 'strip-json-comments';
const fileData = fs.readFileSync('config.jsonc', 'utf8');
const cleanJson = stripJsonComments(fileData);
const data = JSON.parse(cleanJson);
๐ 2. Python
✅ Tooling Support:
commentjson– a drop-in replacement forjsonthat supports comments.json5– also parses JSON with comments and trailing commas.
๐ป Install:
pip install commentjson
๐ง Example Code:
# parse_jsonc.py
import commentjson
jsonc_text = """
{
// Single-line comment
"language": "Python",
"supports": ["JSON", "JSONC"],
/* Multi-line
comment */
"version": 3.11
}
"""
data = commentjson.loads(jsonc_text)
print(data)
๐งพ Output:
{'language': 'Python', 'supports': ['JSON', 'JSONC'], 'version': 3.11}
✅ Alternative using json5:
import json5
data = json5.loads(jsonc_text)
๐น 3. Go (Golang)
✅ Tooling Support:
- Go’s standard library
encoding/jsondoes not support comments. - Use libraries like:
jsonc(github.com/tidwall/jsonc)github.com/muhammadmuzzammil1998/jsonc
๐ป Install:
go get github.com/tidwall/jsonc
๐ง Example Code:
// parse_jsonc.go
package main
import (
"fmt"
"github.com/tidwall/jsonc"
"encoding/json"
)
func main() {
jsoncData := `
{
// JSONC example
"language": "Go",
"version": 1.23,
/* Comment block */
"features": ["Fast", "Concurrent"]
}`
// Remove comments
clean := jsonc.ToJSON(jsoncData)
var data map[string]interface{}
json.Unmarshal([]byte(clean), &data)
fmt.Println(data)
}
๐งพ Output:
map[language:Go version:1.23 features:[Fast Concurrent]]
๐งฐ ๐ง Key Takeaways
- JSONC is not officially part of the JSON standard but enhances readability.
- Ideal for configuration files where comments help documentation.
- Before parsing, JSONC text must have its comments removed or ignored by a compatible parser.
- Native JSON parsers (like
JSON.parse,json.loads,encoding/json) will fail if comments are present.
Examples for writing JSONC back to files (serialization) in these languages too?
Perfect ✅ — here are examples for writing (serializing) JSONC data back to files in
JavaScript/Node.js, Python, and Go, along with notes on tooling that supports writing JSONC safely (preserving comments when possible).
๐ 1. JavaScript / Node.js — Write JSONC to File
✅ Tooling Support:
jsonc-parsercan modify JSONC text while preserving comments.strip-json-comments+JSON.stringifycan be used to save plain JSON (comments lost).
๐ป Install:
npm install jsonc-parser
๐ง Example Code (Preserving Comments):
// write-jsonc.js
import fs from 'fs';
import { modify, applyEdits } from 'jsonc-parser';
const jsoncText = `
{
// Configuration for ChatGPT
"model": "GPT-5",
"temperature": 0.8
}
`;
// Add a new property to the existing JSONC while preserving comments
const edits = modify(jsoncText, ['max_tokens'], 2048, { formattingOptions: { insertSpaces: true, tabSize: 2 } });
const updatedText = applyEdits(jsoncText, edits);
// Write back to file
fs.writeFileSync('config.jsonc', updatedText, 'utf8');
console.log('✅ config.jsonc updated successfully!');
๐งพ Resulting config.jsonc:
{
// Configuration for ChatGPT
"model": "GPT-5",
"temperature": 0.8,
"max_tokens": 2048
}
๐งฉ If you don’t need to preserve comments:
const fs = require('fs');
const data = {
name: 'ChatGPT',
version: 5,
features: ['AI', 'Chat', 'Knowledge']
};
fs.writeFileSync('data.jsonc', JSON.stringify(data, null, 2));
๐ 2. Python —
Write JSONC to File
✅ Tooling Support:
commentjsoncan dump JSONC with comments, but you must include them manually as part of the string.json5can serialize JSON5/JSONC-like data (with optional comments manually added).
๐ป Example Code:
# write_jsonc.py
import commentjson
data = {
"name": "ChatGPT",
"version": 5,
"languages": ["Python", "JavaScript", "Go"]
}
# Serialize to JSON (comments must be added manually)
json_text = commentjson.dumps(data, indent=2)
# Add a comment line manually
jsonc_output = f"// Example JSONC output\n{json_text}"
with open("data.jsonc", "w") as f:
f.write(jsonc_output)
print("✅ data.jsonc written successfully!")
๐งพ Output (data.jsonc):
// Example JSONC output
{
"name": "ChatGPT",
"version": 5,
"languages": [
"Python",
"JavaScript",
"Go"
]
}
๐ง Note: There is no automated Python library that preserves comments round-trip, so comments are usually inserted manually.
๐น 3. Go (Golang) — Write JSONC to File
✅ Tooling Support:
- Go libraries like
tidwall/jsoncstrip comments during parsing, so you’ll need to reinsert them manually. - You can write regular JSON and prepend comments.
๐ป Example Code:
// write_jsonc.go
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
data := map[string]interface{}{
"app": "ChatGPT",
"version": 5,
"features": []string{"AI", "Chat", "Knowledge"},
}
jsonBytes, _ := json.MarshalIndent(data, "", " ")
// Add JSONC-style comment manually
jsoncOutput := "// Example JSONC configuration\n" + string(jsonBytes)
os.WriteFile("config.jsonc", []byte(jsoncOutput), 0644)
fmt.Println("✅ config.jsonc written successfully!")
}
๐งพ Output (config.jsonc):
// Example JSONC configuration
{
"app": "ChatGPT",
"version": 5,
"features": [
"AI",
"Chat",
"Knowledge"
]
}
๐งฐ
๐ง Key Tips
- Round-trip editing (preserve + modify + write) is easiest in Node.js via
jsonc-parser. - Python and Go lack native comment-preserving JSONC writers; treat comments as plain text.
- For config files, consider storing templates (
.jsonc) and modifying them using string operations if preserving comments matters.
A practical real-world example (like editing a settings.jsonc config file for an app) using all three languages next?
Excellent ✅ — let’s build a real-world JSONC configuration editing example in
JavaScript (Node.js), Python, and Go, showing how to:
- Load a
.jsoncconfiguration file with comments. - Modify a setting (e.g.,
themeorautoSave). - Write it back while keeping the original comments intact (where possible).
⚙️ Example Scenario — settings.jsonc
We’ll use this same JSONC file for all languages:
{
// Application display settings
"theme": "light",
// Auto-save interval in minutes
"autoSave": 10,
/* Enable experimental features */
"experimental": false
}
Our goal:
✅ Change "theme" to "dark"
✅ Change "experimental" to true
✅ Save back the updated file
๐ 1. JavaScript / Node.js — Best for True JSONC Preservation
✅ Tool: jsonc-parser
Maintains formatting + comments automatically.
๐ป Install:
npm install jsonc-parser
๐ง Code: editSettings.js
import fs from "fs";
import { modify, applyEdits } from "jsonc-parser";
const filePath = "settings.jsonc";
// Read original JSONC file
const jsoncText = fs.readFileSync(filePath, "utf8");
// Change "theme" to "dark"
let edits = modify(jsoncText, ["theme"], "dark", { formattingOptions: { insertSpaces: true, tabSize: 2 } });
let updatedText = applyEdits(jsoncText, edits);
// Update "experimental" to true
edits = modify(updatedText, ["experimental"], true, { formattingOptions: { insertSpaces: true, tabSize: 2 } });
updatedText = applyEdits(updatedText, edits);
// Save updated JSONC back
fs.writeFileSync(filePath, updatedText, "utf8");
console.log("✅ settings.jsonc updated successfully!");
๐งพ Result (settings.jsonc):
{
// Application display settings
"theme": "dark",
// Auto-save interval in minutes
"autoSave": 10,
/* Enable experimental features */
"experimental": true
}
๐ง Preserves all comments — the cleanest approach.
๐ 2. Python — Simple, but Manual Comment Handling
✅ Tool: commentjson
Reads JSONC but cannot auto-preserve comments; we’ll handle them manually.
๐ป Install:
pip install commentjson
๐ง Code: edit_settings.py
import commentjson
file_path = "settings.jsonc"
# Read JSONC text
with open(file_path, "r") as f:
jsonc_text = f.read()
# Parse ignoring comments
data = commentjson.loads(jsonc_text)
# Modify values
data["theme"] = "dark"
data["experimental"] = True
# Serialize back (comments will be added manually)
json_output = commentjson.dumps(data, indent=2)
final_output = """// Application configuration (Python updated)\n""" + json_output
# Write back
with open(file_path, "w") as f:
f.write(final_output)
print("✅ settings.jsonc updated successfully!")
๐งพ Result:
// Application configuration (Python updated)
{
"theme": "dark",
"autoSave": 10,
"experimental": true
}
⚠️ Note: Original inline comments are lost — Python doesn’t preserve them automatically.
๐น 3. Go — Manual Comment Re-Insertion
✅ Tool: tidwall/jsonc (for parsing JSONC)
Go’s native encoding/json is used for writing, with comment reinsertion by string concatenation.
๐ป Install:
go get github.com/tidwall/jsonc
๐ง Code: edit_settings.go
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/tidwall/jsonc"
)
func main() {
filePath := "settings.jsonc"
// Read JSONC file
jsoncData, err := os.ReadFile(filePath)
if err != nil {
panic(err)
}
// Remove comments and parse
clean := jsonc.ToJSON(string(jsoncData))
var data map[string]interface{}
if err := json.Unmarshal([]byte(clean), &data); err != nil {
panic(err)
}
// Modify values
data["theme"] = "dark"
data["experimental"] = true
// Serialize back to JSON
jsonBytes, _ := json.MarshalIndent(data, "", " ")
// Re-add comments manually
jsoncOutput := `{
// Application display settings
"theme": "dark",
// Auto-save interval in minutes
"autoSave": 10,
/* Enable experimental features */
"experimental": true
}`
os.WriteFile(filePath, []byte(jsoncOutput), 0644)
fmt.Println("✅ settings.jsonc updated successfully!")
}
๐ง Manual reinsertion is required — Go’s parser doesn’t preserve comments.
๐งฐ Best Practices for Real-World Use
- For JSONC-heavy configs (like VS Code, TypeScript) → Use Node.js with
jsonc-parser. - For Python scripts → Use
commentjsonfor reading, then rewrite with a comment header. - For Go CLI tools → Store original comments separately or use templates for regenerating configs.
- Never use plain
JSON.parse()orencoding/jsondirectly on JSONC — they will throw syntax errors. - Keep a
.jsonctemplate file and modify it programmatically for safe round-trip editing.
Add a cross-language CLI demo (where all three scripts modify the same JSONC file sequentially and produce versioned backups like settings.v1.jsonc, settings.v2.jsonc, etc.)?
Perfect ๐ฅ — Let’s now build a cross-language CLI demo where JavaScript, Python, and Go scripts all edit the same settings.jsonc file sequentially, creating versioned backups like:
settings.v1.jsonc ← Node.js update
settings.v2.jsonc ← Python update
settings.v3.jsonc ← Go update
This demo shows how to maintain a versioned, comment-preserving JSONC configuration workflow across multiple languages.
๐งฉ Base Configuration File: settings.jsonc
We’ll start with this file in the working directory:
{
// Application display settings
"theme": "light",
// Auto-save interval in minutes
"autoSave": 10,
/* Enable experimental features */
"experimental": false
}
๐ Step 1: Node.js Script — Create settings.v1.jsonc
✅ Tool: jsonc-parser
๐ป edit_node.js
import fs from "fs";
import { modify, applyEdits } from "jsonc-parser";
const input = "settings.jsonc";
const output = "settings.v1.jsonc";
let jsoncText = fs.readFileSync(input, "utf8");
// Update theme to "dark"
let edits = modify(jsoncText, ["theme"], "dark", { formattingOptions: { insertSpaces: true, tabSize: 2 } });
let updated = applyEdits(jsoncText, edits);
// Save version 1
fs.writeFileSync(output, updated, "utf8");
console.log("✅ Node.js: Created settings.v1.jsonc");
๐งพ Result (settings.v1.jsonc):
{
// Application display settings
"theme": "dark",
// Auto-save interval in minutes
"autoSave": 10,
/* Enable experimental features */
"experimental": false
}
๐ Step 2: Python Script — Create settings.v2.jsonc
✅ Tool: commentjson
๐ป edit_python.py
import commentjson
input_file = "settings.v1.jsonc"
output_file = "settings.v2.jsonc"
# Read JSONC text
with open(input_file, "r") as f:
jsonc_text = f.read()
# Parse ignoring comments
data = commentjson.loads(jsonc_text)
# Modify configuration
data["autoSave"] = 15
data["experimental"] = True
# Serialize back (adding version comment)
json_text = commentjson.dumps(data, indent=2)
final_output = f"// Updated by Python script\n{json_text}"
# Write new version
with open(output_file, "w") as f:
f.write(final_output)
print("✅ Python: Created settings.v2.jsonc")
๐งพ Result (settings.v2.jsonc):
// Updated by Python script
{
"theme": "dark",
"autoSave": 15,
"experimental": true
}
⚠️ Inline comments from v1 are lost, but Python adds a header comment.
๐น Step 3: Go Script — Create settings.v3.jsonc
✅ Tool: tidwall/jsonc
๐ป edit_go.go
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/tidwall/jsonc"
)
func main() {
inputFile := "settings.v2.jsonc"
outputFile := "settings.v3.jsonc"
data, err := os.ReadFile(inputFile)
if err != nil {
panic(err)
}
// Remove comments before parsing
clean := jsonc.ToJSON(string(data))
var config map[string]interface{}
if err := json.Unmarshal([]byte(clean), &config); err != nil {
panic(err)
}
// Modify Go-specific property
config["compiledBy"] = "GoLang v1.23"
// Serialize and reinsert comments manually
jsonBytes, _ := json.MarshalIndent(config, "", " ")
jsoncOutput := fmt.Sprintf("// Updated by Go script\n%s", string(jsonBytes))
os.WriteFile(outputFile, []byte(jsoncOutput), 0644)
fmt.Println("✅ Go: Created settings.v3.jsonc")
}
๐งพ Result (settings.v3.jsonc):
// Updated by Go script
{
"theme": "dark",
"autoSave": 15,
"experimental": true,
"compiledBy": "GoLang v1.23"
}
๐งฐ How to Run Sequentially
From a terminal:
# 1️⃣ Run Node.js
node edit_node.js
# 2️⃣ Run Python
python edit_python.py
# 3️⃣ Run Go
go run edit_go.go
✅ You’ll end up with:
settings.jsonc ← Original file
settings.v1.jsonc ← Node.js updated version
settings.v2.jsonc ← Python updated version
settings.v3.jsonc ← Go updated version
๐ Real-World Applications
This approach can be used to:
- Manage cross-language configuration pipelines.
- Build CLI tools that process JSONC configs consistently.
- Automate app configuration versioning (useful for dev environments).
- Enable multi-language tooling (e.g., Node.js backend + Go microservice + Python utility).
Extended this example into a cross-language automation script (like a master shell or Node.js controller script that runs all three sequentially and logs version diffs)?
Perfect ๐ฅ — let’s now take this to the next level:
we’ll build a cross-language automation pipeline that:
- Runs Node.js, Python, and Go scripts sequentially.
- Creates versioned
.jsoncfiles (v1,v2,v3). - Logs what changed between versions (a small “diff” summary).
We’ll implement this with one master controller script (using Node.js) — because Node can easily call external processes and handle file I/O.
๐งฉ Folder Setup
jsonc-demo/
├── settings.jsonc ← Base config
├── edit_node.js ← Node.js step
├── edit_python.py ← Python step
├── edit_go.go ← Go step
├── run_pipeline.js ← Master controller
Make sure the other three scripts (edit_node.js, edit_python.py, edit_go.go) are the same as before.
⚙️ Step 1: Create run_pipeline.js
✅ Purpose:
- Runs each script one by one.
- Creates versioned files automatically.
- Compares file differences between steps.
๐ป Code: run_pipeline.js
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// Utility to log version differences
function logDiff(oldFile, newFile) {
const oldText = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, "utf8") : "";
const newText = fs.readFileSync(newFile, "utf8");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
console.log(`\n๐ Diff between ${path.basename(oldFile)} and ${path.basename(newFile)}:`);
newLines.forEach((line, i) => {
if (oldLines[i] !== line) {
console.log(` ➕ ${line.trim()}`);
}
});
}
try {
console.log("๐ Starting cross-language JSONC pipeline...\n");
// Step 1: Run Node.js script
execSync("node edit_node.js", { stdio: "inherit" });
// Step 2: Run Python script
execSync("python edit_python.py", { stdio: "inherit" });
// Step 3: Run Go script
execSync("go run edit_go.go", { stdio: "inherit" });
console.log("\n✅ All scripts executed successfully!");
// Compare outputs
logDiff("settings.jsonc", "settings.v1.jsonc");
logDiff("settings.v1.jsonc", "settings.v2.jsonc");
logDiff("settings.v2.jsonc", "settings.v3.jsonc");
console.log("\n๐ Pipeline completed! Check versioned files for final configurations.");
} catch (err) {
console.error("❌ Error running pipeline:", err.message);
}
๐ง How It Works
The master Node.js script executes:
node edit_node.jspython edit_python.pygo run edit_go.go
Each script:
- Reads the previous JSONC version.
- Updates specific fields.
- Writes out a new version (
settings.v1.jsonc, etc.).
The controller then prints differences between each version:
- Added/modified lines are prefixed with
➕.
- Added/modified lines are prefixed with
๐งพ Example Console Output
When you run:
node run_pipeline.js
You’ll see:
๐ Starting cross-language JSONC pipeline...
✅ Node.js: Created settings.v1.jsonc
✅ Python: Created settings.v2.jsonc
✅ Go: Created settings.v3.jsonc
✅ All scripts executed successfully!
๐ Diff between settings.jsonc and settings.v1.jsonc:
➕ "theme": "dark",
๐ Diff between settings.v1.jsonc and settings.v2.jsonc:
➕ "autoSave": 15,
➕ "experimental": true
๐ Diff between settings.v2.jsonc and settings.v3.jsonc:
➕ "compiledBy": "GoLang v1.23"
๐ Pipeline completed! Check versioned files for final configurations.
๐งฐ Real-World Benefits
✅ Demonstrates multi-language JSONC compatibility.
✅ Enables cross-team configuration pipelines (e.g., Node backend + Python analytics + Go service).
✅ Shows how comments survive or are re-added.
✅ Creates audit trail via versioned files and diffs.
๐ก Optional Extension Ideas
You can enhance this demo further by:
- ✨ Auto-generating version numbers dynamically (using timestamps).
- ๐️ Storing diffs in a
changelog.txtfile. - ๐งช Running all steps inside a Docker container for reproducibility.
- ๐ Using Node.js to automatically revert to a specific version.
Add the optional extension — for example, an automated changelog generator (changelog.txt) that logs each version change with timestamps and diffs?
Fantastic ⚡ — let’s now upgrade your cross-language JSONC automation pipeline with a
๐ Changelog Generator that automatically logs every version change, timestamp, and diff summary into a changelog.txt file.
This creates a complete audit trail for how your JSONC configuration evolves across Node.js, Python, and Go.
๐งฉ Updated Folder Structure
jsonc-demo/
├── settings.jsonc
├── edit_node.js
├── edit_python.py
├── edit_go.go
├── run_pipeline.js ← Master automation controller
├── changelog.txt ← Auto-generated
⚙️ Step 1: Enhanced Master Script — run_pipeline.js
We’ll extend the controller to:
- Run all language scripts sequentially.
- Log changes between versions.
- Append readable diffs with timestamps into
changelog.txt.
๐ป run_pipeline.js
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// === Utility: Compare two JSONC files line-by-line ===
function getDiff(oldFile, newFile) {
const oldText = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, "utf8") : "";
const newText = fs.readFileSync(newFile, "utf8");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const diffLines = [];
newLines.forEach((line, i) => {
if (oldLines[i] !== line) {
diffLines.push(` ➕ ${line.trim()}`);
}
});
return diffLines.join("\n");
}
// === Utility: Append changelog entry ===
function logChange(oldFile, newFile) {
const timestamp = new Date().toISOString().replace("T", " ").split(".")[0];
const diffText = getDiff(oldFile, newFile);
const logEntry = `
๐ ${timestamp}๐ From: ${path.basename(oldFile)}
๐ To: ${path.basename(newFile)}
๐ Changes:
${diffText || " (No visible changes)"}`;
fs.appendFileSync("changelog.txt", logEntry, "utf8");
console.log(`๐ Logged changes from ${oldFile} → ${newFile}`);
}
// === Master runner ===
try {
console.log("๐ Starting Cross-Language JSONC Pipeline...\n");
// Clean up old changelog
if (fs.existsSync("changelog.txt")) fs.unlinkSync("changelog.txt");
// 1️⃣ Run Node.js
execSync("node edit_node.js", { stdio: "inherit" });
// 2️⃣ Run Python
execSync("python edit_python.py", { stdio: "inherit" });
// 3️⃣ Run Go
execSync("go run edit_go.go", { stdio: "inherit" });
console.log("\n✅ All scripts executed successfully!\n");
// === Create changelog entries ===
logChange("settings.jsonc", "settings.v1.jsonc");
logChange("settings.v1.jsonc", "settings.v2.jsonc");
logChange("settings.v2.jsonc", "settings.v3.jsonc");
console.log("\n๐ Pipeline completed! See 'changelog.txt' for version history.\n");
} catch (err) {
console.error("❌ Pipeline failed:", err.message);
}
๐งพ Example Output (Terminal)
๐ Starting Cross-Language JSONC Pipeline...
✅ Node.js: Created settings.v1.jsonc
✅ Python: Created settings.v2.jsonc
✅ Go: Created settings.v3.jsonc
✅ All scripts executed successfully!
๐ Logged changes from settings.jsonc → settings.v1.jsonc
๐ Logged changes from settings.v1.jsonc → settings.v2.jsonc
๐ Logged changes from settings.v2.jsonc → settings.v3.jsonc
๐ Pipeline completed! See 'changelog.txt' for version history.
๐ Example Generated
changelog.txt
๐ 2025-11-04 10:52:31
๐ From: settings.jsonc
๐ To: settings.v1.jsonc
๐ Changes:
➕ "theme": "dark",
๐ 2025-11-04 10:52:33
๐ From: settings.v1.jsonc
๐ To: settings.v2.jsonc
๐ Changes:
➕ "autoSave": 15,
➕ "experimental": true
๐ 2025-11-04 10:52:35
๐ From: settings.v2.jsonc
๐ To: settings.v3.jsonc
๐ Changes:
➕ "compiledBy": "GoLang v1.23"
๐งฐ How to Run the Full Demo# 1️⃣ Ensure dependencies installed
npm install jsonc-parser
pip install commentjson
go get github.com/tidwall/jsonc
# 2️⃣ Run the entire automation
node run_pipeline.js
After running, your directory will contain:
settings.jsonc
settings.v1.jsonc
settings.v2.jsonc
settings.v3.jsonc
changelog.txt
๐ง What You’ve Built
✅ A cross-language JSONC workflow integrating JavaScript, Python, and Go.
✅ A pipeline automation system with versioning and diffs.
✅ A timestamped changelog for auditability and tracking.
This is a real-world blueprint for:
- Configuration version control (for cross-platform systems).
- Automated environment syncing across services.
- Testing JSONC compatibility pipelines.
✨ Optional Next Upgrade Ideas
- Add Git integration – auto-commit each new version.
- Generate HTML changelog reports (pretty formatted diffs).
- Add a
rollbackcommand to restore previous JSONC versions. - Containerize (e.g.,
Dockerfilewith all three environments pre-installed).
Add the Git auto-commit integration next — so each version and changelog entry is automatically committed with a descriptive message like “Update via Python step”?
Perfect ๐ — now we’ll extend the cross-language JSONC automation pipeline to include
๐พ automatic Git commits for every version and changelog entry.
This turns your workflow into a fully version-controlled multi-language config manager — perfect for real projects where each service (Node, Python, Go) updates config files and commits changes automatically.
๐งฉ Updated Folder Overview
jsonc-demo/
├── settings.jsonc
├── edit_node.js
├── edit_python.py
├── edit_go.go
├── run_pipeline.js ← Master automation + changelog + git commit
├── changelog.txt
└── .git/ ← Git repo initialized automatically
⚙️ Step 1: Git Auto-Commit–Enabled Master Script
We’ll enhance the Node.js controller to:
- Run Node, Python, and Go steps sequentially.
- Log differences to
changelog.txt. - Auto-initialize Git (if not already).
- Auto-commit each stage with a clear message.
๐ป run_pipeline.js
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// === Utility: Diff between two files ===
function getDiff(oldFile, newFile) {
const oldText = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, "utf8") : "";
const newText = fs.readFileSync(newFile, "utf8");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const diffLines = [];
newLines.forEach((line, i) => {
if (oldLines[i] !== line) diffLines.push(` ➕ ${line.trim()}`);
});
return diffLines.join("\n");
}
// === Utility: Log changelog entry ===
function logChange(oldFile, newFile) {
const timestamp = new Date().toISOString().replace("T", " ").split(".")[0];
const diffText = getDiff(oldFile, newFile);
๐ ${timestamp}
๐ From: ${path.basename(oldFile)}
๐ To: ${path.basename(newFile)}
๐ Changes:
${diffText || " (No visible changes)"}
;
fs.appendFileSync("changelog.txt", entry, "utf8");
console.log(`๐ Logged ${oldFile} → ${newFile}`);
}
// === Utility: Run shell command ===
function runCommand(cmd) {
execSync(cmd, { stdio: "inherit" });
}
// === Utility: Git commit helper ===
function gitCommit(file, message) {
try {
execSync(`git add ${file} changelog.txt`, { stdio: "ignore" });
execSync(`git commit -m "${message}"`, { stdio: "ignore" });
console.log(`๐พ Git committed: ${message}`);
} catch (err) {
console.error("⚠️ Git commit failed:", err.message);
}
}
// === Master controller ===
try {
console.log("๐ Starting Cross-Language JSONC Pipeline with Git tracking...\n");
// Initialize Git if missing
if (!fs.existsSync(".git")) {
runCommand("git init");
console.log("๐ Initialized new Git repository.\n");
}
// Clean up previous changelog
if (fs.existsSync("changelog.txt")) fs.unlinkSync("changelog.txt");
// Run Node.js step
runCommand("node edit_node.js");
logChange("settings.jsonc", "settings.v1.jsonc");
gitCommit("settings.v1.jsonc", "Node.js step → Updated theme to dark");
// Run Python step
runCommand("python edit_python.py");
logChange("settings.v1.jsonc", "settings.v2.jsonc");
gitCommit("settings.v2.jsonc", "Python step → Updated autoSave & experimental");
// Run Go step
runCommand("go run edit_go.go");
logChange("settings.v2.jsonc", "settings.v3.jsonc");
gitCommit("settings.v3.jsonc", "Go step → Added compiledBy field");
console.log("\n✅ All steps completed and committed!");
console.log("๐ See changelog.txt and git log for version history.\n");
} catch (err) {
console.error("❌ Pipeline failed:", err.message);
}
๐ง What Happens Automatically
When you run:
node run_pipeline.js
The controller will:
- Create/update
.gitrepo if not already initialized. - Run each language script sequentially.
- Log diffs to
changelog.txt. - Stage and commit each updated version and changelog entry with descriptive messages.
๐งพ Example Terminal Output
๐ Starting Cross-Language JSONC Pipeline with Git tracking...
๐ Initialized new Git repository.
✅ Node.js: Created settings.v1.jsonc
๐ Logged settings.jsonc → settings.v1.jsonc
๐พ Git committed: Node.js step → Updated theme to dark
✅ Python: Created settings.v2.jsonc
๐ Logged settings.v1.jsonc → settings.v2.jsonc
๐พ Git committed: Python step → Updated autoSave & experimental
✅ Go: Created settings.v3.jsonc
๐ Logged settings.v2.jsonc → settings.v3.jsonc
๐พ Git committed: Go step → Added compiledBy field
✅ All steps completed and committed!
๐ See changelog.txt and git log for version history.
๐งฐ Check Git History
After running the script, try:
git log --oneline
You’ll see:
d8a7f32 Go step → Added compiledBy field
a24b0c1 Python step → Updated autoSave & experimental
3be1b9a Node.js step → Updated theme to dark
Each commit will include both the settings.vX.jsonc and the updated changelog.txt.
๐ Example Changelog O
๐ 2025-11-04 11:02:15
๐ From: settings.jsonc
๐ To: settings.v1.jsonc
๐ Changes:
➕ "theme": "dark",
..๐ง Why This Is Powerful
✅ Fully automated versioning and commit tracking across multiple languages.
✅ Maintains a human-readable changelog for quick audits.
✅ Builds a Git-backed configuration evolution history — ideal for collaborative projects.
✅ Can plug directly into CI/CD pipelines (e.g., GitHub Actions or Jenkins).
๐ก Optional Next Steps
You can easily extend this system to:
- Push automatically to GitHub after each pipeline run.
git remote add origin <your_repo_url> git push origin main - Tag releases (e.g.,
v1.0,v2.0):execSync('git tag v1.0 && git push --tags');- Add rollback script to restore specific versions:
git checkout HEAD~1 settings.v2.jsonc - Generate HTML or Markdown changelog reports for easy review.
Add that automatic Git push + tag functionality next (so each stage also pushes to GitHub and tags versions like v1, v2, v3)?
๐ฅ Excellent — let’s now extend the automation to full GitOps-level control:
Every stage (Node, Python, Go) will not only commit changes but also automatically push to GitHub and create version tags (v1, v2, v3).
This turns your JSONC pipeline into a continuous integration (CI) mini system — complete with audit logs, changelogs, and remote synchronization.
๐งฉ Folder Layout (Final Version)
jsonc-demo/
├── settings.jsonc
├── edit_node.js
├── edit_python.py
├── edit_go.go
├── run_pipeline.js ← master script (with auto Git push + tags)
├── changelog.txt
└── .git/
⚙️ Step 1: Updated run_pipeline.js
Now includes:
- Automatic Git initialization if missing
- Commits per stage
- Auto-tagging (
v1,v2,v3) - Auto-pushing to GitHub (if remote is set)
๐ป run_pipeline.js
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// === Utility: Run command safely ===
function run(cmd, options = {}) {
try {
execSync(cmd, { stdio: "inherit", ...options });
} catch (err) {
console.error(`⚠️ Command failed: ${cmd}\n${err.message}`);
}
}
// === Utility: Compare two files ===
function getDiff(oldFile, newFile) {
const oldText = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, "utf8") : "";
const newText = fs.readFileSync(newFile, "utf8");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const diffLines = [];
newLines.forEach((line, i) => {
if (oldLines[i] !== line) diffLines.push(` ➕ ${line.trim()}`);
});
return diffLines.join("\n");
}
// === Utility: Log changelog entry ===
function logChange(oldFile, newFile) {
const timestamp = new Date().toISOString().replace("T", " ").split(".")[0];
const diffText = getDiff(oldFile, newFile);
const entry = `
๐ ${timestamp}
๐ From: ${path.basename(oldFile)}
๐ To: ${path.basename(newFile)}
๐ Changes:
${diffText || " (No visible changes)"}
;
fs.appendFileSync("changelog.txt", entry, "utf8");
console.log(`๐ Logged ${oldFile} → ${newFile}`);
}
// === Git commit helper ===
function gitCommitAndTag(file, message, tag) {
try {
execSync(`git add ${file} changelog.txt`, { stdio: "ignore" });
execSync(`git commit -m "${message}"`, { stdio: "ignore" });
console.log(`๐พ Commit created: ${message}`);
// Create and push tag
execSync(`git tag -f ${tag}`, { stdio: "ignore" });
console.log(`๐ท️ Tagged version: ${tag}`);
// Push to remote (if exists)
try {
execSync("git push -u origin main --tags", { stdio: "ignore" });
console.log(`๐ Changes pushed to GitHub with tag ${tag}\n`);
} catch {
console.log("⚠️ Git remote not set — skipping push.\n");
}
} catch (err) {
console.error("⚠️ Git commit/tag failed:", err.message);
}
}
// === MASTER CONTROLLER ===
try {
console.log("๐ Starting JSONC Cross-Language Pipeline (with Git push & tags)...\n");
// Initialize Git if not yet present
if (!fs.existsSync(".git")) {
run("git init");
run('git branch -M main');
console.log("๐ Initialized new Git repository.\n");
console.log("๐ To enable auto-push, run once:");
console.log(' git remote add origin <your_repo_url>\n');
}
// Clean old changelog
if (fs.existsSync("changelog.txt")) fs.unlinkSync("changelog.txt");
// === Step 1: Node.js ===
run("node edit_node.js");
logChange("settings.jsonc", "settings.v1.jsonc");
gitCommitAndTag("settings.v1.jsonc", "Node.js step → dark theme", "v1");
// === Step 2: Python ===
run("python edit_python.py");
logChange("settings.v1.jsonc", "settings.v2.jsonc");
gitCommitAndTag("settings.v2.jsonc", "Python step → autoSave & experimental", "v2");
// === Step 3: Go ===
run("go run edit_go.go");
logChange("settings.v2.jsonc", "settings.v3.jsonc");
gitCommitAndTag("settings.v3.jsonc", "Go step → added compiledBy field", "v3");
console.log("\n✅ All steps completed, tagged, and (optionally) pushed!");
console.log("๐ Run `git log --oneline` or `git tag` to view versions.\n");
} catch (err) {
console.error("❌ Pipeline failed:", err.message);
}
๐ง What Happens
When you run:
node run_pipeline.js
You’ll get this automated process:
✅ Node.js edits → creates
settings.v1.jsonc- Commits to Git (
v1tag) - Pushes (if remote added)
- Commits to Git (
✅ Python edits → creates
settings.v2.jsonc- Commits to Git (
v2tag) - Pushes
- Commits to Git (
✅ Go edits → creates
settings.v3.jsonc- Commits to Git (
v3tag) - Pushes
- Commits to Git (
๐งพ Example Terminal Output
๐ Starting JSONC Cross-Language Pipeline (with Git push & tags)...
๐ Initialized new Git repository.
✅ Node.js: Created settings.v1.jsonc
๐ Logged settings.jsonc → settings.v1.jsonc
๐พ Commit created: Node.js step → dark theme
๐ท️ Tagged version: v1
⚠️ Git remote not set — skipping push.
✅ Python: Created settings.v2.jsonc
๐ Logged settings.v1.jsonc → settings.v2.jsonc
๐พ Commit created: Python step → autoSave & experimental
๐ท️ Tagged version: v2
✅ Go: Created settings.v3.jsonc
๐ Logged settings.v2.jsonc → settings.v3.jsonc
๐พ Commit created: Go step → added compiledBy field
๐ท️ Tagged version: v3
✅ All steps completed, tagged, and (optionally) pushed!
๐ Run `git log --oneline` or `git tag` to view versions.
๐งฐ To Enable Auto Push to GitHub
After the first run, connect
your local folder to a GitHub repo:
git remote add origin https://github.com/<your-username>/<repo-name>.git
git push -u origin main --tags
Next time you run node run_pipeline.js, all commits and tags (v1, v2, v3) will automatically sync to GitHub.
๐ก Optional Enhancements
You can easily add:
- Release Notes Generator (auto-create Markdown summaries per tag)
- GitHub Action that runs the pipeline on every push
- Slack or Discord webhook notifications when a new version is tagged
- HTML changelog report from the text logs
Add a Release Notes generator next — so each new Git tag (v1, v2, v3) automatically creates a formatted Markdown release summary (e.g., RELEASE_NOTES.md)?
Awesome ๐ฅ — let’s now extend your cross-language JSONC GitOps pipeline with a
๐ชถ Release Notes Generator that automatically builds a formatted RELEASE_NOTES.md file for every version tag (v1, v2, v3).
Every new tag will:
- Append a section to
RELEASE_NOTES.md. - Include timestamp, version, summary, and a diff preview from the changelog.
- Be committed and (optionally) pushed to GitHub.
๐งฉ Final Folder Layout
jsonc-demo/
├── settings.jsonc
├── edit_node.js
├── edit_python.py
├── edit_go.go
├── run_pipeline.js ← master controller (now with changelog + git + release notes)
├── changelog.txt
├── RELEASE_NOTES.md ← auto-generated release summaries
└── .git/
⚙️ Step 1: Updated Master Script — run_pipeline.js
Below is the final integrated version, including:
- JSONC automation
- Changelog
- Git commits + tags + pushes
- Markdown release notes generation
๐ป run_pipeline.js
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
// === Helper: Safe command runner ===
function run(cmd) {
try {
execSync(cmd, { stdio: "inherit" });
} catch (err) {
console.error(`⚠️ Command failed: ${cmd}\n${err.message}`);
}
}
// === Helper: Diff two files ===
function getDiff(oldFile, newFile) {
const oldText = fs.existsSync(oldFile) ? fs.readFileSync(oldFile, "utf8") : "";
const newText = fs.readFileSync(newFile, "utf8");
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const diffLines = [];
newLines.forEach((line, i) => {
if (oldLines[i] !== line) diffLines.push(`+ ${line.trim()}`);
});
return diffLines.join("\n");
}
// === Helper: Append changelog entry ===
function logChange(oldFile, newFile) {
const timestamp = new Date().toISOString().replace("T", " ").split(".")[0];
const diffText = getDiff(oldFile, newFile);
const entry = `
๐ ${timestamp}
๐ From: ${path.basename(oldFile)}
๐ To: ${path.basename(newFile)}
๐ Changes:
${diffText || " (No visible changes)"}
; fs.appendFileSync("changelog.txt", entry, "utf8");
console.log(`๐ Logged ${oldFile} → ${newFile}`);
return diffText;
}
// === Helper: Generate release note section ===
function generateReleaseNotes(version, message, diffText) {
const timestamp = new Date().toISOString().replace("T", " ").split(".")[0];
const section = `
---
## ๐ท️ ${version} — ${timestamp}
**Change Summary:** ${message}
\`\`\`diff
${diffText || "(No diff found)"}
\`\`\`
`;
fs.appendFileSync("RELEASE_NOTES.md", section, "utf8");
console.log(`๐ชถ Added release notes for ${version}`);
}
// === Helper: Commit, tag, and push ===
function gitCommitAndTag(file, message, tag, diffText) {
try {
execSync(`git add ${file} changelog.txt RELEASE_NOTES.md`, { stdio: "ignore" });
execSync(`git commit -m "${message}"`, { stdio: "ignore" });
console.log(`๐พ Commit created: ${message}`);
execSync(`git tag -f ${tag}`, { stdio: "ignore" });
console.log(`๐ท️ Tagged version: ${tag}`);
// Push if remote exists
try {
execSync("git push -u origin main --tags", { stdio: "ignore" });
console.log(`๐ Pushed commit + tag ${tag}\n`);
} catch {
console.log("⚠️ Git remote not set — skipping push.\n");
}
// Write release notes
generateReleaseNotes(tag, message, diffText);
} catch (err) {
console.error("⚠️ Git commit/tag failed:", err.message);
}
}
// === MAIN PIPELINE ===
try {
console.log("๐ Starting JSONC Cross-Language Pipeline (Git + Release Notes)...\n");
// Initialize Git if needed
if (!fs.existsSync(".git")) {
run("git init");
run("git branch -M main");
console.log("๐ Initialized new Git repository.");
console.log("๐ Run `git remote add origin <your_repo_url>` to enable pushing.\n");
}
// Clean old logs
if (fs.existsSync("changelog.txt")) fs.unlinkSync("changelog.txt");
if (fs.existsSync("RELEASE_NOTES.md")) fs.unlinkSync("RELEASE_NOTES.md");
// === Node.js Step ===
run("node edit_node.js");
const diff1 = logChange("settings.jsonc", "settings.v1.jsonc");
gitCommitAndTag("settings.v1.jsonc", "Node.js step → dark theme", "v1", diff1);
// === Python Step ===
run("python edit_python.py");
const diff2 = logChange("settings.v1.jsonc", "settings.v2.jsonc");
gitCommitAndTag("settings.v2.jsonc", "Python step → autoSave & experimental", "v2", diff2);
// === Go Step ===
run("go run edit_go.go");
const diff3 = logChange("settings.v2.jsonc", "settings.v3.jsonc");
gitCommitAndTag("settings.v3.jsonc", "Go step → added compiledBy field", "v3", diff3);
console.log("\n✅ All steps completed, tagged, and release notes updated!");
console.log("๐ See RELEASE_NOTES.md for summaries.\n");
} catch (err) {
console.error("❌ Pipeline failed:", err.message);
}
๐งพ Example Output (Terminal)
๐ Starting JSONC Cross-Language Pipeline (Git + Release Notes)...
๐ Initialized new Git repository.
✅ Node.js: Created settings.v1.jsonc
๐ Logged settings.jsonc → settings.v1.jsonc
๐พ Commit created: Node.js step → dark theme
๐ท️ Tagged version: v1
๐ชถ Added release notes for v1
✅ Python: Created settings.v2.jsonc
๐ Logged settings.v1.jsonc → settings.v2.jsonc
๐พ Commit created: Python step → autoSave & experimental
๐ท️ Tagged version: v2
๐ชถ Added release notes for v2
✅ Go: Created settings.v3.jsonc
๐ Logged settings.v2.jsonc → settings.v3.jsonc
๐พ Commit created: Go step → added compiledBy field
๐ท️ Tagged version: v3
๐ชถ Added release notes for v3
✅ All steps completed, tagged, and release notes updated!
๐ See RELEASE_NOTES.md for summaries.
๐ Example Generated RELEASE_NOTES.md
---
## ๐ท️ v1 — 2025-11-04 12:15:05
**Change Summary:** Node.js step → dark theme
```diff
+ "theme": "dark",
๐ท️ v2 — 2025-11-04 12:15:09
Change Summary: Python step → autoSave & experimental
+ "autoSave": 15,
+ "experimental": true
๐ท️ v3 — 2025-11-04 12:15:13
Change Summary: Go step → added compiledBy field
+ "compiledBy": "GoLang v1.23"
Each version automatically includes:
- Timestamp
- Tag (`v1`, `v2`, `v3`)
- Change summary
- Diff preview
---
## ✅ How to Run It
```bash
# Install dependencies
npm install jsonc-parser
pip install commentjson
go get github.com/tidwall/jsonc
# Initialize once and add your repo
git init
git branch -M main
git remote add origin https://github.com/<username>/<repo-name>.git
# Run the automation
node run_pipeline.js
๐ง What You Now Have
✅ Multi-language JSONC editing (Node, Python, Go)
✅ Automated changelog tracking
✅ Git commits, tagging, and optional pushes
✅ Markdown RELEASE_NOTES.md with diff previews
✅ A portable GitOps-ready configuration management pipeline
⚙️ Optional Next Steps
Would you like me to add:
- ๐ค Automatic GitHub Release publishing (creates GitHub releases from tags and uploads the
RELEASE_NOTES.mdcontent automatically)?
This would make your pipeline publish formal releases to GitHub each timev1,v2,v3are created.
Perfect ๐
So here’s what your setup can now do, all automated in one command (node run_pipeline.js):
✅ Parse and update JSONC files using Node.js, Python, and Go
✅ Automatically generate:
changelog.txt(human-readable diff log)RELEASE_NOTES.md(Markdown summaries per version)
✅ Create Git commits + version tags (v1,v2,v3)
✅ Optionally push to GitHub if you’ve set your remote
✅ Quick Recap – How to Use
Initialize & Connect Repo
git init git branch -M main git remote add originhttps://github.com/<your-username>/<repo-name>.gitInstall Dependencies
npm install jsonc-parser pip install commentjson go get github.com/tidwall/jsoncRun the Complete Pipeline
node run_pipeline.jsCheck Outputs
cat changelog.txt cat RELEASE_NOTES.md git log --oneline git tag
Add automatic GitHub Release publishing
(using your repo’s API token — so each tag (v1, v2, v3) is turned into a GitHub Release
with RELEASE_NOTES.md as the description)?
Good ๐ before we proceed — to clarify:
automatically creating GitHub Releases from tags means your pipeline would talk to GitHub’s API using a personal access token (PAT).
That token gives permission for your local script to create releases on your GitHub repository (e.g., when you tag v1, it would make a new release page on GitHub with the release notes).
For security reasons, I can’t handle or store your token myself — but I can show you exactly how to set it up safely on your computer.
Here’s what you’ll need to do next ๐
๐ชถ Step 1: Create a GitHub Personal Access Token (PAT)
- Go to ๐ https://github.com/settings/tokens
- Click “Generate new token (classic)”
- Give it a name like
jsonc-pipeline - Check repo scope (for creating releases)
- Copy the generated token (keep it private)
⚙️ Step 2: Set Token as Environment Variable
On your terminal (Linux/macOS):
export GITHUB_TOKEN=ghp_yourgeneratedtoken
On Windows PowerShell:
setx GITHUB_TOKEN "ghp_yourgeneratedtoken"
๐งฉ Step 3: Add Auto-Release Feature to Your Script
Here’s a small Node.js helper you can drop into your run_pipeline.js (at the bottom, after tagging):
import https from "https";
function createGitHubRelease(tag, message, notes) {
const token = process.env.GITHUB_TOKEN;
const repoUrl = execSync("git remote get-url origin").toString().trim();
// Extract owner/repo from the Git remote
const match = repoUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
if (!match) {
console.log("⚠️ Could not parse GitHub repo URL. Skipping release.");
return;
}
const [_, owner, repo] = match;
const data = JSON.stringify({
tag_name: tag,
name: `${tag} — ${message}`,
body: notes,
draft: false,
prerelease: false,
});
const options = {
hostname: "api.github.com",
path: `/repos/${owner}/${repo}/releases`,
method: "POST",
headers: {
"User-Agent": "jsonc-pipeline",
"Authorization": `token ${token}`,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
};
const req = https.request(options, (res) => {
console.log(`๐ค GitHub release created for ${tag} (status: ${res.statusCode})`);
});
req.on("error", (err) => console.error("❌ Release failed:", err.message));
req.write(data);
req.end();
}
Then, right after each gitCommitAndTag(...) call, add:
createGitHubRelease(tag, message, diffText);
This will automatically publish a
release to your GitHub repository with:
- The tag (e.g.,
v3) - The release title (from your commit message)
- The content (diff summary from the changelog)
✅ Result
When you run:
node run_pipeline.js
You’ll get:
- Local commits + tags
RELEASE_NOTES.mdupdated- GitHub Releases automatically published ๐
- final full integrated script (with changelog, tagging, release notes, and GitHub Release publishing all in one file)?
Perfect ๐ — here’s your complete, production-ready Node.js pipeline script:
It handles JSONC parsing, changelog creation, semantic version tagging, release note generation, and automatic GitHub Releases — all in one place.
๐ run_pipeline.js
/**
* JSONC Release Pipeline Script
* ---------------------------------------------
* ✅ Parses and updates JSONC files
* ✅ Generates changelog.txt and RELEASE_NOTES.md
* ✅ Creates git commits + tags (v1, v2, v3, ...)
* ✅ Publishes GitHub Releases automatically
*
* Requirements:
* npm install jsonc-parser
* export GITHUB_TOKEN=ghp_yourtoken
*/
import fs from "fs";
import { execSync } from "child_process";
import { parse, modify, applyEdits, printParseErrorCode } from "jsonc-parser";
import https from "https";
// -------------------- Configuration --------------------
const jsoncFile = "config.jsonc"; // your JSONC file
const changelogFile = "changelog.txt";
const releaseNotesFile = "RELEASE_NOTES.md";
let versionCounter = 1;
// -------------------- Helpers --------------------
function log(msg) {
console.log(`\x1b[36m[INFO]\x1b[0m ${msg}`);
}
function run(cmd) {
return execSync(cmd, { stdio: "pipe" }).toString().trim();
}
function readJSONC(file) {
const content = fs.readFileSync(file, "utf-8");
const errors = [];
const data = parse(content, errors, { allowTrailingComma: true });
if (errors.length) {
errors.forEach((e) =>
console.error(`❌ JSONC Error: ${printParseErrorCode(e.error)} at ${e.offset}`)
);
throw new Error("Invalid JSONC");
}
return data;
}
function writeJSONC(file, json) {
const content = JSON.stringify(json, null, 2);
fs.writeFileSync(file, content);
}
// -------------------- Core Logic --------------------
function updateJSONCFile() {
log("Parsing JSONC file...");
const data = readJSONC(jsoncFile);
// Example modification: add or update version
versionCounter++;
data.version = `v${versionCounter}`;
log("Updating JSONC file...");
writeJSONC(jsoncFile, data);
return `Updated version to v${versionCounter}`;
}
function appendChangelog(tag, message) {
const entry = `[${new Date().toISOString()}] ${tag} — ${message}\n`;
fs.appendFileSync(changelogFile, entry);
}
function appendReleaseNotes(tag, message) {
const entry = `\n## ${tag}\n${message}\n`;
fs.appendFileSync(releaseNotesFile, entry);
}
// -------------------- Git Automation --------------------
function gitCommitAndTag(message) {
log("Committing changes...");
run("git add .");
run(`git commit -m "${message}"`);
const tag = `v${versionCounter}`;
run(`git tag ${tag}`);
log(`✅ Created git tag ${tag}`);
return tag;
}
// -------------------- GitHub Release Publisher --------------------
function createGitHubRelease(tag, message, notes) {
const token = process.env.GITHUB_TOKEN;
if (!token) {
console.log("⚠️ No GITHUB_TOKEN found. Skipping GitHub release.");
return;
}
const repoUrl = run("git remote get-url origin");
const match = repoUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
if (!match) {
console.log("⚠️ Could not parse GitHub repo URL. Skipping release.");
return;
}
const [_, owner, repo] = match;
const data = JSON.stringify({
tag_name: tag,
name: `${tag} — ${message}`,
body: notes,
draft: false,
prerelease: false,
});
const options = {
hostname: "api.github.com",
path: `/repos/${owner}/${repo}/releases`,
method: "POST",
headers: {
"User-Agent": "jsonc-pipeline",
"Authorization": `token ${token}`,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
};
const req = https.request(options, (res) => {
console.log(`๐ค GitHub release created for ${tag} (status: ${res.statusCode})`);
});
req.on("error", (err) => console.error("❌ Release failed:", err.message));
req.write(data);
req.end();
}
// -------------------- Pipeline Execution --------------------
log("๐ Starting JSONC release pipeline...");
const message = updateJSONCFile();
const tag = gitCommitAndTag(message);
appendChangelog(tag, message);
appendReleaseNotes(tag, message);
createGitHubRelease(tag, message, message);
log("๐ Pipeline complete!");
⚙️ How to Use
Create a JSONC file (e.g.,
config.jsonc):{ // App configuration "name": "MyApp", "version": "v1" }Initialize Git & Connect Repo
git init git add . git commit -m "initial" git branch -M main git remote add originhttps://github.com/<your-username>/<repo>.gitInstall Dependencies
npm install jsonc-parserSet Your GitHub Token
export GITHUB_TOKEN=ghp_yourtokenRun the Pipeline
node run_pipeline.js
๐งพ What You Get Automatically
| Output File | Description |
|---|---|
config.jsonc | Updated with incremented version |
changelog.txt | Append-only human-readable log |
RELEASE_NOTES.md | Markdown release notes |
Git tags (v1, v2, …) | Created for each release |
| GitHub Release | Published automatically via API |
Here include Python and Go equivalents of this GitHub-release pipeline (so you can trigger it in any language)?
Excellent ๐ — below are Python ๐ and Go ๐น equivalents of your JSONC release pipeline.
Both versions replicate the Node.js pipeline logic:
- Parse and update a
.jsoncfile - Commit and tag in Git
- Generate changelog + release notes
- Publish a GitHub Release automatically
๐ Python Version — run_pipeline.py
"""
JSONC Release Pipeline (Python Version)
------------------------------------------
✅ Parses JSONC using commentjson
✅ Updates version number
✅ Commits + tags in Git
✅ Generates changelog.txt & RELEASE_NOTES.md
✅ Publishes GitHub Release (requires GITHUB_TOKEN)
"""
import os
import json
import subprocess
import requests
import commentjson
from datetime import datetime
JSONC_FILE = "config.jsonc"
CHANGELOG = "changelog.txt"
RELEASE_NOTES = "RELEASE_NOTES.md"
version_counter = 1
def log(msg):
print(f"\033[96m[INFO]\033[0m {msg}")
def run(cmd):
return subprocess.check_output(cmd, shell=True).decode().strip()
def read_jsonc(file):
with open(file, "r") as f:
return commentjson.load(f)
def write_jsonc(file, data):
with open(file, "w") as f:
json.dump(data, f, indent=2)
def update_jsonc():
global version_counter
data = read_jsonc(JSONC_FILE)
version_counter += 1
data["version"] = f"v{version_counter}"
write_jsonc(JSONC_FILE, data)
return f"Updated version to v{version_counter}"
def append_changelog(tag, message):
with open(CHANGELOG, "a") as f:
f.write(f"[{datetime.utcnow().isoformat()}] {tag} — {message}\n")
def append_release_notes(tag, message):
with open(RELEASE_NOTES, "a") as f:
f.write(f"\n## {tag}\n{message}\n")
def git_commit_and_tag(message):
log("Committing changes...")
run("git add .")
run(f'git commit -m "{message}"')
tag = f"v{version_counter}"
run(f"git tag {tag}")
log(f"✅ Created git tag {tag}")
return tag
def create_github_release(tag, message, notes):
token = os.getenv("GITHUB_TOKEN")
if not token:
log("⚠️ No GITHUB_TOKEN found. Skipping GitHub release.")
return
repo_url = run("git remote get-url origin")
import re
m = re.search(r"github\.com[:/](.+?)/(.+?)(\.git)?$", repo_url)
if not m:
log("⚠️ Could not parse GitHub repo URL.")
return
owner, repo = m.group(1), m.group(2)
url = f"https://api.github.com/repos/{owner}/{repo}/releases"
payload = {
"tag_name": tag,
"name": f"{tag} — {message}",
"body": notes,
"draft": False,
"prerelease": False,
}
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github+json",
"User-Agent": "jsonc-pipeline",
}
res = requests.post(url, json=payload, headers=headers)
log(f"๐ค GitHub release created for {tag} (status: {res.status_code})")
if __name__ == "__main__":
log("๐ Starting Python JSONC pipeline...")
message = update_jsonc()
tag = git_commit_and_tag(message)
append_changelog(tag, message)
append_release_notes(tag, message)
create_github_release(tag, message, message)
log("๐ Pipeline complete!")
๐น Go Version — run_pipeline.go
// JSONC Release Pipeline (Go Version)
// ------------------------------------
// ✅ Parses JSONC using tidwall/jsonc
// ✅ Updates version
// ✅ Git commit + tag
// ✅ Changelog + release notes
// ✅ GitHub Release publishing
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"regexp"
"time"
"github.com/tidwall/jsonc"
)
var (
jsoncFile = "config.jsonc"
changelogFile = "changelog.txt"
releaseNotes = "RELEASE_NOTES.md"
versionCounter = 1
)
func run(cmd string) string {
out, _ := exec.Command("bash", "-c", cmd).Output()
return string(bytes.TrimSpace(out))
}
func updateJSONC() string {
data, _ := ioutil.ReadFile(jsoncFile)
clean := jsonc.ToJSON(string(data))
var obj map[string]interface{}
json.Unmarshal([]byte(clean), &obj)
versionCounter++
obj["version"] = fmt.Sprintf("v%d", versionCounter)
out, _ := json.MarshalIndent(obj, "", " ")
ioutil.WriteFile(jsoncFile, out, 0644)
return fmt.Sprintf("Updated version to v%d", versionCounter)
}
func appendFile(file, content string) {
f, _ := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
f.WriteString(content)
}
func gitCommitAndTag(message string) string {
run("git add .")
run(fmt.Sprintf("git commit -m \"%s\"", message))
tag := fmt.Sprintf("v%d", versionCounter)
run(fmt.Sprintf("git tag %s", tag))
fmt.Println("✅ Created git tag", tag)
return tag
}
func createGitHubRelease(tag, message, notes string) {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
fmt.Println("⚠️ No GITHUB_TOKEN found, skipping GitHub release.")
return
}
repoURL := run("git remote get-url origin")
re := regexp.MustCompile(`github\.com[:/](.+?)/(.+?)(\.git)?$`)
match := re.FindStringSubmatch(repoURL)
if match == nil {
fmt.Println("⚠️ Could not parse GitHub repo URL.")
return
}
owner, repo := match[1], match[2]
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
payload := map[string]interface{}{
"tag_name": tag,
"name": fmt.Sprintf("%s — %s", tag, message),
"body": notes,
"draft": false,
"prerelease": false,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body))
req.Header.Add("Authorization", "token "+token)
req.Header.Add("User-Agent", "jsonc-pipeline")
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(req)
if err == nil {
fmt.Printf("๐ค GitHub release created for %s (status: %s)\n", tag, res.Status)
}
}
func main() {
fmt.Println("๐ Starting Go JSONC pipeline...")
message := updateJSONC()
tag := gitCommitAndTag(message)
appendFile(changelogFile, fmt.Sprintf("[%s] %s — %s\n", time.Now().UTC().Format(time.RFC3339), tag, message))
appendFile(releaseNotes, fmt.Sprintf("\n## %s\n%s\n", tag, message))
createGitHubRelease(tag, message, message)
fmt.Println("๐ Pipeline complete!")
}
⚙️ Setup (Same for All Three)
Initialize Git Repo & JSONC File
git init echo '{ "name": "MyApp", "version":"v1" }' > config.jsonc git add . git commit -m "initial" git branch -M main git remote add origin https://github.com/<user>/<repo>.gitSet GitHub Token
export GITHUB_TOKEN=ghp_yourtokenRun
- Node.js →
node run_pipeline.js - Python →
python run_pipeline.py - Go →
go run run_pipeline.go
- Node.js →
Here include a CI/CD workflow (GitHub Actions) so this runs automatically whenever you push to main or merge a PR?
Perfect ๐ — let’s make your pipeline fully automated in CI/CD using GitHub Actions.
This workflow will:
✅ Run your Node.js / Python / Go JSONC release pipeline
✅ Auto-commit, tag, and push updates
✅ Automatically publish GitHub Releases when a new tag is created
✅ Work securely with your GITHUB_TOKEN (no need for extra setup)
๐ .github/workflows/jsonc-release.yml
name: JSONC Auto Release Pipeline
on:
push:
branches:
- main
workflow_dispatch: # allows manual run from GitHub UI
permissions:
contents: write # required for creating commits/tags/releases
jobs:
release:
runs-on: ubuntu-latest
steps:
# ------------------- 1. Checkout Repository -------------------
- name: Checkout code
uses: actions/checkout@v4
# ------------------- 2. Setup Node (for JSONC + Git release) -------------------
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# ------------------- 3. Setup Python -------------------
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
# ------------------- 4. Setup Go -------------------
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
# ------------------- 5. Install Dependencies -------------------
- name: Install dependencies
run: |
npm install jsonc-parser
pip install commentjson requests
go get github.com/tidwall/jsonc
# ------------------- 6. Run JSONC Pipeline -------------------
# Pick ONE script depending on your preferred language
# (Uncomment the one you use)
- name: Run Node.js pipeline
run: node run_pipeline.js
# - name: Run Python pipeline
# run: python run_pipeline.py
# - name: Run Go pipeline
# run: go run run_pipeline.go
# ------------------- 7. Push new tags and commits -------------------
- name: Push changes back to repo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git push origin main --tags
๐งฐ How It Works
๐ Trigger
- Runs automatically when you push or merge to
main - You can also trigger it manually from the GitHub Actions tab (
Run workflow)
⚙️ Actions
- Checks out your repository
- Sets up Node.js, Python, and Go (so you can run any pipeline)
- Installs dependencies
- Runs your JSONC pipeline (
run_pipeline.js/.py/.go) - Pushes back changes, commits, and tags
- Automatically publishes a GitHub Release (since your script uses
GITHUB_TOKEN)
✅ Security Notes
- You don’t need to manually add a personal token — GitHub Actions provides a secure
GITHUB_TOKENautomatically. - It has permission to create commits, tags, and releases.
๐งพ Example Workflow Output
After merging or pushing to main:
config.jsoncgets updated (version→ next value)changelog.txtandRELEASE_NOTES.mdupdated- Git tag created (e.g.,
v3) - GitHub Release appears under your repo’s Releases tab ๐
builds and attaches binaries or ZIPs (e.g., compiled .exe, .tar.gz, or .zip files) to each GitHub Release automatically?
Excellent ๐ — now let’s extend the workflow to not only tag and publish releases automatically,
but also build and attach binaries or ZIP archives (for Node.js, Python, or Go projects).
This is the kind of setup you’d see in a production CI/CD pipeline used for open-source releases ๐
⚙️ Full Workflow — .github/workflows/jsonc-release.yml
name: JSONC Auto Release with Artifacts
on:
push:
branches: [main]
workflow_dispatch: # allows manual run
permissions:
contents: write # needed to create commits, tags, and releases
jobs:
release:
runs-on: ubuntu-latest
steps:
# ------------------- 1. Checkout -------------------
- name: Checkout repository
uses: actions/checkout@v4
# ------------------- 2. Setup Languages -------------------
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
# ------------------- 3. Install Dependencies -------------------
- name: Install dependencies
run: |
npm install jsonc-parser
pip install commentjson requests
go get github.com/tidwall/jsonc
# ------------------- 4. Run Pipeline Script -------------------
# Choose the one that matches your project:
- name: Run JSONC pipeline (Node)
run: node run_pipeline.js
# - name: Run JSONC pipeline (Python)
# run: python run_pipeline.py
# - name: Run JSONC pipeline (Go)
# run: go run run_pipeline.go
# ------------------- 5. Build Release Artifacts -------------------
# Examples — adjust for your language/project type
- name: Build artifacts
run: |
mkdir dist
# Node.js build (if applicable)
if [ -f package.json ]; then
npm pack
mv *.tgz dist/
fi
# Python build (wheel + source)
if [ -f setup.py ]; then
python setup.py sdist bdist_wheel
mv dist/* dist/ 2>/dev/null || true
fi
# Go binary build
if [ -f go.mod ]; then
go build -o dist/jsonc-pipeline .
fi
# ZIP all outputs for convenience
cd dist
zip -r release_files.zip .
cd ..
# ------------------- 6. Create Release and Upload Artifacts -------------------
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@v2
with:
files: dist/release_files.zip
name: Auto Release ${{ github.run_number }}
tag_name: v${{ github.run_number }}
body_path: RELEASE_NOTES.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ------------------- 7. Push Updated Changes -------------------
- name: Push commits and tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git push origin main --tags
๐งพ What This Workflow Does
| Step | Description |
|---|---|
| 1–2 | Checks out code and sets up Node, Python, and Go |
| 3 | Installs dependencies (for JSONC parsing) |
| 4 | Runs your JSONC version-bump pipeline |
| 5 | Builds project artifacts (Node .tgz, Python .whl, Go binary, ZIP) |
| 6 | Creates a GitHub Release with attached build ZIP |
| 7 | Pushes updated changelog + tags to main |
๐ฆ Example Result on GitHub
Each release (e.g., v23) will show:
- ✅ Updated version in
config.jsonc - ๐️ New entry in
changelog.txt - ๐
RELEASE_NOTES.mdas the release description - ๐ฆ
release_files.zipcontaining:- Node.js
.tgz - Python
.whl/.tar.gz - Go binary
- Node.js
๐งฐ Tips
- If your project is only one language, you can delete the other build sections.
- For multi-platform Go builds, you can use:
GOOS=windows GOARCH=amd64 go build -o dist/jsonc-pipeline.exe . GOOS=linux GOARCH=amd64 gobuild -o dist/jsonc-pipeline-linux . GOOS=darwin GOARCH=arm64 gobuild -o dist/jsonc-pipeline-mac .- For Node.js, you can replace the
npm packstep with a real build command (likenpm run build).
this CI setup to also auto-increment the semantic version (v1.0.0 → v1.1.0) based on commit type (feat, fix, etc.) — similar to how semantic-release works?
Perfect ๐ — this next upgrade turns your GitHub Actions pipeline into a semantic versioning system
(similar to professional CI setups like semantic-release).
It will automatically:
✅ Detect commit types (feat, fix, perf, docs, etc.)
✅ Increment version numbers (v1.0.0 → v1.1.0 → v2.0.0)
✅ Tag + publish GitHub Releases
✅ Attach built artifacts
✅ Update your JSONC version and changelogs automatically
๐ .github/workflows/jsonc-semantic-release.yml
name: JSONC Semantic Release Pipeline
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
issues: read
pull-requests: read
jobs:
semantic-release:
runs-on: ubuntu-latest
steps:
# ------------------- 1. Checkout -------------------
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # required for semantic versioning
# ------------------- 2. Setup Node.js -------------------
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
# ------------------- 3. Install Dependencies -------------------
- name: Install release tools
run: |
npm install -g semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github
npm install jsonc-parser
# ------------------- 4. Run Semantic Release -------------------
- name: Run Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx semantic-release
# ------------------- 5. Build and Attach Artifacts -------------------
- name: Build release artifacts
run: |
mkdir -p dist
# Optional build logic — adjust to your tech stack
if [ -f package.json ]; then
npm pack
mv *.tgz dist/
fi
if [ -f setup.py ]; then
pip install wheel
python setup.py sdist bdist_wheel
mv dist/* dist/ 2>/dev/null || true
fi
if [ -f go.mod ]; then
go build -o dist/jsonc-pipeline
fi
zip -r dist/release_files.zip dist/*
# ------------------- 6. Upload Artifacts to Release -------------------
- name: Upload build artifacts
uses: softprops/action-gh-release@v2
with:
files: dist/release_files.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
๐งฉ Add Semantic Release Configuration
Create a file named .releaserc.json in the root of your repo:
{
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/changelog",
{
"changelogFile": "changelog.txt"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "dist/release_files.zip",
"label": "Release Build"
}
]
}
],
[
"@semantic-release/git",
{
"assets": ["config.jsonc", "changelog.txt", "RELEASE_NOTES.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
๐งพ How It Works
Semantic Commit Detection
When you push commits like:
feat: add new login feature fix: correct user API bug docs: update README→ Semantic Release detects the highest impact and bumps:
fix:→ patch (v1.0.1)feat:→ minor (v1.1.0)BREAKING CHANGE:→ major (v2.0.0)
Automatic Version + Release Notes
- Updates
config.jsoncversion (if included in your script) - Generates changelog entries in
changelog.txt - Publishes a GitHub Release with attached ZIP artifacts
- Updates
Artifacts Attached
- Node
.tgz, Python.whl/.tar.gz, or Go binaries - All compressed in
release_files.zip
- Node
✅ Example Output
After merging a PR:
config.jsonc→"version": "v1.2.0"changelog.txtupdated- Git tag
v1.2.0created - GitHub Release with notes
- + build artifacts attached
- No manual tagging or pushing needed ๐
An auto-update step for config.jsonc’s version field
so Semantic Release writes the new version back into that file automatically (and commits it)?
Awesome ๐ — let’s finish the setup so your config.jsonc version also updates automatically each time a new semantic release is created.
This makes your JSONC version file the single source of truth, just like package.json or pyproject.toml.
๐งฉ Step 1 — Add a Custom Version Updater Script
Create a file named update-jsonc-version.js in your repository root:
// update-jsonc-version.js
import fs from "fs";
import { parse, printParseErrorCode, modify } from "jsonc-parser";
const filePath = "./config.jsonc";
const version = process.env.RELEASE_VERSION;
if (!version) {
console.error("❌ RELEASE_VERSION environment variable not set");
process.exit(1);
}
const text = fs.readFileSync(filePath, "utf8");
let errors = [];
let json = parse(text, errors);
if (errors.length > 0) {
console.error("❌ Failed to parse JSONC:");
errors.forEach((e) => console.error(printParseErrorCode(e.error)));
process.exit(1);
}
// Apply modification to version field
const edits = modify(text, ["version"], version, { formattingOptions: { insertSpaces: true, tabSize: 2 } });
const updated = applyEdits(text, edits);
fs.writeFileSync(filePath, updated, "utf8");
console.log(`✅ Updated config.jsonc version to ${version}`);
function applyEdits(text, edits) {
let result = text;
for (let i = edits.length - 1; i >= 0; i--) {
const edit = edits[i];
result = result.substring(0, edit.offset) + edit.content + result.substring(edit.offset + edit.length);
}
return result;
}
This uses the jsonc-parser library to safely update the version field without breaking comments or formatting.
⚙️ Step 2 — Update .releaserc.json Configuration
Add a custom step after Semantic Release runs to invoke this updater:
{
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{ "preset": "conventionalcommits" }
],
[
"@semantic-release/release-notes-generator",
{ "preset": "conventionalcommits" }
],
[
"@semantic-release/changelog",
{ "changelogFile": "changelog.txt" }
],
[
"@semantic-release/github",
{
"assets": [
{ "path": "dist/release_files.zip", "label": "Release Build" }
]
}
],
[
"@semantic-release/git",
{
"assets": ["config.jsonc", "changelog.txt", "RELEASE_NOTES.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
],
"prepare": [
{
"path": "@semantic-release/exec",
"cmd": "RELEASE_VERSION=${nextRelease.version} node update-jsonc-version.js"
}
]
}
This tells Semantic Release to run your script and update config.jsonc just before committing.
๐งฑ Step 3 — Add Dependency to package.json
If your repo has a Node.js environment, add this dependency:
npm install jsonc-parser
๐ Step 4 — Run the Workflow
Each time you push commits to main that follow Conventional Commit rules:
feat: add support for JSONC comments
fix: correct missing comma issue
The pipeline will:
- Determine new version (e.g.
1.1.0) - Update:
config.jsonc→"version": "1.1.0"changelog.txtwith new section
- Commit those updates back to the repo
- Create Git tag + GitHub Release
- Attach your build ZIP
๐งพ Example config.jsonc Before and After
Before:
{
// Application configuration
"name": "jsonc-pipeline",
"version": "1.0.0"
}
After:
{
// Application configuration
"name": "jsonc-pipeline",
"version": "1.1.0"
}
An example of Conventional Commit messages and how they map to automatic version bumps (major/minor/patch)?
Perfect ๐ — here’s the Conventional Commit guide for your JSONC Semantic Release setup.
These are the commit message formats that trigger automatic version bumps in your GitHub Actions pipeline.
๐งญ What Are Conventional Commits?
Conventional Commits follow this pattern:
<type>(optional scope): <description>
[optional body]
[optional footer]
Example:
feat(parser): add JSONC comment stripping support
๐ Version Bump Rules (How Semantic Release Decides the Next Version)
| Commit Type | Example | Version Bump | Description |
|---|---|---|---|
fix: | fix: handle trailing commas in JSONC | Patch (x.y.z → x.y.z+1) | For bug fixes or small corrections |
feat: | feat(parser): add comment parsing feature | Minor (x.y.z → x.y+1.0) | For new features that don’t break existing code |
perf: | perf: improve JSONC parse speed by 20% | Minor | Performance improvements |
docs: | docs: update README with JSONC examples | No bump | Documentation-only changes |
style: | style: format code with Prettier | No bump | Code style, linting, etc. |
refactor: | refactor: simplify parse logic | No bump | Internal code changes, no user impact |
test: | test: add new parser unit tests | No bump | Testing-only changes |
chore: | chore: update dependencies | No bump | Maintenance or tooling changes |
BREAKING CHANGE: | Footer: BREAKING CHANGE: parser API updated | Major (x.y.z → x+1.0.0) | When backward compatibility is broken |
๐ก Example Scenarios
๐งฑ 1. Small Fix
Commit:
fix(parser): handle null values correctly
→ Version bumps from 1.0.0 → 1.0.1
๐ 2. New Feature
Commit:
feat(cli): add --strip-comments option
→ Version bumps from 1.0.1 → 1.1.0
๐ฅ 3. Breaking Change
Commit:
feat(parser): switch to async API
BREAKING CHANGE: old parse() method removed
→ Version bumps from 1.1.0 → 2.0.0
๐ 4. Non-Impactful Change
Commit:
docs: update usage examples in README
→ No version bump, no release.
๐งฉ Combining Multiple Commits
If your merge includes several commits:
- One
feat:and onefix:→ bumps minor - One
BREAKING CHANGE:→ bumps major - Only
fix:commits → bumps patch
Semantic Release always chooses the highest-impact bump in a batch.
✅ Recommended Workflow for Your Project
- Use clear Conventional Commit messages.
- Push or merge into
main. - GitHub Actions runs the semantic release pipeline:
- Detects commit types
- Updates
config.jsoncversion - Updates
changelog.txt - Publishes GitHub Release + assets
A ready-made Git commit template (so developers automatically follow this format when committing)?
Perfect ๐ — let’s add a Git commit message template so that everyone on your project automatically follows the correct Conventional Commit format (making Semantic Release work smoothly every time).
๐งฉ Step 1 — Create a Commit Template File
Create a file at the root of your repo called:.gitmessage.txt
Paste this inside:
# ๐งฑ Conventional Commit Template
#
# Format: <type>(optional scope): <short summary>
# Example: feat(parser): add JSONC comment support
#
# Types: feat | fix | perf | docs | style | refactor | test | chore
#
# BREAKING CHANGES:
# Add a footer starting with BREAKING CHANGE: to indicate breaking updates
# Commit title (keep under 100 characters)
feat(parser):
# Commit body (optional) – explain why the change was made
# Use full sentences and mention issue numbers if any.
# Footer (optional)
# BREAKING CHANGE: description of what changed
# Closes: #123
⚙️ Step 2 — Configure Git to Use This Template
Run this command in your repository:
git config commit.template .gitmessage.txt
Now, every time you run git commit, the editor will open with this prefilled structure.
That ensures all developers stick to the same format automatically.
๐ง Optional — Enforce Format with a Commit Linter
You can use commitlint to reject commits that don’t follow the rules.
Install:
npm install --save-dev @commitlint/{config-conventional,cli} husky
Add config files:
commitlint.config.js
export default {
extends: ['@commitlint/config-conventional']
};
.husky/commit-msg (make it executable with chmod +x .husky/commit-msg)
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
Initialize Husky hooks:
npx husky install
npm pkg set scripts.prepare="husky install"
Now any bad commit message (e.g. “update stuff”) will be rejected automatically.
✅ Developer Workflow Example
# Create a feature branch
git checkout -b feature/jsonc-support
# Make changes, then commit
git commit
# The editor opens with the Conventional Commit template.
# Fill it out, for example:
feat(parser): add JSONC comment stripping support
# Push to main
git push origin feature/jsonc-support
When merged into main, your GitHub Action pipeline will:
- Detect
feat→ bump minor version - Update
config.jsoncandchangelog.txt - Create a GitHub release
- Attach build artifacts
A ready-to-copy Husky + Commitlint setup inside your existing GitHub workflow (so all contributors are automatically linted in CI)?
Excellent ๐ฅ — let’s now fully integrate Commitlint + Husky into your existing GitHub Actions and local developer setup.
This ensures that every commit, whether local or from a pull request, follows the Conventional Commits format your Semantic Release system depends on.
๐งฑ Step 1 — Install the Tools
Run these once in your project:
npm install --save-dev husky @commitlint/{config-conventional,cli}
This adds both Husky (for Git hooks) and Commitlint (for validation).
⚙️ Step 2 — Configure Commitlint
Create a file at the root of your repo:
commitlint.config.js
export default {
extends: ['@commitlint/config-conventional'],
};
This tells Commitlint to enforce the official Conventional Commit rules.
⚙️ Step 3 — Set Up Husky Hooks
Initialize Husky:
npx husky install
Then tell npm to reinstall Husky automatically whenever someone clones your repo:
npm pkg set scripts.prepare="husky install"
Now, add a commit hook:
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
Make sure it’s executable:
chmod +x .husky/commit-msg
✅ From now on, every time someone runs git commit, Husky triggers Commitlint — and blocks invalid messages like “updated stuff” or “bug fix”.
๐งฉ Step 4 — Update Your .gitmessage.txt Template (Optional)
You can combine this with the commit template we added earlier for guidance.
Add this to your .gitmessage.txt:
# ๐งฑ Conventional Commit Template
# feat, fix, docs, style, refactor, perf, test, chore
# Example: feat(parser): add JSONC comment stripping
feat(scope): short message
# Body (optional): explain why and what changed
# Footer (optional)
# BREAKING CHANGE: details
# Closes: #123
Then set it globally:
git config commit.template .gitmessage.txt
⚙️ Step 5 — Enforce Commitlint in GitHub Actions (CI Check)
To make sure all contributors (even those not using Husky locally) follow the rules,
add this extra job to your existing GitHub workflow (for example, inside .github/workflows/jsonc-semantic-release.yml):
lint-commits:
name: Validate Conventional Commits
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Commitlint
run: npm install --save-dev @commitlint/{config-conventional,cli}
- name: Lint commit messages
run: |
npx commitlint --from=$(git rev-list --max-parents=0 HEAD) --to=HEAD
This job:
- Checks every commit in the push/PR.
- Fails the workflow if a bad commit message is detected.
✅ Example CI + Local Workflow Summary
| Action | Tool | Result |
|---|---|---|
| Developer commits locally | Husky + Commitlint | Stops invalid commit message |
| Commit message template | .gitmessage.txt | Helps users follow the format |
| GitHub Actions check | lint-commits job | Rejects bad PRs or pushes |
| Semantic Release | GitHub Actions | Publishes new versions automatically |
๐ง Example in Practice
✅ Good:
feat(parser): add support for trailing commas in JSONC
✅ Good:
fix(config): handle missing version field gracefully
❌ Bad:
update parser
❌ Bad:
bug fixNow merge all this (semantic release + commitlint + husky + artifact build) into a single unified GitHub Actions workflow file so you can copy-paste it directly into .github/workflows/ci.yml?
Perfect ๐ช — here’s your complete, production-ready GitHub Actions workflow
that unifies everything:
✅ Semantic Release (auto versioning)
✅ JSONC version updater
✅ Build + attach artifacts (Node.js / Python / Go)
✅ Commitlint validation (Conventional Commits)
✅ Fully automated changelog and GitHub release
๐ .github/workflows/ci.yml
name: JSONC CI & Semantic Release Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: write
issues: read
pull-requests: read
jobs:
# ------------------- ๐งฉ 1. Commit Message Validation -------------------
lint-commits:
name: Validate Conventional Commits
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Commitlint
run: npm install --save-dev @commitlint/{config-conventional,cli}
- name: Lint commit messages
run: npx commitlint --from=$(git rev-list --max-parents=0 HEAD) --to=HEAD
# ------------------- ๐ 2. Build, Version, and Release -------------------
release:
name: Semantic Release & Build
runs-on: ubuntu-latest
needs: lint-commits
steps:
# Checkout full history for semantic-release
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup Node, Python, and Go
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
# Install Dependencies
- name: Install dependencies
run: |
npm install -g semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github @semantic-release/exec
npm install jsonc-parser
pip install wheel
go mod tidy || true
# ------------------- ๐ง JSONC Version Updater -------------------
- name: Create version update script
run: |
cat <<'EOF' > update-jsonc-version.js
import fs from "fs";
import { parse, modify, printParseErrorCode } from "jsonc-parser";
const filePath = "./config.jsonc";
const version = process.env.RELEASE_VERSION;
if (!version) {
console.error("❌ RELEASE_VERSION not set");
process.exit(1);
}
const text = fs.readFileSync(filePath, "utf8");
let errors = [];
const json = parse(text, errors);
if (errors.length) {
console.error("❌ Parse error:", errors.map(e => printParseErrorCode(e.error)).join(", "));
process.exit(1);
}
const edits = modify(text, ["version"], version, { formattingOptions: { insertSpaces: true, tabSize: 2 } });
let result = text;
for (let i = edits.length - 1; i >= 0; i--) {
const e = edits[i];
result = result.slice(0, e.offset) + e.content + result.slice(e.offset + e.length);
}
fs.writeFileSync(filePath, result, "utf8");
console.log("✅ Updated config.jsonc version to", version);
EOF
# ------------------- ๐ฆ Build Artifacts -------------------
- name: Build artifacts
run: |
mkdir -p dist
# Node.js
if [ -f package.json ]; then
npm pack
mv *.tgz dist/
fi
# Python
if [ -f setup.py ]; then
python setup.py sdist bdist_wheel
mv dist/* dist/ 2>/dev/null || true
fi
# Go
if [ -f go.mod ]; then
go build -o dist/jsonc-pipeline
fi
zip -r dist/release_files.zip dist/*
# ------------------- ๐ Run Semantic Release -------------------
- name: Run Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo '{
"branches": ["main"],
"plugins": [
["@semantic-release/commit-analyzer", {"preset": "conventionalcommits"}],
["@semantic-release/release-notes-generator", {"preset": "conventionalcommits"}],
["@semantic-release/changelog", {"changelogFile": "changelog.txt"}],
["@semantic-release/exec", {"prepareCmd": "RELEASE_VERSION=${nextRelease.version} node update-jsonc-version.js"}],
["@semantic-release/git", {
"assets": ["config.jsonc", "changelog.txt", "RELEASE_NOTES.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}"
}],
["@semantic-release/github", {
"assets": [{"path": "dist/release_files.zip", "label": "Release Build"}]
}]
]
}' > .releaserc.json
npx semantic-release
๐งฉ Folder Setup Overview
.
├── config.jsonc
├── changelog.txt
├── RELEASE_NOTES.md
├── package.json
├── setup.py (optional)
├── go.mod (optional)
├── .gitmessage.txt
├── commitlint.config.js
└── .github/
└── workflows/
└── ci.yml
✅ What Happens When You Push
| Step | Action |
|---|---|
| 1️⃣ | GitHub validates commit messages with Commitlint |
| 2️⃣ | Semantic Release runs, analyzes commits |
| 3️⃣ | Updates config.jsonc version |
| 4️⃣ | Updates changelog.txt & RELEASE_NOTES.md |
| 5️⃣ | Builds artifacts (Node, Python, Go) |
| 6️⃣ | Publishes a GitHub Release with ZIP attachment |
| 7️⃣ | Commits version + changelog back to main |
๐ง Example Commit & Result
Commit:
feat(parser): add JSONC comment stripping support
Result:
- Version bump:
1.0.0 → 1.1.0 config.jsoncupdated automaticallychangelog.txtentry added- GitHub Release created with artifact ZIP
add an optional step that automatically publishes the built package
to npm (for Node.js) or PyPI (for Python)
when a release is made?
Excellent ๐ฅ — let’s finish your CI/CD pipeline by adding the final publishing step,
so that every successful release automatically uploads your built packages to:
- ๐ฆ npm (for Node.js modules)
- ๐ PyPI (for Python packages)
- ๐พ (optional) Go release binaries are already uploaded to GitHub
This makes your JSONC project a fully automated, versioned, and deployable package.
๐ Final Version — .github/workflows/ci.yml
Here’s the complete end-to-end workflow ready to copy into your repo:
name: JSONC CI, Release & Publish Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: write
issues: read
pull-requests: read
packages: write
jobs:
# ------------------- ๐งฉ 1. Validate Commit Messages -------------------
lint-commits:
name: Validate Conventional Commits
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Commitlint
run: npm install --save-dev @commitlint/{config-conventional,cli}
- name: Lint commits
run: npx commitlint --from=$(git rev-list --max-parents=0 HEAD) --to=HEAD
# ------------------- ๐ 2. Build, Release, and Publish -------------------
release:
name: Build, Semantic Release & Publish
runs-on: ubuntu-latest
needs: lint-commits
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup environments
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
# Install required dependencies
- name: Install dependencies
run: |
npm install -g semantic-release @semantic-release/changelog @semantic-release/git @semantic-release/github @semantic-release/exec
npm install jsonc-parser
pip install wheel twine
go mod tidy || true
# ------------------- ๐ง JSONC Version Updater -------------------
- name: Create JSONC version update script
run: |
cat <<'EOF' > update-jsonc-version.js
import fs from "fs";
import { parse, modify, printParseErrorCode } from "jsonc-parser";
const filePath = "./config.jsonc";
const version = process.env.RELEASE_VERSION;
if (!version) { console.error("❌ RELEASE_VERSION not set"); process.exit(1); }
const text = fs.readFileSync(filePath, "utf8");
let errors = [];
const json = parse(text, errors);
if (errors.length) {
console.error("❌ Parse error:", errors.map(e => printParseErrorCode(e.error)).join(", "));
process.exit(1);
}
const edits = modify(text, ["version"], version, { formattingOptions: { insertSpaces: true, tabSize: 2 } });
let result = text;
for (let i = edits.length - 1; i >= 0; i--) {
const e = edits[i];
result = result.slice(0, e.offset) + e.content + result.slice(e.offset + e.length);
}
fs.writeFileSync(filePath, result, "utf8");
console.log("✅ Updated config.jsonc version to", version);
EOF
# ------------------- ๐ฆ Build Artifacts -------------------
- name: Build artifacts
run: |
mkdir -p dist
# Node.js package
if [ -f package.json ]; then
npm pack
mv *.tgz dist/
fi
# Python package
if [ -f setup.py ]; then
python setup.py sdist bdist_wheel
mv dist/* dist/ 2>/dev/null || true
fi
# Go binary
if [ -f go.mod ]; then
go build -o dist/jsonc-pipeline
fi
zip -r dist/release_files.zip dist/*
# ------------------- ๐ Semantic Release -------------------
- name: Run Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo '{
"branches": ["main"],
"plugins": [
["@semantic-release/commit-analyzer", {"preset": "conventionalcommits"}],
["@semantic-release/release-notes-generator", {"preset": "conventionalcommits"}],
["@semantic-release/changelog", {"changelogFile": "changelog.txt"}],
["@semantic-release/exec", {"prepareCmd": "RELEASE_VERSION=${nextRelease.version} node update-jsonc-version.js"}],
["@semantic-release/git", {
"assets": ["config.jsonc", "changelog.txt", "RELEASE_NOTES.md"],
"message": "chore(release): ${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}"
}],
["@semantic-release/github", {
"assets": [{"path": "dist/release_files.zip", "label": "Release Build"}]
}]
]
}' > .releaserc.json
npx semantic-release
# ------------------- ๐ค Publish to npm -------------------
- name: Publish to npm
if: ${{ env.NPM_TOKEN != '' && hashFiles('package.json') != '' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
npm publish --access public || echo "⚠️ npm publish skipped"
# ------------------- ๐ Publish to PyPI -------------------
- name: Publish to PyPI
if: ${{ secrets.PYPI_USERNAME != '' && secrets.PYPI_PASSWORD != '' && hashFiles('setup.py') != '' }}
run: |
twine upload dist/* -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} || echo "⚠️ PyPI publish skipped"
๐งพ Required Repository Secrets
| Secret | Purpose |
|---|---|
GITHUB_TOKEN | Auto-generated by GitHub for release commits |
NPM_TOKEN | Your npm auth token (for npm publish) |
PYPI_USERNAME | PyPI username (for Python publish) |
PYPI_PASSWORD | PyPI password or API token |
✅ Summary of What Happens
| Step | Action |
|---|---|
| ๐งฉ Commitlint | Ensures Conventional Commit messages |
| ๐ง JSONC Version Updater | Updates version in config.jsonc |
| ๐️ Build Stage | Builds Node, Python, and Go artifacts |
| ๐ Semantic Release | Bumps version, updates changelog, tags release |
| ๐ฆ Publish | Pushes to npm and/or PyPI |
| ๐พ GitHub Release | Uploads artifacts ZIP automatically |
๐ง Example
Commit:
feat(parser): add JSONC multiline comment support
Result:
- Version:
1.0.0 → 1.1.0 config.jsoncupdatedchangelog.txtupdated- GitHub release created with ZIP
- npm and/or PyPI publish triggered ๐ฏ
✅ You now have a complete, professional-grade CI/CD setup
for JSONC-based projects — ready to auto-version, release, and deploy
across Node.js, Python, and Go environments.