Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/release/pre-commit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use strict'

/**
* conventional-changelog-action@v5 pre-commit hook.
*
* Runs AFTER the changelog/version-file bump and BEFORE the action's
* git add/commit/tag. Files staged here land in the release commit AND
* the tag, so tag-pinned consumers (agent-plugins pins ref:vX.Y.Z) get a
* tree whose SKILL.md / .codex-plugin/plugin.json versions match the tag.
*
* This replaces the old post-tag amend + force-push, which left the tag
* pointing at a pre-sync tree.
*
* Node built-ins only (fs, path, child_process). No npm deps. The action
* does NOT auto-stage hook-modified files (only version-file + the
* changelog), so the hook stages them explicitly.
*
* SKILL.md metadata.version stays the single CI-owned version source;
* .codex-plugin/plugin.json mirrors it for the Codex plugin host.
*/

const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

const SKILL_REL = 'skills/terraform-skill/SKILL.md'
const MANIFEST_REL = '.codex-plugin/plugin.json'
const SENTINEL_REL = 'version.json'

function repoRoot() {
const root = process.env.GITHUB_WORKSPACE || process.cwd()
if (!fs.existsSync(path.join(root, SENTINEL_REL))) {
throw new Error(
`pre-commit: sentinel ${SENTINEL_REL} not found in repo root ` +
`${root}; refusing to run (wrong working directory?)`
)
}
return root
}

// Rewrite the SKILL.md YAML frontmatter " version: X.Y.Z" line in place,
// preserving the exact 2-space indent (version is a child of metadata:).
function updateSkillVersion(root, version) {
const file = path.join(root, SKILL_REL)
if (!fs.existsSync(file)) {
throw new Error(`pre-commit: ${SKILL_REL} not found`)
}
const lines = fs.readFileSync(file, 'utf8').split('\n')
let updated = false
// Frontmatter is the leading block; the version line sits within the
// first handful of lines. Bound the search to avoid a body match.
const limit = Math.min(lines.length, 15)
for (let i = 0; i < limit; i++) {
if (lines[i].trimStart().startsWith('version:')) {
lines[i] = ` version: ${version}`
updated = true
break
}
}
if (!updated) {
throw new Error(
`pre-commit: version line not found in ${SKILL_REL} frontmatter`
)
}
fs.writeFileSync(file, lines.join('\n'))
return SKILL_REL
}

// Value-only replace of the top-level " "version": "..."" line.
// Anchored to a 2-space indent; rewrites only the quoted value so the
// trailing comma / key position / byte layout are untouched (no JSON
// re-serialize, no diff noise, position-independent).
function updateCodexManifest(root, version) {
const file = path.join(root, MANIFEST_REL)
if (!fs.existsSync(file)) {
return null
}
const src = fs.readFileSync(file, 'utf8')
const re = /^( {2}"version":\s*)"[^"]*"/m
if (!re.test(src)) {
throw new Error(
`pre-commit: top-level "version" line not found in ${MANIFEST_REL}`
)
}
fs.writeFileSync(file, src.replace(re, `$1"${version}"`))
return MANIFEST_REL
}

async function preCommit(props) {
const version = props && props.version
if (!version || typeof version !== 'string') {
throw new Error(`pre-commit: invalid version from action: ${version}`)
}

let root
try {
root = repoRoot()
const skill = updateSkillVersion(root, version)
console.log(`pre-commit: synced ${skill} -> ${version}`)

const manifest = updateCodexManifest(root, version)
if (manifest) {
console.log(`pre-commit: synced ${manifest} -> ${version}`)
} else {
console.log(`pre-commit: ${MANIFEST_REL} absent; skipped`)
}

// The action stages only version-file + changelog. Stage ours so
// they are in the release commit and the tag.
const toStage = [skill]
if (manifest) toStage.push(manifest)
execSync(`git add ${toStage.join(' ')}`, { cwd: root, stdio: 'inherit' })
} catch (err) {
// Fail loud so the release job stops rather than tagging a stale tree.
console.error(`pre-commit: ${err && err.message ? err.message : err}`)
throw err
}
}

module.exports = { preCommit }
109 changes: 5 additions & 104 deletions .github/workflows/automated-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,110 +47,11 @@ jobs:
skip-on-empty: 'false'
skip-version-file: 'false'
skip-commit: 'false'

# 2b. Sync version fields and git ref
# This ensures all version fields stay synchronized:
# - root version.json (updated by conventional-changelog-action above)
# - SKILL.md metadata.version (synced here)
# - .codex-plugin/plugin.json version (synced here, if present)
- name: Sync Plugin Version and SKILL.md
if: steps.changelog.outputs.skipped == 'false'
env:
VERSION: ${{ steps.changelog.outputs.version }}
run: |
python3 << 'EOF'
import json
import os
import sys

def update_skill_version(version):
"""Update version in SKILL.md YAML frontmatter."""
skill_path = 'skills/terraform-skill/SKILL.md'

if not os.path.exists(skill_path):
raise FileNotFoundError(f"{skill_path} not found")

with open(skill_path, 'r') as f:
lines = f.readlines()

# Find and update version line in frontmatter (typically line 7)
updated = False
for i, line in enumerate(lines):
# Match " version: X.Y.Z" pattern (2 spaces indent)
if line.strip().startswith('version:') and i < 10: # Within frontmatter
lines[i] = f' version: {version}\n'
updated = True
break

if not updated:
raise ValueError("Could not find version field in SKILL.md frontmatter")

with open(skill_path, 'w') as f:
f.writelines(lines)

return skill_path

def update_codex_manifest(version):
"""Mirror the version into .codex-plugin/plugin.json if present.

The Codex plugin host reads this manifest when the
agent-plugins marketplace resolves terraform-skill as a
url-source plugin. Keep its version in lockstep with the
SKILL.md single version source so it never goes stale.
"""
manifest_path = '.codex-plugin/plugin.json'

if not os.path.exists(manifest_path):
return None

with open(manifest_path, 'r') as f:
lines = f.readlines()

updated = False
for i, line in enumerate(lines):
# Top-level key only: exactly 2-space indent. Anchored so a
# nested "version" key could never be matched by accident.
if line.startswith(' "version":'):
lines[i] = f' "version": "{version}"\n'
updated = True
break

if not updated:
raise ValueError(
"Could not find version field in .codex-plugin/plugin.json")

with open(manifest_path, 'w') as f:
f.writelines(lines)

return manifest_path

try:
version = os.environ['VERSION']

# SKILL.md frontmatter is the single version source.
skill_path = update_skill_version(version)
print(f"✅ Synced {skill_path} metadata.version to {version}")

manifest_path = update_codex_manifest(version)
if manifest_path:
print(f"✅ Synced {manifest_path} version to {version}")
else:
print("ℹ️ No .codex-plugin/plugin.json; skipped")

except Exception as e:
print(f"❌ ERROR: Failed to sync versions: {e}")
sys.exit(1)
EOF

# Commit the sync
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add version.json skills/terraform-skill/SKILL.md
if [ -f .codex-plugin/plugin.json ]; then
git add .codex-plugin/plugin.json
fi
git commit --amend --no-edit
git push --force-with-lease
skip-ci: 'true'
# Sync SKILL.md + .codex-plugin/plugin.json BEFORE the action's
# commit/tag so tag-pinned consumers get a matching tree. Replaces
# the old post-tag amend + force-push.
pre-commit: '.github/release/pre-commit.js'

# 3. Create GitHub Release
# Only run if a new version was created
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ on:
- 'skills/**'
- '.claude-plugin/**'
- '.codex-plugin/**'
- '.github/workflows/**'
- '.github/release/**'
push:
branches: [master, main]
paths:
- 'skills/**'
- '.claude-plugin/**'
- '.codex-plugin/**'
- '.github/workflows/**'
- '.github/release/**'
workflow_dispatch:

jobs:
Expand Down
Loading