Skip to content
Open
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
30 changes: 23 additions & 7 deletions .github/scripts/generate_release_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import datetime
import argparse
import subprocess
import sys

def get_current_date_version():
"""Returns the current date in Year.Month format (e.g., 2025.12)."""
Expand Down Expand Up @@ -49,15 +50,16 @@ def calculate_next_version(base_version, tags, release_type="stable"):
next_patch = max_patch + 1
base_ver = f"{base_version}.{next_patch}"

# Add suffix based on release type
# Add suffix based on release type (PEP 440 compliant)
if release_type == "prerelease":
return f"{base_ver}-beta"
return f"{base_ver}b1" # beta1 is PEP 440 compliant
elif release_type == "development":
try:
sha = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], text=True).strip()
return f"{base_ver}-dev-{sha}"
# PEP 440: Use .dev0 for development, +sha for build metadata
return f"{base_ver}.dev0+{sha}"
except Exception:
return f"{base_ver}-dev"
return f"{base_ver}.dev0"
else: # stable
return base_ver

Expand All @@ -70,9 +72,23 @@ def main():
help="Type of release (stable, prerelease, development)")
args = parser.parse_args()

base_ver = get_current_date_version()
tags = get_existing_tags()
next_version = calculate_next_version(base_ver, tags, args.release_type)
# Fallback version if version generation fails
FALLBACK_VERSION = "2026.1.2"

try:
base_ver = get_current_date_version()
tags = get_existing_tags()
next_version = calculate_next_version(base_ver, tags, args.release_type)

# Validate version format (PEP 440 compliant)
# Pattern matches MAJOR.MINOR.PATCH with optional pre-release (.dev0, .a1, .b1, .rc1) and build (+build) suffixes
# Examples: 2026.1.2, 2026.1.2.dev0+9d07a00, 2026.1.2b1, 2026.1.2+9d07a00
if not next_version or not re.match(r'^[0-9]+\.[0-9]+\.[0-9]+(\.[a-z]+[0-9]+|[a-z]+[0-9]+)?(\+[a-zA-Z0-9.-]+)?$', next_version):
print(f"Warning: Generated version '{next_version}' is invalid, using fallback: {FALLBACK_VERSION}", file=sys.stderr)
next_version = FALLBACK_VERSION
except Exception as e:
print(f"Error generating version: {e}, using fallback: {FALLBACK_VERSION}", file=sys.stderr)
next_version = FALLBACK_VERSION

print(next_version)

Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci-orchestrator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ jobs:
run: |
echo "Processing PR #$PR_NUMBER for Auto-Merge..."

# 0. Check for no-auto-merge label
echo "Checking for no-auto-merge label..."
HAS_NO_AUTO_MERGE=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[] | select(.name == "no-auto-merge") | .name' | head -n 1)
if [[ -n "$HAS_NO_AUTO_MERGE" ]]; then
echo "::notice::PR has 'no-auto-merge' label. Skipping auto-merge."
exit 0
fi
echo "No 'no-auto-merge' label found. Proceeding..."

# 1. Check CI Status
# Implicitly passed because this job 'needs' build/test-backend.
echo "CI Orchestrator pipeline passed."
Expand Down
67 changes: 48 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
name: CI

# on:
# push:
# branches: [ main ]
# paths-ignore:
# - '**.md'
# - 'docs/**'
# - 'LICENSE'
# - '.github/ISSUE_TEMPLATE/**'
# pull_request:
# branches: [ main ]
# paths-ignore:
# - '**.md'
# - 'docs/**'
# - 'LICENSE'
# - '.github/ISSUE_TEMPLATE/**'
on:
push:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.github/ISSUE_TEMPLATE/**'
pull_request:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:

concurrency:
Expand All @@ -29,36 +28,62 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.11", "3.12"]
python-version: ["3.14", "3.15"]

steps:
- uses: actions/checkout@v6

# Set condition as env variable for reuse across steps
# Condition: Python 3.14 always runs, Python 3.15 only on main branch push events
- name: Set condition variable
id: should_run
run: |
if [[ "${{ matrix.python-version }}" == "3.14" ]] || \
([[ "${{ matrix.python-version }}" == "3.15" ]] && \
[[ "${{ github.event_name }}" == "push" ]] && \
[[ "${{ github.ref }}" == "refs/heads/main" ]]); then
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "should_run=false" >> $GITHUB_OUTPUT
fi
shell: bash

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
if: steps.should_run.outputs.should_run == 'true'
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: ${{ matrix.python-version == '3.15' }}

- name: Install dependencies
if: steps.should_run.outputs.should_run == 'true'
run: |
python -m pip install --upgrade pip
pip install .
pip install .[test,modern]
pip install typing-extensions rich click pefile olefile py7zr PyYAML requests pillow pytest packaging
# Install dev dependencies if needed, or just run tests if they don't need extra
# Install test and modern dependencies (includes flet for GUI tests)

- name: Run Tests
if: steps.should_run.outputs.should_run == 'true'
timeout-minutes: 20
run: |
export PYTHONPATH=$PYTHONPATH:$(pwd)/src:$(pwd)/src/switchcraft:$(pwd)/src/switchcraft_advanced:$(pwd)/src/switchcraft_ai:$(pwd)/src/switchcraft_winget:$(pwd)/src/switchcraft_debug
export CI=true
export GITHUB_ACTIONS=true
python -m pytest tests
shell: bash

- name: Smoke Test (CLI)
if: steps.should_run.outputs.should_run == 'true'
timeout-minutes: 5
run: |
python -m switchcraft.main --version
python -m switchcraft.main --help
shell: bash

- name: Linting
if: steps.should_run.outputs.should_run == 'true'
timeout-minutes: 10
run: |
pip install flake8
# Stop the build if there are Python syntax errors or undefined names
Expand All @@ -68,6 +93,8 @@ jobs:
shell: bash

- name: Check Translations (i18n)
if: steps.should_run.outputs.should_run == 'true'
timeout-minutes: 5
run: |
python scripts/check_i18n.py
shell: bash
Expand All @@ -89,11 +116,13 @@ jobs:
python-version: '3.14'

- name: Install Dependencies
timeout-minutes: 10
run: |
python -m pip install --upgrade pip
pip install .
pip install .[gui]
pip install pyinstaller customtkinter tkinterdnd2 pillow

- name: Build with PyInstaller
timeout-minutes: 15
run: |
pyinstaller switchcraft.spec
1 change: 1 addition & 0 deletions .github/workflows/docs_preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
source-dir: docs/.vitepress/dist
preview-branch: gh-pages
umbrella-dir: pr-preview
pages-base-path: /SwitchCraft
action: auto

cleanup-preview:
Expand Down
46 changes: 39 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,20 @@ jobs:

- name: Generate Version
id: version
run: python .github/scripts/generate_release_version.py --type ${{ github.event.inputs.release_type }}
run: |
VERSION=$(python .github/scripts/generate_release_version.py --type ${{ github.event.inputs.release_type }})
# Fallback to 2026.1.2 if version generation fails or returns invalid version
# Strict regex: PEP 440 compliant format
# Matches MAJOR.MINOR.PATCH with optional pre-release (.dev0, .a1, .b1, .rc1) and build (+build) suffixes
# Examples: 2026.1.2, 2026.1.2.dev0+9d07a00, 2026.1.2b1, 2026.1.2+9d07a00
# Pattern: ^[0-9]+\.[0-9]+\.[0-9]+(\.[a-z]+[0-9]+|[a-z]+[0-9]+)?(\+[a-zA-Z0-9.-]+)?$
if [ -z "$VERSION" ] || [ "$VERSION" = "0.0.0" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(\.[a-z]+[0-9]+|[a-z]+[0-9]+)?(\+[a-zA-Z0-9.-]+)?$'; then
echo "Warning: Version generation failed or returned invalid version '$VERSION', using fallback: 2026.1.2"
VERSION="2026.1.2"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_prerelease=${{ github.event.inputs.release_type != 'stable' }}" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION"

- name: Update Version in Files
run: |
Expand Down Expand Up @@ -133,30 +146,49 @@ jobs:
if: ${{ github.event.inputs.release_type == 'stable' }}
run: |
VERSION="${{ steps.version.outputs.version }}"
# Calculate next dev version (increment patch and add -dev)
# Calculate next dev version (increment patch and use PEP 440 compliant format)
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
PATCH=$(echo $VERSION | cut -d. -f3)
NEXT_PATCH=$((PATCH + 1))
DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-dev"

# Get commit SHA for build metadata (PEP 440: X.Y.Z.dev0+sha)
SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "")
if [ -n "$SHA" ]; then
DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev0+${SHA}"
else
DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev0"
fi

# Use release version as fallback (without .dev0 suffix)
FALLBACK_VERSION="${MAJOR}.${MINOR}.${PATCH}"

echo "Setting development version to $DEV_VERSION"
echo "Updating fallback versions to $FALLBACK_VERSION"

# Update version files
sed -i "s/version = \".*\"/version = \"$DEV_VERSION\"/" pyproject.toml
sed -i "s/__version__ = \".*\"/__version__ = \"$DEV_VERSION\"/" src/switchcraft/__init__.py
python .github/scripts/update_version_info.py "$DEV_VERSION"

# Update .iss files (Inno Setup installer scripts) - keep base version without -dev suffix for installer
BASE_VERSION=$(echo $DEV_VERSION | sed 's/-dev$//')
# Update .iss files (Inno Setup installer scripts) - keep base version without .dev0 suffix for installer
BASE_VERSION=$(echo $DEV_VERSION | sed 's/\.dev0.*$//')
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$BASE_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_modern.iss
sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$BASE_VERSION\"/" switchcraft_legacy.iss
sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_legacy.iss

# Update fallback versions in build scripts and version generator
# Update build_release.ps1 fallback
sed -i "s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
sed -i "s/\$AppVersionNumeric = \".*\"/\$AppVersionNumeric = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1

# Update generate_release_version.py fallback
sed -i "s/FALLBACK_VERSION = \".*\"/FALLBACK_VERSION = \"$FALLBACK_VERSION\"/" .github/scripts/generate_release_version.py

Comment on lines +181 to +188
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current format of AppVersion assignment in build_release.ps1
echo "=== Checking AppVersion assignments ===" 
grep -n 'AppVersion' scripts/build_release.ps1 | head -20
echo ""
echo "=== Checking context around line 156 ==="
sed -n '150,165p' scripts/build_release.ps1

Repository: FaserF/SwitchCraft

Length of output: 2092


The sed pattern will not match the conditional assignment syntax in build_release.ps1.

Line 156 of scripts/build_release.ps1 uses a conditional:

$AppVersion = if ($env:SWITCHCRAFT_VERSION) { $env:SWITCHCRAFT_VERSION } else { "2026.1.2" }

The sed pattern s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/ expects $AppVersion = "..." but will not match this if-else syntax. The command will silently fail, leaving the fallback version unchanged.

🔧 Suggested fix

Target the fallback value inside the else block:

-          sed -i "s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
-          sed -i "s/\$AppVersionNumeric = \".*\"/\$AppVersionNumeric = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
+          # Update the fallback version in the else block
+          sed -i "s/else { \"[0-9]*\.[0-9]*\.[0-9]*\" }/else { \"$FALLBACK_VERSION\" }/" scripts/build_release.ps1

Alternatively, consider extracting the fallback to a dedicated variable at the top of the script that's easier to update with sed.

🤖 Prompt for AI Agents
In @.github/workflows/release.yml around lines 181 - 188, The sed replacements
target a simple assignment but the script uses a conditional assignment for
$AppVersion and $AppVersionNumeric (e.g. $AppVersion = if (...) { ... } else {
"2026.1.2" }), so update the workflow to either (A) change the sed regex to
match the else-block pattern and replace the literal inside the else { "..." }
for both $AppVersion and $AppVersionNumeric, or (B) modify the script to expose
a single fallback variable (e.g. $AppVersionFallback) that is used in the
conditional and then change the workflow to sed-replace that fallback variable
(and keep the existing replacement for FALLBACK_VERSION in
generate_release_version.py); target the symbols $AppVersion,
$AppVersionNumeric, FALLBACK_VERSION and the sed commands in the workflow when
making the change.

# Commit
git add pyproject.toml src/switchcraft/__init__.py file_version_info.txt switchcraft_modern.iss switchcraft_legacy.iss
git commit -m "chore: bump version to $DEV_VERSION for development [skip ci]"
git add pyproject.toml src/switchcraft/__init__.py file_version_info.txt switchcraft_modern.iss switchcraft_legacy.iss scripts/build_release.ps1 .github/scripts/generate_release_version.py
git commit -m "chore: bump version to $DEV_VERSION and update fallback versions to $FALLBACK_VERSION [skip ci]"
git push origin main

build:
Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/review-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,25 @@ jobs:
run: |
echo "Processing PR #$PR_NUMBER for Auto-Merge (Trigger: Review)..."

# 0. Check for no-auto-merge label
echo "Checking for no-auto-merge label..."
HAS_NO_AUTO_MERGE=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[] | select(.name == "no-auto-merge") | .name' | head -n 1)
if [[ -n "$HAS_NO_AUTO_MERGE" ]]; then
echo "::notice::PR has 'no-auto-merge' label. Skipping auto-merge."
exit 0
fi
echo "No 'no-auto-merge' label found. Proceeding..."

# 1. Check CI Status (Must manually verify CI Orchestrator passed)
echo "Checking CI Status for PR #$PR_NUMBER..."
PR_HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
CI_CONCLUSION=$(gh run list --commit "$PR_HEAD_SHA" --workflow "CI Orchestrator" --json conclusion --jq '.[0].conclusion')
# Use workflow file name instead of display name
CI_CONCLUSION=$(gh run list --commit "$PR_HEAD_SHA" --workflow "ci-orchestrator.yml" --json conclusion --jq '.[0].conclusion' || echo "null")

if [[ "$CI_CONCLUSION" == "null" || "$CI_CONCLUSION" == "" ]]; then
echo "::notice::CI Orchestrator workflow not found or not yet run. Skipping merge."
exit 0
fi

if [[ "$CI_CONCLUSION" != "success" ]]; then
echo "::notice::CI Orchestrator is not 'success' (current: $CI_CONCLUSION). Skipping merge."
Expand Down
26 changes: 24 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,47 @@ jobs:
fail-fast: true
max-parallel: 1
matrix:
python-version: ["3.13", "3.14"]
python-version: ["3.14", "3.15"]

steps:
- uses: actions/checkout@v6

# Set condition as env variable for reuse across steps
- name: Set condition variable
id: should_run
run: |
if [[ "${{ matrix.python-version }}" == "3.14" ]] || \
([[ "${{ matrix.python-version }}" == "3.15" ]] && \
[[ "${{ github.event_name }}" == "push" ]] && \
[[ "${{ github.ref }}" == "refs/heads/main" ]]); then
echo "should_run=true" >> $GITHUB_OUTPUT
else
echo "should_run=false" >> $GITHUB_OUTPUT
fi
shell: bash

- name: Set up Python ${{ matrix.python-version }}
if: steps.should_run.outputs.should_run == 'true'
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: ${{ matrix.python-version == '3.15' }}

- name: Install dependencies
if: steps.should_run.outputs.should_run == 'true'
run: |
python -m pip install --upgrade pip
pip install .[test,gui]

- name: Run All Python Tests
if: steps.should_run.outputs.should_run == 'true'
timeout-minutes: 20
run: |
pytest tests/ -v --tb=short
env:
PYTHONPATH: ${{ github.workspace }}/src
CI: true
GITHUB_ACTIONS: true

test-cli-core:
runs-on: windows-latest
Expand All @@ -45,14 +66,15 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"

- name: Install Core (No GUI)
run: |
python -m pip install --upgrade pip
pip install .[test]

- name: Run CLI Dynamic Tests
timeout-minutes: 10
run: |
pytest tests/test_cli_dynamic.py tests/test_cli_core.py -v
env:
Expand Down
1 change: 1 addition & 0 deletions data/stacks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
3 changes: 3 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export default defineConfig({

vite: {
publicDir: 'public',
server: {
cors: false
},
build: {
rollupOptions: {
output: {
Expand Down
Loading
Loading