From 910796ded23298b1492b320e9e385a1e357f2ee1 Mon Sep 17 00:00:00 2001 From: JerrettDavis Date: Wed, 15 Apr 2026 20:28:29 -0500 Subject: [PATCH] feat: add act testing config, fix gitignore, make workflow production-ready - Add .actrc with act defaults (Ubuntu runner, reuse, quiet) - Add .github/act/dry-run.json and push-feat.json for local workflow testing - Add .actrc.local.example for local overrides (gitignored) - Add .env.act.example documenting required secrets - Fix .gitignore: .env.act excluded, .env.act.example allowed - Replace all bash git commands with Python subprocess (act compatibility) - Add pip install build wheel for python -m build step - Use absolute paths for artifact uploads (github.workspace) - All publish jobs: dry_run + *_SKIP vars as safety gates --- .actrc | 14 +++++ .actrc.local.example | 15 ++++++ .env.act.example | 15 ++++++ .github/act/dry-run.json | 5 ++ .github/act/push-feat.json | 8 +++ .github/workflows/release.yml | 96 ++++++++++++++++++++++++----------- .gitignore | 5 ++ 7 files changed, 127 insertions(+), 31 deletions(-) create mode 100644 .actrc create mode 100644 .actrc.local.example create mode 100644 .env.act.example create mode 100644 .github/act/dry-run.json create mode 100644 .github/act/push-feat.json diff --git a/.actrc b/.actrc new file mode 100644 index 000000000..7bd85cc01 --- /dev/null +++ b/.actrc @@ -0,0 +1,14 @@ +# Default flags for act runs in this repo. +# See https://github.com/nektos/act#configuration for all options. + +# Use Ubuntu runner image for all jobs +--platform=ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest + +# Reuse containers so subsequent runs are faster +--reuse + +# Pull the image in the background if not present +--pull=true + +# Quiet mode (less verbose output) +--quiet diff --git a/.actrc.local.example b/.actrc.local.example new file mode 100644 index 000000000..bc6fbe571 --- /dev/null +++ b/.actrc.local.example @@ -0,0 +1,15 @@ +# Example local act configuration. +# Copy this file to .actrc.local and edit for your local environment. +# +# cp .actrc.local.example .actrc.local +# +# .actrc.local is NOT committed — it overrides .actrc for your machine only. + +# Use a specific runner image (uncomment if you have issues with the default) +# --platform=ubuntu-latest=ghcr.io/catthehacker/ubuntu:runner-latest + +# Or use act with a specific container runtime +# --container-args="--runtime=runc" + +# Extra verbose output for debugging (uncomment to see all steps) +# --verbose diff --git a/.env.act.example b/.env.act.example new file mode 100644 index 000000000..4fee86f82 --- /dev/null +++ b/.env.act.example @@ -0,0 +1,15 @@ +# Example .env file for act local testing. +# Copy this to .env.act (which is gitignored) and fill in test values. +# +# cp .env.act.example .env.act +# +# act automatically reads .env and passes as secrets to workflows. +# DO NOT commit .env — it contains real or test tokens. + +# npmjs.org token (use a test token, not a real one) +NPM_TOKEN=test_token_replace_me + +# PyPI trusted publisher (not needed for act, but documented) +# PYPI_TRUSTED_PUBLISHER=... + +#dry_run=true diff --git a/.github/act/dry-run.json b/.github/act/dry-run.json new file mode 100644 index 000000000..e86a7aed7 --- /dev/null +++ b/.github/act/dry-run.json @@ -0,0 +1,5 @@ +{ + "inputs": { + "dry_run": "true" + } +} diff --git a/.github/act/push-feat.json b/.github/act/push-feat.json new file mode 100644 index 000000000..f7ae22ebc --- /dev/null +++ b/.github/act/push-feat.json @@ -0,0 +1,8 @@ +{ + "ref": "refs/heads/main", + "commits": [ + { + "message": "feat: add new release automation" + } + ] +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ad2c3992..6f47e15a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,14 @@ env: # GitHub Package Registry GITHUB_PACKAGES_REGISTRY_URL: https://npm.pkg.github.com + # ─── Safety Gates ────────────────────────────────────────────────────────────── + # Set to 'true' to skip a publish target (e.g., when tokens are not configured). + # In GitHub: repo Settings → Variables → Actions Variables → New repository variable. + # Locally via act: pass -e event.yml or set in .actrc.local (see .actrc.example). + PYPI_SKIP: "false" + NPM_SKIP: "false" + GITHUB_PKG_SKIP: "false" + on: push: branches: [main] @@ -45,26 +53,36 @@ jobs: - name: Detect bump level id: bump - shell: bash run: | - msg=$(git log -1 --format=%s) - body=$(git log -1 --format=%B) - previous_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - echo "commit_message=$msg" >> $GITHUB_OUTPUT - echo "previous_tag=$previous_tag" >> $GITHUB_OUTPUT - if [[ "$msg" =~ ^feat!.* ]] || [[ "$msg" =~ ^feat: && "$body" =~ BREAKING\ CHANGE ]]; then - echo "level=major" >> $GITHUB_OUTPUT - elif [[ "$msg" =~ ^feat: ]]; then - echo "level=minor" >> $GITHUB_OUTPUT - else - echo "level=patch" >> $GITHUB_OUTPUT - fi + python - <<'PYEOF' + import subprocess, os, re, tomllib, sys + + # Get latest commit message and body + msg = subprocess.run(["git", "log", "-1", "--format=%s"], capture_output=True, text=True, check=False).stdout.strip() + body = subprocess.run(["git", "log", "-1", "--format=%B"], capture_output=True, text=True, check=False).stdout.strip() + + # Get previous tag, fall back to version from pyproject.toml + tag_result = subprocess.run(["git", "describe", "--tags", "--abbrev=0"], capture_output=True, text=True) + previous_tag = tag_result.stdout.strip() if tag_result.returncode == 0 else "" + if not previous_tag: + with open("pyproject.toml", "rb") as f: + previous_tag = "v" + tomllib.load(f)["project"]["version"] + + # Determine bump level + is_major = bool(re.search(r'^feat!', msg)) or "BREAKING CHANGE" in body + is_minor = bool(re.search(r'^feat:', msg)) and not is_major + level = "major" if is_major else ("minor" if is_minor else "patch") + + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"commit_message={msg}\n") + f.write(f"previous_tag={previous_tag}\n") + f.write(f"level={level}\n") + PYEOF - name: Compute version id: ver - shell: bash run: | - version=$(python - <<'PYEOF' + python - <<'PYEOF' import os, re, tomllib with open("pyproject.toml", "rb") as f: @@ -84,11 +102,10 @@ jobs: if manual: new_ver = manual - print(new_ver) + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"version={new_ver}\n") + f.write("is_release=true\n") PYEOF - ) - echo "version=$version" >> $GITHUB_OUTPUT - echo "is_release=true" >> $GITHUB_OUTPUT env: LEVEL: ${{ needs.detect-version.outputs.level }} MANUAL_VER: ${{ github.event.inputs.version }} @@ -112,10 +129,28 @@ jobs: - name: Commit version bump run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "chore: bump version to ${{ needs.detect-version.outputs.version }}" + python - <<'PYEOF' + import subprocess, os + + ver = os.environ["VERSION"] + + def run(cmd, **kwargs): + result = subprocess.run(cmd, **kwargs) + if result.returncode != 0: + print(f"WARNING: {' '.join(cmd)} exited {result.returncode}", file=os.sys.stderr) + return result + + run(["git", "config", "user.name", "github-actions[bot]"]) + run(["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"]) + run(["git", "add", "-A"]) + result = run(["git", "commit", "-m", f"chore: bump version to {ver}"], capture_output=True) + if result.returncode == 0: + print(f"Committed version {ver}") + else: + print("No commit needed (dry run or no changes)", file=os.sys.stderr) + PYEOF + env: + VERSION: ${{ needs.detect-version.outputs.version }} - name: Run changelog generation run: | @@ -130,22 +165,21 @@ jobs: uses: actions/upload-artifact@v4 with: name: changelog - path: .changelog.md + path: ${{ github.workspace }}/.changelog.md - name: Build Python package - run: python -m build + run: python -m pip install build wheel && python -m build - name: Upload dist artifact uses: actions/upload-artifact@v4 with: name: dist - path: dist/ + path: ${{ github.workspace }}/dist/ publish-pypi: needs: [build] - if: github.event.inputs.dry_run != 'true' - environment: - name: ${{ env.PYPI_ENVIRONMENT }} + if: github.event.inputs.dry_run != 'true' && vars.PYPI_SKIP != 'true' + environment: pypi # NOTE: environment name must be a literal; update here if the GitHub environment name changes runs-on: ubuntu-latest steps: - name: Download dist artifact @@ -159,7 +193,7 @@ jobs: publish-npm: needs: [build] - if: github.event.inputs.dry_run != 'true' + if: github.event.inputs.dry_run != 'true' && vars.NPM_SKIP != 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -198,7 +232,7 @@ jobs: publish-github-packages: needs: [build] - if: github.event.inputs.dry_run != 'true' + if: github.event.inputs.dry_run != 'true' && vars.GITHUB_PKG_SKIP != 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index a08e63c69..63dcdda42 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ pytest_cache/ # Environments .env .env.* +!.env.act.example .venv env/ venv/ @@ -198,3 +199,7 @@ docs/superpowers/ # Managed platform (separate private repo) headroom-managed/ + +# Local act testing (never commit test tokens) +/.env.act +.actrc.local