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-parser internally for .jsonc files.
๐ป 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 for json that 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/json does 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-parser can modify JSONC text while preserving comments.strip-json-comments + JSON.stringify can 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:
commentjson can dump JSONC with comments, but you must include them manually as part of the string.json5 can 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/jsonc strip 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
.jsonc configuration file with comments. - Modify a setting (e.g.,
theme or autoSave). - 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
commentjson for 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() or encoding/json directly on JSONC — they will throw syntax errors. - Keep a
.jsonc template 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
.jsonc files (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
➕.
๐งพ 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.txt file. - ๐งช 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
rollback command to restore previous JSONC versions. - Containerize (e.g.,
Dockerfile with 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
.git repo 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 (
v1 tag) - Pushes (if remote added)
✅ Python edits → creates settings.v2.jsonc
- Commits to Git (
v2 tag) - Pushes
✅ Go edits → creates settings.v3.jsonc
- Commits to Git (
v3 tag) - Pushes
๐งพ 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.md content automatically)?
This would make your pipeline publish formal releases to GitHub each time v1, v2, v3 are 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 origin
https://github.com/<your-username>
/<repo-name>.git
Install Dependencies
npm install jsonc-parser
pip install commentjson
go get github.com/tidwall/jsonc
Run the Complete Pipeline
node run_pipeline.js
Check 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.md updated- 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 origin
https://github.com/<your-
username>/<repo>.git
Install Dependencies
npm install jsonc-parser
Set Your GitHub Token
export GITHUB_TOKEN=ghp_yourtoken
Run 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
.jsonc file - 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>.git
Set GitHub Token
export GITHUB_TOKEN=ghp_yourtoken
Run
- Node.js →
node run_pipeline.js - Python →
python run_pipeline.py - Go →
go run run_pipeline.go
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_TOKEN automatically. - It has permission to create commits, tags, and releases.
๐งพ Example Workflow Output
After merging or pushing to main:
config.jsonc gets updated (version → next value)changelog.txt and RELEASE_NOTES.md updated- 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.md as the release description - ๐ฆ
release_files.zip containing:- Node.js
.tgz - Python
.whl / .tar.gz - Go binary
๐งฐ 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 go
build -o dist/jsonc-pipeline-linux .
GOOS=darwin GOARCH=arm64 go
build -o dist/jsonc-pipeline-mac .
- For Node.js, you can replace the
npm pack step with a real build command (like npm 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.jsonc version (if included in your script) - Generates changelog entries in
changelog.txt - Publishes a GitHub Release with attached ZIP artifacts
Artifacts Attached
- Node
.tgz, Python .whl / .tar.gz, or Go binaries - All compressed in
release_files.zip
✅ Example Output
After merging a PR:
config.jsonc → "version": "v1.2.0"changelog.txt updated- Git tag
v1.2.0 created - 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.txt with 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 one fix: → 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.jsonc version - 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.jsonc and changelog.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 fix
Now 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.jsonc updated automaticallychangelog.txt entry 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.jsonc updatedchangelog.txt updated- 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.