Add Apple Silicon modernization implementation plan

12 tasks covering Phase 1 (script rewrite, --no-binary removal,
flavor deletion, README, local validation on 3.13.13 and 3.14.5)
and Phase 2 (patch bumps, 3.14 enablement, 3.9/3.10 final-release
notes, pip pin sweep, Dependabot, manual release dispatches).

Phase 3 (CI consolidation, runner migration, action bumps) is
out of scope; Task 7 includes a stopgap so existing workflows keep
calling the refactored script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik Gomez
2026-05-12 12:21:26 -05:00
co-authored by Claude Opus 4.7
parent b060a58130
commit 31813eef39
@@ -0,0 +1,849 @@
# Apple Silicon Modernization Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Refactor `build_python_framework_pkgs.zsh` to run natively on Apple Silicon without universal2 enforcement, trim to a single build flavor, bump upstream SHAs and Python interpreter versions, add Python 3.14, and cut a final release for the EOL 3.9 and 3.10 branches.
**Architecture:** The build script is reorganized into named functions (`parse_args`, `prepare_build_dirs`, `download_tool`, `build_framework`, `codesign_framework`, `build_pkg`, `notarize_and_staple`, `zip_framework`, `cleanup`) with `set -eu` short-circuiting on failure. Long-flag arguments replace positional ones, with the major Python version derived from the full version. CI-only steps (Homebrew nuke, `xcode-select`) are gated behind `$CI` / `$GITHUB_ACTIONS`. The universal2 dylib/so audit is deleted; arm64 wheels from PyPI are used directly.
**Tech Stack:** zsh, `relocatable-python`, `munki-pkg`, `codesign`, `notarytool`, GitHub Actions, Python 3.9 3.14.
---
## File Structure
**Modified files:**
- `build_python_framework_pkgs.zsh` — full rewrite into functions; drops universal check, ad-hoc/signed codesign duplication, per-version symlink branches, and CI-environment hardcodes. Bumps `RP_SHA` and `MP_SHA`.
- `requirements_recommended.txt` — drop `--no-binary` directives for `black`, `cffi`, `charset-normalizer`, `PyYAML`, `tomli`, `xattr`. Bump package pins where newer versions have arm64 wheels across all supported branches.
- `README.md` — drop "Flavors of Python", "Minimal", "No Customization", and "build on Intel macOS" sections; update examples for Apple Silicon.
- `.github/workflows/build_python_3.11.yml` — bump `PYTHON_VERSION` to `3.11.9`.
- `.github/workflows/build_python_3.12.yml` — bump `PYTHON_VERSION` to `3.12.10`.
- `.github/workflows/build_python_3.13.yml` — bump `PYTHON_VERSION` to `3.13.13`.
- `.github/workflows/build_python_3.9.yml` — augment release notes to mark final release; leave Python version at `3.9.13`.
- `.github/workflows/build_python_3.10.yml` — augment release notes to mark final release; leave Python version at `3.10.11`.
**Created files:**
- `.github/workflows/build_python_3.14.yml` — new workflow for Python 3.14.5 (cloned and adapted from 3.13).
- `.github/dependabot.yml` — Dependabot config for GitHub Actions only (no pip).
**Deleted files:**
- `requirements_minimal.txt`
- `requirements_no_customization.txt`
- `requirement_files/requirements_minimal.txt`
- `requirement_files/requirements_opinionated.txt`
- `build_all_python_frameworks.zsh`
---
## Task 1: Rewrite `build_python_framework_pkgs.zsh`
**Files:**
- Modify: `build_python_framework_pkgs.zsh` (full rewrite)
- [ ] **Step 1: Replace the entire file contents**
Replace the file with this content verbatim:
```zsh
#!/bin/zsh
#
# Build the macadmins Python 3 framework.
# Produces an installable .pkg (when signing identities are supplied) and a
# portable framework zip targeting Apple Silicon.
#
# Adapted from https://github.com/munki/munki/blob/Munki3dev/code/tools/build_python_framework.sh
set -eu
# --- Pinned upstream commits ---
RP_SHA="8ee72fe3a5dbef733365370ebf44f25022b895ef" # gregneagle/relocatable-python
MP_SHA="bbd07730d1b93ed3828246575ef5676bba74b5d1" # munki/munki-pkg
# --- Paths and constants ---
TYPE="recommended"
FRAMEWORKDIR="/Library/ManagedFrameworks/Python"
PYTHON_BIN_NEW="$FRAMEWORKDIR/Python3.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python"
PYTHON_BASEURL="https://www.python.org/ftp/python/%s/python-%s-macos11.pkg"
TOOLSDIR="$(/usr/bin/dirname "$0")"
OUTPUTSDIR="$TOOLSDIR/outputs"
RP_BINDIR="/tmp/relocatable-python"
MP_BINDIR="/tmp/munki-pkg"
RP_ZIP="/tmp/relocatable-python.zip"
MP_ZIP="/tmp/munki-pkg.zip"
CONSOLEUSER="$(/usr/bin/stat -f "%Su" /dev/console)"
PIPCACHEDIR="/Users/${CONSOLEUSER}/Library/Caches/pip"
# --- CLI arguments (set by parse_args) ---
PYTHON_VERSION=""
INSTALLER_ID=""
APPLICATION_ID=""
NOTARY_PASSWORD=""
XCODE_PATH=""
usage() {
cat <<EOF
Usage: $(/usr/bin/basename "$0") --python-version X.Y.Z [options]
Required:
--python-version Full Python version, e.g. 3.13.13
Optional (omit for an unsigned local build):
--installer-id Developer ID Installer identity
--application-id Developer ID Application identity
--notary-password App-specific password for notarytool
--xcode-path Path to Xcode.app (CI only)
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--python-version) PYTHON_VERSION="$2"; shift 2 ;;
--installer-id) INSTALLER_ID="$2"; shift 2 ;;
--application-id) APPLICATION_ID="$2"; shift 2 ;;
--notary-password) NOTARY_PASSWORD="$2"; shift 2 ;;
--xcode-path) XCODE_PATH="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$PYTHON_VERSION" ]]; then
echo "error: --python-version is required" >&2
usage
exit 1
fi
PYTHON_MAJOR_VERSION="${PYTHON_VERSION%.*}" # 3.13.13 -> 3.13
PYTHON_BIN_VERSION="$PYTHON_MAJOR_VERSION"
}
is_ci() {
[[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]
}
derive_build_version() {
local rev_count
rev_count="$(/usr/bin/git -C "$TOOLSDIR" rev-list --count HEAD)"
NEWSUBBUILD=$((80620 + rev_count))
AUTOMATED_PYTHON_BUILD="$PYTHON_VERSION.$NEWSUBBUILD"
echo "$AUTOMATED_PYTHON_BUILD" > "$TOOLSDIR/build_info.txt"
echo "Build version: $AUTOMATED_PYTHON_BUILD"
}
prepare_ci_env() {
if ! is_ci; then
return
fi
echo "CI detected — clearing Homebrew and selecting Xcode."
/usr/local/bin/brew remove --force "$(/usr/local/bin/brew list)" || true
if [[ -n "$XCODE_PATH" && -d "$XCODE_PATH" ]]; then
/usr/bin/sudo /usr/bin/xcode-select -s "$XCODE_PATH"
fi
}
prepare_build_dirs() {
/usr/bin/sudo /bin/mkdir -m 777 -p "$FRAMEWORKDIR"
if [[ -d "$FRAMEWORKDIR/Python.framework" ]]; then
/usr/bin/sudo /bin/rm -rf "$FRAMEWORKDIR/Python.framework"
fi
if [[ -d "$PIPCACHEDIR" ]]; then
echo "Removing pip cache to reduce build errors"
/usr/bin/sudo /bin/rm -rf "$PIPCACHEDIR"
fi
/bin/rm -rf "$TOOLSDIR/$TYPE"
/bin/mkdir -p "$TOOLSDIR/$TYPE/scripts"
/bin/mkdir -p "$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}"
/bin/mkdir -p "$TOOLSDIR/$TYPE/payload/usr/local/bin"
/usr/bin/sudo /usr/sbin/chown -R "${CONSOLEUSER}":wheel "$TOOLSDIR/$TYPE"
/bin/ln -s "$PYTHON_BIN_NEW" "$TOOLSDIR/$TYPE/payload/usr/local/bin/managed_python3"
}
download_tool() {
local name="$1" sha="$2" url="$3" zip_path="$4" dest="$5"
echo "Downloading $name @ $sha"
/bin/rm -rf "$zip_path" "$dest"
/usr/bin/curl -fL "$url" -o "$zip_path"
/usr/bin/unzip -q "$zip_path" -d "$dest"
}
build_framework() {
export C_INCLUDE_PATH="/Library/ManagedFrameworks/Python/Python.framework/Versions/Current/Headers/"
local rp_extract="${RP_BINDIR}/relocatable-python-${RP_SHA}"
"${rp_extract}/make_relocatable_python_framework.py" \
--baseurl "${PYTHON_BASEURL}" \
--python-version "${PYTHON_VERSION}" \
--os-version 11 \
--upgrade-pip \
--no-unsign \
--pip-requirements "${TOOLSDIR}/requirements_${TYPE}.txt" \
--destination "${FRAMEWORKDIR}"
/bin/mv "${FRAMEWORKDIR}/Python.framework" \
"$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/Python3.framework"
}
codesign_framework() {
local identity="${APPLICATION_ID:--}" # `-` means ad-hoc
local framework_root="$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/Python3.framework"
local versioned="$framework_root/Versions/${PYTHON_BIN_VERSION}"
if [[ "$identity" == "-" ]]; then
echo "Ad-hoc signing framework"
else
echo "Signing framework with identity: $identity"
fi
local -a cs_args
if [[ "$identity" == "-" ]]; then
cs_args=(--preserve-metadata=identifier,entitlements,flags,runtime -f)
else
cs_args=(--timestamp --preserve-metadata=identifier,entitlements,flags,runtime -f)
fi
/usr/bin/find "$versioned/bin" -type f -perm -u=x -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/find "$versioned/lib" -type f -perm -u=x -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/find "$versioned/lib" -type f -name "*dylib" -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/codesign -s "$identity" --deep "${cs_args[@]}" "$versioned/Resources/Python.app"
/usr/bin/codesign -s "$identity" "${cs_args[@]}" "$versioned/Python"
/usr/bin/codesign -s "$identity" "${cs_args[@]}" "$framework_root/Versions/Current/Python"
/usr/sbin/spctl -a -vvvv "$versioned/Python" || true
}
build_pkg() {
/bin/mkdir -p "$OUTPUTSDIR"
/bin/cp "${TOOLSDIR}/preinstall-cleanup" "$TOOLSDIR/$TYPE/scripts/preinstall"
if [[ -z "$INSTALLER_ID" ]]; then
echo "No installer identity provided; skipping signed pkg"
return
fi
/bin/cat <<JSON > "$TOOLSDIR/$TYPE/build-info.json"
{
"ownership": "recommended",
"suppress_bundle_relocation": true,
"identifier": "io.macadmins.python.$TYPE",
"postinstall_action": "none",
"distribution_style": true,
"version": "$AUTOMATED_PYTHON_BUILD",
"name": "python_${TYPE}_signed-$AUTOMATED_PYTHON_BUILD.pkg",
"install_location": "/",
"preserve_xattr": true,
"signing_info": {
"identity": "$INSTALLER_ID",
"timestamp": true
}
}
JSON
"${MP_BINDIR}/munki-pkg-${MP_SHA}/munkipkg" "$TOOLSDIR/$TYPE"
}
notarize_and_staple() {
if [[ -z "$NOTARY_PASSWORD" || -z "$INSTALLER_ID" ]]; then
echo "Skipping notarization (no notary password or installer id)"
return
fi
local xcode_dev xcode_notary xcode_stapler pkg
xcode_dev="$(/usr/bin/xcode-select -p)"
xcode_notary="$xcode_dev/usr/bin/notarytool"
xcode_stapler="$xcode_dev/usr/bin/stapler"
pkg="$TOOLSDIR/$TYPE/build/python_${TYPE}_signed-$AUTOMATED_PYTHON_BUILD.pkg"
"$xcode_notary" store-credentials \
--apple-id "opensource@macadmins.io" \
--team-id "T4SK8ZXCXG" \
--password "$NOTARY_PASSWORD" \
macadminpython
"$xcode_notary" submit "$pkg" --keychain-profile macadminpython --wait
"$xcode_stapler" staple "$pkg"
/bin/mv "$pkg" "$OUTPUTSDIR"
}
zip_framework() {
local zipfile="Python3.framework_$TYPE-$AUTOMATED_PYTHON_BUILD.zip"
/usr/bin/ditto -c -k --sequesterRsrc \
"$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/" "$zipfile"
/bin/mv "$zipfile" "$OUTPUTSDIR"
/usr/bin/sudo /usr/sbin/chown -R "${CONSOLEUSER}":wheel "$OUTPUTSDIR"
}
cleanup() {
/usr/bin/sudo /bin/rm -rf "$TOOLSDIR/$TYPE"
/usr/bin/sudo /bin/rm -rf "$FRAMEWORKDIR"
}
# --- Main ---
parse_args "$@"
echo "Building Python framework — $PYTHON_VERSION"
prepare_ci_env
derive_build_version
prepare_build_dirs
download_tool relocatable-python "$RP_SHA" \
"https://github.com/gregneagle/relocatable-python/archive/${RP_SHA}.zip" \
"$RP_ZIP" "$RP_BINDIR"
download_tool munki-pkg "$MP_SHA" \
"https://github.com/munki/munki-pkg/archive/${MP_SHA}.zip" \
"$MP_ZIP" "$MP_BINDIR"
build_framework
codesign_framework
build_pkg
notarize_and_staple
zip_framework
cleanup
echo "Done."
```
- [ ] **Step 2: Lint with shellcheck (best-effort)**
Run: `shellcheck -s bash build_python_framework_pkgs.zsh || true`
Expected: any issues are warnings about zsh-only constructs (e.g., `[[ ]]`); resolve real bugs only. shellcheck has limited zsh support — informational only.
- [ ] **Step 3: Verify executable bit**
Run: `ls -l build_python_framework_pkgs.zsh`
Expected: shows `-rwxr-xr-x` (mode 755). If not, run `chmod +x build_python_framework_pkgs.zsh`.
- [ ] **Step 4: Commit**
```bash
git add build_python_framework_pkgs.zsh
git commit -m "Refactor build script for Apple Silicon
- Drop universal2 enforcement; arm64 wheels used directly
- Single 'recommended' flavor; remove minimal/no_customization branches
- Long-flag arguments; derive major version from full version
- Functions: parse_args, prepare_build_dirs, download_tool,
build_framework, codesign_framework, build_pkg,
notarize_and_staple, zip_framework, cleanup
- Collapse signed/ad-hoc codesign duplication; fixes latent path bug
- Bump relocatable-python and munki-pkg SHAs
- Gate CI-only steps (brew remove, xcode-select) on \$CI"
```
---
## Task 2: Drop `--no-binary` markers from `requirements_recommended.txt`
**Files:**
- Modify: `requirements_recommended.txt`
- [ ] **Step 1: Remove all `--no-binary` lines**
Replace the file with:
```
asn1crypto==1.5.1
aspy.yaml==1.3.0
attrs==25.3.0
black==25.1.0
certifi==2025.6.15
cffi==1.17.1
cfgv==3.4.0
charset-normalizer==3.4.2
click==8.2.1
distlib==0.3.9
docklib==2.0.0
entrypoints==0.4
filelock==3.18.0
flake8==7.3.0
flake8-bugbear==24.12.12
identify==2.6.12
idna==3.10
isort==6.0.1
mccabe==0.7.0
mypy-extensions==1.1.0
nodeenv==1.9.1
packaging==25.0
pathspec==0.12.1
platformdirs==4.3.8
pre-commit==4.2.0
pycodestyle==2.14.0
pycparser==2.22
pyflakes==3.4.0
pyobjc==11.1
PyYAML==6.0.2
requests==2.32.4
six==1.17.0
tokenize-rt==6.2.0
tomli==2.2.1
urllib3==2.5.0
virtualenv==20.31.2
xattr==1.1.4
```
- [ ] **Step 2: Verify no `--no-binary` remains**
Run: `grep -n -- '--no-binary' requirements_recommended.txt`
Expected: no output (exit code 1 — no matches).
- [ ] **Step 3: Commit**
```bash
git add requirements_recommended.txt
git commit -m "Drop --no-binary directives (use prebuilt arm64 wheels)"
```
---
## Task 3: Delete obsolete build flavors
**Files:**
- Delete: `requirements_minimal.txt`
- Delete: `requirements_no_customization.txt`
- Delete: `requirement_files/requirements_minimal.txt`
- Delete: `requirement_files/requirements_opinionated.txt`
- Delete: `build_all_python_frameworks.zsh`
- [ ] **Step 1: Delete files**
Run:
```bash
git rm requirements_minimal.txt requirements_no_customization.txt \
requirement_files/requirements_minimal.txt \
requirement_files/requirements_opinionated.txt \
build_all_python_frameworks.zsh
```
Expected: 5 files removed (`rm 'requirements_minimal.txt'`, etc.).
- [ ] **Step 2: Verify no remaining references**
Run: `grep -rn 'minimal\|no_customization\|build_all_python_frameworks' --include='*.zsh' --include='*.yml' --include='*.md' .`
Expected: only matches in the design spec (`docs/superpowers/specs/…`) and the deletion-context release notes. If any active script or workflow still references them, fix that reference now.
- [ ] **Step 3: Commit**
```bash
git commit -m "Remove minimal and no_customization build flavors"
```
---
## Task 4: Update README.md
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Replace the file contents**
Replace `README.md` with:
```markdown
# python
A Python 3 framework that installs to `/Library/ManagedFrameworks/Python/Python3.framework`.
Please see Apple's documentation on [file system basics](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) for context.
This is an intended replacement for `/usr/bin/python`, which Apple removed in macOS 12.3 (Spring 2022).
## Apple Silicon Only
Builds and packages target Apple Silicon (arm64). Universal2 outputs are no longer produced. Build hosts and target machines must be Apple Silicon Macs.
## Why use this instead of a package from python.org?
- Ships with PyObjC and other modules useful for Mac admins, similar in spirit to the Apple Python it replaces
- Installs to a location less likely to be overwritten, removed, or modified by other Python installations
## Using interactively
After installing the package, `/usr/local/bin/managed_python3` is a symlink to `/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python`.
## Using with scripts
Point your shebang directly at the symlink:
```
#!/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3
print('This is an example script.')
```
### zshenv global alias
For zsh scripts you can add a global alias to `/etc/zshenv`:
`alias -g python3.framework='/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3'`
See Armin Briegel's "Moving to Zsh" Part [II](https://scriptingosx.com/2019/06/moving-to-zsh-part-2-configuration-files/) and [IV](https://scriptingosx.com/2019/07/moving-to-zsh-part-4-aliases-and-functions/).
## Notes
Only a single package may be installed at any given time. The preinstall script removes any previous framework.
### Upgrades
Python itself has its own release cadence; this package will see additional updates as 3rd-party libraries release fixes and security updates. Always test your scripts before deploying broadly.
### Downgrades
Not supported.
### pip
`pip` is bundled but **not recommended** for installing external libraries into the framework. Use a [virtual environment](https://docs.python.org/3/library/venv.html) or a tool like [pyenv](https://github.com/pyenv/pyenv) instead. Pull requests to the `recommended` requirements file are welcome.
# Building locally
Build an unsigned framework on Apple Silicon with:
```
./build_python_framework_pkgs.zsh --python-version 3.13.13
```
Pass `--installer-id`, `--application-id`, and `--notary-password` to produce a signed and notarized `.pkg`.
# Updating packages
Do this in a clean virtual environment. After every Python package install, run `pip freeze | xargs pip uninstall -y` to reset the environment.
# CI Job
To update the signing certificate, run `base64 -i /path/to/certificate.p12 -o base64string` and import it into the GitHub Actions secrets store along with the matching password.
# Credits
Built on two open-source tools by [Greg Neagle](https://www.linkedin.com/in/gregneagle/):
- [relocatable-python](https://github.com/gregneagle/relocatable-python)
- [munki-pkg](https://github.com/munki/munki-pkg)
```
- [ ] **Step 2: Verify no flavor references remain**
Run: `grep -E 'Minimal|No Customization|Flavors of Python|Intel macOS device' README.md`
Expected: no output.
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "Update README for Apple Silicon, single-flavor build"
```
---
## Task 5: Local validation — unsigned 3.13.13 build
**Files:** none (validation only)
- [ ] **Step 1: Run unsigned build**
Run: `./build_python_framework_pkgs.zsh --python-version 3.13.13`
Expected:
- Final lines include `Build version: 3.13.13.<N>` and `Done.`
- `outputs/Python3.framework_recommended-3.13.13.<N>.zip` exists.
- No `outputs/*.pkg` (no installer identity passed).
- No error about `2 architectures` (the validation block is gone).
- [ ] **Step 2: Install the framework and smoke-test**
Run:
```bash
sudo rm -rf /Library/ManagedFrameworks/Python/Python3.framework
sudo mkdir -p /Library/ManagedFrameworks/Python
sudo ditto -x -k outputs/Python3.framework_recommended-3.13.13.*.zip /Library/ManagedFrameworks/Python/
sudo ln -sf /Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python /usr/local/bin/managed_python3
managed_python3 --version
managed_python3 -c "import platform; print(platform.machine())"
managed_python3 -c "import objc, xattr, requests, yaml; print('ok')"
```
Expected output:
- `Python 3.13.13`
- `arm64`
- `ok`
If `import objc` fails with `Symbol not found` or an architecture mismatch, halt and investigate — the framework is not arm64 compatible.
- [ ] **Step 3: Tear down the test install**
Run: `sudo rm -rf /Library/ManagedFrameworks/Python/Python3.framework /usr/local/bin/managed_python3`
Expected: no output.
- [ ] **Step 4: Record the validation in the spec / plan**
No commit yet — validation is a checkpoint. If steps 1 and 2 passed, mark this task done and move on.
---
## Task 6: Local validation — unsigned 3.14.5 build
**Files:** none (validation only)
- [ ] **Step 1: Run unsigned build**
Run: `./build_python_framework_pkgs.zsh --python-version 3.14.5`
Expected: `Done.` and `outputs/Python3.framework_recommended-3.14.5.<N>.zip` exists.
- [ ] **Step 2: Smoke-test**
Run the same install + smoke-test commands from Task 5 Step 2, substituting `3.14.5` for `3.13.13` in the zip filename.
Expected:
- `Python 3.14.5`
- `arm64`
- `ok`
If any pip package fails to install during step 1 (no arm64 wheel for 3.14), record which package failed and proceed to Task 10 (pin sweep) to address.
- [ ] **Step 3: Tear down the test install**
Run: `sudo rm -rf /Library/ManagedFrameworks/Python/Python3.framework /usr/local/bin/managed_python3`
---
## Task 7: Bump Python patch versions in 3.11 / 3.12 / 3.13 workflows
**Files:**
- Modify: `.github/workflows/build_python_3.11.yml`
- Modify: `.github/workflows/build_python_3.12.yml`
- Modify: `.github/workflows/build_python_3.13.yml`
- [ ] **Step 1: Bump 3.11**
In `.github/workflows/build_python_3.11.yml`, change `PYTHON_VERSION: "3.11.7"` to `PYTHON_VERSION: "3.11.9"`. Also update the release-notes line `- Upgraded Python to 3.11.7` to `- Upgraded Python to 3.11.9`.
- [ ] **Step 2: Bump 3.12**
In `.github/workflows/build_python_3.12.yml`, change `PYTHON_VERSION: "3.12.1"` to `PYTHON_VERSION: "3.12.10"`. Also update the release-notes line `- Upgraded Python to 3.12.1` to `- Upgraded Python to 3.12.10`.
- [ ] **Step 3: Bump 3.13**
In `.github/workflows/build_python_3.13.yml`, change `PYTHON_VERSION: "3.13.5"` to `PYTHON_VERSION: "3.13.13"`. Also update the release-notes line `- Upgraded Python to 3.13.5` to `- Upgraded Python to 3.13.13`.
- [ ] **Step 4: Verify the script argument call still works**
Search each workflow for the `Run build package script` step. Today it reads:
```yaml
run: ./build_python_framework_pkgs.zsh "$TYPE" "$DEV_INSTALLER_ID" "$DEV_APPLICATION_ID" "$PYTHON_VERSION" "$PYTHON_MAJOR_VERSION" "${NOTARY_APP_PASSWORD}"
```
After Task 1 the script no longer accepts positional arguments, so this **will break in CI**. Phase 3 will rewrite the workflows, but to keep CI green for the in-between window, update the call in each of the three modified workflows to:
```yaml
run: |
./build_python_framework_pkgs.zsh \
--python-version "$PYTHON_VERSION" \
--installer-id "$DEV_INSTALLER_ID" \
--application-id "$DEV_APPLICATION_ID" \
--notary-password "$NOTARY_APP_PASSWORD" \
--xcode-path "/Applications/Xcode_15.2.app"
```
The `TYPE` env var is unused by the new script; leave it in the env block for now (Phase 3 cleanup will remove it).
- [ ] **Step 5: Commit**
```bash
git add .github/workflows/build_python_3.11.yml \
.github/workflows/build_python_3.12.yml \
.github/workflows/build_python_3.13.yml
git commit -m "Bump 3.11/3.12/3.13 to latest patches; update script invocation"
```
---
## Task 8: Add Python 3.14 workflow
**Files:**
- Create: `.github/workflows/build_python_3.14.yml`
- [ ] **Step 1: Create the workflow file**
Create `.github/workflows/build_python_3.14.yml` by copying `.github/workflows/build_python_3.13.yml` and changing:
- `name: Build Python 3.13``name: Build Python 3.14`
- `PYTHON_VERSION: "3.13.13"``PYTHON_VERSION: "3.14.5"`
- `PYTHON_MAJOR_VERSION: "3.13"``PYTHON_MAJOR_VERSION: "3.14"`
- `Python 3.13.13 Framework``Python 3.14.5 Framework`
- `- Upgraded Python to 3.13.13``- Upgraded Python to 3.14.5`
All other contents (action versions, build script invocation from Task 7) remain identical.
- [ ] **Step 2: Verify YAML parses**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/build_python_3.14.yml'))"`
Expected: no output (no errors).
- [ ] **Step 3: Commit**
```bash
git add .github/workflows/build_python_3.14.yml
git commit -m "Add Python 3.14.5 build workflow"
```
---
## Task 9: Mark 3.9 and 3.10 workflows as final releases
**Files:**
- Modify: `.github/workflows/build_python_3.9.yml`
- Modify: `.github/workflows/build_python_3.10.yml`
- [ ] **Step 1: Update script invocation in 3.9 workflow**
In `.github/workflows/build_python_3.9.yml`, replace the `Run build package script` step's `run:` line with the same long-flag invocation as Task 7 Step 4 (so the final release works against the new script).
- [ ] **Step 2: Add final-release notice to 3.9 release body**
In `.github/workflows/build_python_3.9.yml`, find the `Create Release` step's `body:` block. Insert a new section immediately after the existing `## Security Notice` paragraph:
```yaml
## Final Release
**This is the final release of the Python 3.9 framework.** Python 3.9 reached end-of-life on October 2025 and python.org has not published a macOS installer past 3.9.13. Future framework updates will target Python 3.11 and newer. Plan your migration.
```
- [ ] **Step 3: Update script invocation in 3.10 workflow**
In `.github/workflows/build_python_3.10.yml`, apply the same `run:` replacement as Step 1.
- [ ] **Step 4: Add final-release notice to 3.10 release body**
In `.github/workflows/build_python_3.10.yml`, insert after the existing `## Security Notice` paragraph:
```yaml
## Final Release
**This is the final release of the Python 3.10 framework.** Python 3.10 is in security-fixes-only status and python.org has not published a macOS installer past 3.10.11. Future framework updates will target Python 3.11 and newer. Plan your migration.
```
- [ ] **Step 5: Commit**
```bash
git add .github/workflows/build_python_3.9.yml .github/workflows/build_python_3.10.yml
git commit -m "Mark 3.9 and 3.10 as final releases; switch to new script flags"
```
---
## Task 10: Python package pin sweep
**Files:**
- Modify: `requirements_recommended.txt` (only if a package needs a bump or per-branch hold)
- [ ] **Step 1: List currently pinned packages with their versions**
Run: `grep -E '^[a-zA-Z]' requirements_recommended.txt`
Expected: 37 lines of `name==version`.
- [ ] **Step 2: For each pinned package, check PyPI for a newer release**
Run for each package (using `pyobjc` as the example; substitute each name):
```bash
pip index versions pyobjc --python-version 3.14 2>&1 | head -5
```
This requires `pip` 23.3+; if not available, use `pip install --dry-run pyobjc==99.99 2>&1 | grep "from versions"` instead.
Expected: a sorted list of available versions. Note the newest.
- [ ] **Step 3: Verify arm64 wheel availability for each candidate bump**
For each package where a newer version exists, check that arm64 macOS wheels are published for **every** supported Python branch (3.9, 3.10, 3.11, 3.12, 3.13, 3.14). Visit `https://pypi.org/project/<name>/#files` in a browser or run:
```bash
curl -s "https://pypi.org/pypi/<name>/<new-version>/json" \
| python3 -c "import json,sys; data=json.load(sys.stdin); files=data['urls']; [print(f['filename']) for f in files if 'macosx' in f['filename'] and 'arm64' in f['filename']]"
```
A package qualifies for an unconditional bump only if arm64 macOS wheels exist for **all** supported Python versions. If 3.9 / 3.10 lack a wheel for the new version, hold those at the older pin via Pip's per-version syntax — example: `pyobjc==11.1; python_version >= "3.11"` plus `pyobjc==10.5.1; python_version < "3.11"`.
- [ ] **Step 4: Update `requirements_recommended.txt` with the bumps**
For each package that has a newer version with full arm64 wheel coverage, update the pin. For packages with partial coverage, use the marker syntax from Step 3. Leave packages without newer versions unchanged.
Document each change with a single-line trailing comment if it is a holdback (`pyobjc==10.5.1; python_version < "3.11" # last version with 3.9/3.10 arm64 wheels`).
- [ ] **Step 5: Re-run local validation for 3.9, 3.10, 3.11, 3.12, 3.13, 3.14**
For each version, run:
```bash
./build_python_framework_pkgs.zsh --python-version <version>
```
Then install and smoke-test as in Task 5 Step 2. Each must produce a working framework. If any version fails on a freshly bumped package, revert that pin or hold it for the affected version.
The patch versions to validate: `3.9.13`, `3.10.11`, `3.11.9`, `3.12.10`, `3.13.13`, `3.14.5`.
- [ ] **Step 6: Commit**
```bash
git add requirements_recommended.txt
git commit -m "Bump Python package pins; add per-branch holdbacks where needed"
```
---
## Task 11: Add Dependabot config
**Files:**
- Create: `.github/dependabot.yml`
- [ ] **Step 1: Create the config**
Create `.github/dependabot.yml` with:
```yaml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
```
- [ ] **Step 2: Verify YAML parses**
Run: `python3 -c "import yaml; yaml.safe_load(open('.github/dependabot.yml'))"`
Expected: no output.
- [ ] **Step 3: Commit**
```bash
git add .github/dependabot.yml
git commit -m "Enable Dependabot for GitHub Actions"
```
---
## Task 12: Cut releases for all supported branches
**Files:** none (manual CI dispatch)
This is the final operational step. Each release is kicked off manually via `workflow_dispatch` so we can stage them and verify outputs.
- [ ] **Step 1: Trigger 3.14 release first**
Run: `gh workflow run build_python_3.14.yml --ref <branch-with-this-plan-merged>`
Expected: a workflow run starts. Watch via `gh run watch` or the GitHub UI.
- [ ] **Step 2: Verify 3.14 release artifact**
When the run finishes:
```bash
gh release view v3.14.5.<NEWSUBBUILD>
```
Expected: the release exists, has a signed `.pkg` asset, and the release body matches the new template.
- [ ] **Step 3: Trigger remaining releases in order**
Repeat Steps 12 for: `build_python_3.13.yml`, `build_python_3.12.yml`, `build_python_3.11.yml`, `build_python_3.10.yml`, `build_python_3.9.yml`.
The 3.9 and 3.10 releases must include the **Final Release** notice in their body (added in Task 9).
- [ ] **Step 4: No commit**
Nothing to commit — these are CI-side actions only. Phase 3 work (archiving the 3.9 / 3.10 workflows, consolidating the rest) starts after this plan is fully complete.
---
## Self-Review Notes
- Phase 1 spec coverage: Tasks 16 cover the script refactor, requirements, deletions, README, and local validation.
- Phase 2 spec coverage: Tasks 712 cover patch bumps, 3.14 addition, final-release notes, pin sweep, Dependabot, and release execution.
- Phase 3 explicitly excluded: workflow consolidation, runner migration, action bumps, release-trigger change — Task 7 Step 4 includes a deliberate stopgap (long-flag invocation inside the still-duplicated workflows) so CI keeps working in the in-between state.
- No placeholders: every code block is concrete; every `Expected:` describes verifiable output.
- Type / name consistency: long-flag names (`--python-version`, `--installer-id`, `--application-id`, `--notary-password`, `--xcode-path`) are identical across Tasks 1, 7, and 9.