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
This commit is contained in:
JerrettDavis 2026-04-15 20:28:29 -05:00
parent f33ec4e269
commit 910796ded2
7 changed files with 127 additions and 31 deletions

14
.actrc Normal file
View file

@ -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

15
.actrc.local.example Normal file
View file

@ -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

15
.env.act.example Normal file
View file

@ -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

5
.github/act/dry-run.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"inputs": {
"dry_run": "true"
}
}

8
.github/act/push-feat.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"ref": "refs/heads/main",
"commits": [
{
"message": "feat: add new release automation"
}
]
}

View file

@ -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

5
.gitignore vendored
View file

@ -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