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
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "sentinel",
"name": "SENTINEL",
"version": "0.1.3",
"version": "1.0.0",
"description": "AI-powered contextual security auditing. Red-team-grade SAST: taint flow, attack graphs, exploit chains, hardening.",
"author": "Wembie",
"repo": "https://github.com/Wembie/Sentinel",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sentinel",
"version": "0.1.3",
"version": "1.0.0",
"description": "AI-powered contextual security auditing platform — red-team-grade SAST that thinks like an attacker.",
"author": "Wembie",
"license": "MIT",
Expand Down
8 changes: 1 addition & 7 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,7 @@ jobs:
uses: astral-sh/setup-uv@v4

- name: Ensure static manifests are version-synced
shell: bash
run: |
python scripts/sync_version.py
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Static manifests are out of sync with VERSION. Run: python scripts/sync_version.py and commit the updates."
exit 1
fi
run: python scripts/release/sync_version.py --check

- name: Build package
run: uv build
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
with:
python-version: "3.11"

- name: Ensure static manifests are version-synced
run: python scripts/release/sync_version.py --check

- name: Install uv
uses: astral-sh/setup-uv@v4

Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sentinel",
"version": "0.1.3",
"version": "1.0.0",
"description": "AI-powered contextual security auditing platform. Performs offensive-minded SAST: taint flow tracing, attack graph generation, exploit chain narratives, and hardening checklists.",
"author": "Wembie",
"repo": "https://github.com/Wembie/Sentinel",
Expand Down
26 changes: 26 additions & 0 deletions scripts/release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Release Scripts

These scripts keep `VERSION` and the static release manifests in sync.

## Commands

```bash
python scripts/release/bump_version.py {major}.{minor}.{patch}
python scripts/release/bump_version.py 1.0.0
python scripts/release/bump_version.py patch
python scripts/release/bump_version.py minor --commit
python scripts/release/sync_version.py
python scripts/release/sync_version.py --check
```

## What Each Script Does

- `bump_version.py`: updates `VERSION`, then syncs all static manifests.
- `sync_version.py`: syncs the current `VERSION` value into the static manifests.

## Suggested Flow

1. Bump the version with `python scripts/release/bump_version.py <version-or-bump>`.
2. Review the changes.
3. Commit and push.
4. Let CI/CD validate with `python scripts/release/sync_version.py --check`.
146 changes: 146 additions & 0 deletions scripts/release/bump_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Bump VERSION and sync static manifests in one command.

Examples:
python scripts/release/bump_version.py 1.0.1
python scripts/release/bump_version.py patch
python scripts/release/bump_version.py minor --commit
python scripts/release/bump_version.py 2.0.0 --dry-run
"""
from __future__ import annotations

import argparse
import re
import subprocess
from pathlib import Path

import sync_version

ROOT = sync_version.ROOT
VERSION_FILE = ROOT / "VERSION"
RELEASE_RE = re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:[-+].+)?$")
BUMP_KINDS = {"major", "minor", "patch"}


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Update VERSION and sync all static manifests."
)
parser.add_argument(
"target",
help="Exact version (e.g. 1.0.1) or bump kind: major, minor, patch.",
)
parser.add_argument(
"--commit",
action="store_true",
help="Create a git commit with only the version-sync files.",
)
parser.add_argument(
"--message",
help="Custom commit message. Only used with --commit.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the changes that would be made without writing files.",
)
return parser.parse_args()


def resolve_target_version(current_version: str, target: str) -> str:
normalized = target.strip().lower()
if normalized in BUMP_KINDS:
match = RELEASE_RE.fullmatch(current_version)
if not match:
raise ValueError(
"Automatic major/minor/patch bumps require VERSION to be in X.Y.Z format."
)
major = int(match.group("major"))
minor = int(match.group("minor"))
patch = int(match.group("patch"))
if normalized == "major":
return f"{major + 1}.0.0"
if normalized == "minor":
return f"{major}.{minor + 1}.0"
return f"{major}.{minor}.{patch + 1}"

if not sync_version.is_valid_semver(target):
raise ValueError(
f"Invalid version {target!r}. Use semver like 1.0.1 or a bump kind."
)
return target
Comment on lines +67 to +71


def _write_version(current_version: str, target_version: str, *, dry_run: bool = False) -> bool:
if current_version == target_version:
print(f" OK VERSION: already {target_version!r}")
return False
if dry_run:
print(f" DRY VERSION: {current_version!r} -> {target_version!r}")
return True
VERSION_FILE.write_text(f"{target_version}\n", encoding="utf-8")
print(f" BUMP VERSION: {current_version!r} -> {target_version!r}")
return True


def bump_version(target: str, *, dry_run: bool = False) -> tuple[str, bool, int]:
current_version = sync_version.read_version(VERSION_FILE)
target_version = resolve_target_version(current_version, target)

print(f"Bumping project version toward {target_version!r}...\n")
version_changed = _write_version(current_version, target_version, dry_run=dry_run)
manifest_changes = sync_version.sync_static_manifests(target_version, dry_run=dry_run)
return target_version, version_changed, manifest_changes


def maybe_commit(target_version: str, message: str | None = None) -> bool:
paths = [VERSION_FILE, *sync_version.JSON_TARGETS, *sync_version.YAML_TARGETS]
rel_paths = [str(path.relative_to(ROOT)) for path in paths]

subprocess.run(["git", "add", "--", *rel_paths], cwd=ROOT, check=True)

diff_result = subprocess.run(
["git", "diff", "--cached", "--quiet", "--", *rel_paths],
cwd=ROOT,
check=False,
)
if diff_result.returncode == 0:
print("No version-sync changes to commit.")
return False
if diff_result.returncode != 1:
raise subprocess.CalledProcessError(diff_result.returncode, diff_result.args)

commit_message = message or f"chore: bump version to {target_version}"
subprocess.run(
["git", "commit", "-m", commit_message, "--", *rel_paths],
cwd=ROOT,
check=True,
)
print(f"Created commit: {commit_message}")
return True


def main() -> None:
args = parse_args()
if args.dry_run and args.commit:
raise SystemExit("ERROR: --commit cannot be used with --dry-run")
if args.message and not args.commit:
raise SystemExit("ERROR: --message requires --commit")

target_version, version_changed, manifest_changes = bump_version(
args.target, dry_run=args.dry_run
)

total_changed = int(version_changed) + manifest_changes
print()
if args.dry_run:
print(f"Dry-run complete. {total_changed} file(s) would be updated.")
return

print(f"Done. {total_changed} file(s) updated.")
if args.commit:
maybe_commit(target_version, args.message)


if __name__ == "__main__":
main()
71 changes: 53 additions & 18 deletions scripts/sync_version.py → scripts/release/sync_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
"""Sync the VERSION file to all static manifests.

Run before tagging a release:
python scripts/sync_version.py
python scripts/release/sync_version.py

Dry-run (print diffs, write nothing):
python scripts/sync_version.py --dry-run
python scripts/release/sync_version.py --dry-run

Check mode (exit non-zero if files need updates):
python scripts/release/sync_version.py --check
"""
from __future__ import annotations

Expand All @@ -14,8 +17,8 @@
import sys
from pathlib import Path

ROOT = Path(__file__).parent.parent
DRY_RUN = "--dry-run" in sys.argv
ROOT = Path(__file__).resolve().parents[2]
SEMVER_RE = re.compile(r"\d+\.\d+\.\d+(?:[-+].+)?")

# JSON files where {"version": "<value>"} must be updated.
JSON_TARGETS: list[Path] = [
Expand All @@ -30,17 +33,21 @@
]


def _read_version() -> str:
vf = ROOT / "VERSION"
def is_valid_semver(version: str) -> bool:
return bool(SEMVER_RE.fullmatch(version))


def read_version(vf: Path | None = None) -> str:
vf = vf or (ROOT / "VERSION")
if not vf.exists():
sys.exit(f"ERROR: VERSION file not found at {vf}")
v = vf.read_text(encoding="utf-8").strip()
if not re.fullmatch(r"\d+\.\d+\.\d+(?:[-+].+)?", v):
if not is_valid_semver(v):
sys.exit(f"ERROR: VERSION contains invalid semver: {v!r}")
return v


def _sync_json(path: Path, version: str) -> bool:
def _sync_json(path: Path, version: str, *, dry_run: bool = False, check_only: bool = False) -> bool:
if not path.exists():
print(f" SKIP (missing): {path.relative_to(ROOT)}")
return False
Expand All @@ -52,15 +59,16 @@ def _sync_json(path: Path, version: str) -> bool:
old = data.get("version", "<none>")
data["version"] = version
new_raw = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
if DRY_RUN:
print(f" DRY {path.relative_to(ROOT)}: {old!r} -> {version!r}")
if dry_run or check_only:
prefix = "DRY " if dry_run else "NEED"
print(f" {prefix} {path.relative_to(ROOT)}: {old!r} -> {version!r}")
return True
path.write_text(new_raw, encoding="utf-8")
print(f" BUMP {path.relative_to(ROOT)}: {old!r} -> {version!r}")
return True


def _sync_yaml(path: Path, version: str) -> bool:
def _sync_yaml(path: Path, version: str, *, dry_run: bool = False, check_only: bool = False) -> bool:
if not path.exists():
print(f" SKIP (missing): {path.relative_to(ROOT)}")
return False
Expand All @@ -75,28 +83,55 @@ def _sync_yaml(path: Path, version: str) -> bool:
print(f" OK (current): {path.relative_to(ROOT)}")
return False
new_raw = pattern.sub(f'version: "{version}"', raw, count=1)
if DRY_RUN:
print(f" DRY {path.relative_to(ROOT)}: {old!r} -> {version!r}")
if dry_run or check_only:
prefix = "DRY " if dry_run else "NEED"
print(f" {prefix} {path.relative_to(ROOT)}: {old!r} -> {version!r}")
return True
path.write_text(new_raw, encoding="utf-8")
print(f" BUMP {path.relative_to(ROOT)}: {old!r} -> {version!r}")
return True


def main() -> None:
version = _read_version()
def sync_static_manifests(
version: str | None = None, *, dry_run: bool = False, check_only: bool = False
) -> int:
if dry_run and check_only:
raise ValueError("use only one of dry_run or check_only")

version = version or read_version()
if not is_valid_semver(version):
raise ValueError(f"invalid semver: {version!r}")

print(f"Syncing version {version!r} to static manifests...\n")

changed = 0
for p in JSON_TARGETS:
if _sync_json(p, version):
if _sync_json(p, version, dry_run=dry_run, check_only=check_only):
changed += 1
for p in YAML_TARGETS:
if _sync_yaml(p, version):
if _sync_yaml(p, version, dry_run=dry_run, check_only=check_only):
changed += 1
return changed


def main() -> None:
dry_run = "--dry-run" in sys.argv
check_only = "--check" in sys.argv

if dry_run and check_only:
sys.exit("ERROR: use only one of --dry-run or --check")

changed = sync_static_manifests(dry_run=dry_run, check_only=check_only)

print()
if DRY_RUN:
if check_only:
if changed:
sys.exit(
"ERROR: Static manifests are out of sync with VERSION. "
"Run: python scripts/release/sync_version.py and commit the updates."
)
print("Check complete. All static manifests are in sync.")
elif dry_run:
print(f"Dry-run complete. {changed} file(s) would be updated.")
else:
print(f"Done. {changed} file(s) updated.")
Expand Down
2 changes: 1 addition & 1 deletion sentinel.skill
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: sentinel
version: "0.1.3"
version: "1.0.0"
description: AI-powered contextual security auditing — red-team-grade SAST, taint flow, attack graphs, exploit chains
author: Wembie
repo: https://github.com/Wembie/Sentinel
Expand Down
Loading
Loading