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
15 changes: 0 additions & 15 deletions .github/workflows/lint.yml

This file was deleted.

165 changes: 165 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: python-ci

# Tiered triggering keeps PR feedback fast while still doing a full
# cross-platform sweep on every master push, weekly, and on demand.
# Feature-branch pushes get the fast tier (same as PRs) so contributors
# see CI feedback before opening a PR.
on:
push:
pull_request:
workflow_dispatch:
schedule:
# Monday 06:00 UTC: catches drift on runner image / Python rolling
# bugfix releases without humans needing to remember to push.
- cron: '0 6 * * 1'

# Default to read-only for every job; neither static analysis nor pytest
# needs anything more.
permissions:
contents: read

# Use bash for every run: step so that single-quoted pip extras work
# consistently on all platforms (PowerShell on Windows handles them
# differently in some edge cases) and inside Linux containers.
defaults:
run:
shell: bash

jobs:
# -------------------------------------------------------------------------
# Static analysis (ruff + mypy + bandit). One job, fast, fails fast.
# -------------------------------------------------------------------------
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: install lint extras
run: |
python -m pip install --upgrade pip
python -m pip install -e '.[lint]'
- name: ruff
run: ruff check .
- name: mypy
run: mypy
- name: bandit
run: bandit -r src/keychain -c pyproject.toml

# -------------------------------------------------------------------------
# Fast tier — runs on every event (PR, push, cron, dispatch).
#
# Floor + ceiling on Linux + Windows-on-ceiling. Rationale:
# - 3.9 (floor) catches usage of features added later.
# - 3.13 (ceiling) catches deprecations and stdlib drift.
# - Windows × 3.13 catches POSIX-only assumptions; OS-portability
# bugs almost never differ between Python minor versions, so one
# Windows job is enough for PR-time feedback.
# - macOS is intentionally omitted from this tier because it's a
# POSIX system whose failures correlate ~100% with Linux. The
# full sweep below restores macOS coverage on master.
# -------------------------------------------------------------------------
test-fast:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python-version: "3.9"
- os: ubuntu-latest
python-version: "3.13"
- os: windows-latest
python-version: "3.13"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: install
run: |
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
- name: pytest
run: python -m pytest -v --tb=short --cov=src/keychain --cov-branch --cov-report=term-missing:skip-covered

# -------------------------------------------------------------------------
# Production-target authenticity: Rocky Linux 8 + python3.9 module.
#
# This is the documented install path for RHEL 8 / Rocky 8 users
# (see docs/python-version-modernization.md). Validating it on every
# commit keeps three risks contained:
# - The 3.9-floor guard in src/keychain/__init__.py wiring.
# - The polyglot sh/python multi-version probing shebang baked
# into keychain.pyz by the Makefile.
# - Subtle stdlib differences between AppStream's python3.9 build
# and the upstream cpython binary that setup-python ships.
# -------------------------------------------------------------------------
test-rocky8-py39:
runs-on: ubuntu-latest
container:
image: rockylinux:8
steps:
- name: install python3.9 module + build prerequisites
# git: needed by actions/checkout
# python39: AppStream's RHEL-shipped 3.9 -- our actual target
# gcc + python39-devel: allow C-extension wheels (psutil) to
# build from source if no manylinux wheel matches; not
# strictly required today (no C deps) but cheap insurance
# against transitive deps growing one later.
# make: builds keychain.pyz for the smoke test below.
run: |
dnf install -y git make gcc
dnf module install -y python39
dnf install -y python39-devel
- uses: actions/checkout@v4
- name: python version
run: python3.9 --version
- name: install package
run: |
python3.9 -m pip install --upgrade pip
python3.9 -m pip install -e '.[dev]'
- name: pytest
run: python3.9 -m pytest -v --tb=short --cov=src/keychain --cov-branch --cov-report=term-missing:skip-covered
- name: build polyglot zipapp & smoke-test shebang resolution
# /usr/bin/python3 on Rocky 8 is 3.6.8; the shebang must still
# find python3.9 via the polyglot probe. If the probe regresses
# to /usr/bin/env python3, the version-floor guard fires and
# this step exits 2.
run: |
make keychain.pyz
./keychain.pyz version
./keychain.pyz help >/dev/null

# -------------------------------------------------------------------------
# Full sweep — runs on master pushes, tags, weekly cron, and manual
# dispatch. *Skipped on PRs and feature-branch pushes* to keep PR and
# day-to-day feedback under a few minutes. If a branch needs the full
# sweep before merge (e.g. touching subprocess wiring, signal handling,
# path resolution), a maintainer can trigger it via the Actions tab →
# "Run workflow".
# -------------------------------------------------------------------------
test-full-sweep:
if: >-
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/master') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: install
run: |
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
- name: pytest
run: python -m pytest -v --tb=short --cov=src/keychain --cov-branch --cov-report=term-missing:skip-covered
132 changes: 89 additions & 43 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,108 @@ name: release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- "[0-9]*.[0-9]*.[0-9]*"

permissions:
contents: write

defaults:
run:
shell: bash

jobs:
build:
runs-on: ubuntu-latest
container:
image: debian:bookworm-slim
release:
runs-on: ubuntu-24.04
steps:
- name: Prepare build dependencies
run: |
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates make perl jq git gawk sed gzip tar openssh-client
rm -rf /var/lib/apt/lists/*
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fix git ownership for container
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Verify tag/version consistency
id: ver

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Verify tag matches VERSION
id: version
run: |
TAG_NAME="${GITHUB_REF##*/}"
FILE_VER=$(cat VERSION)
if [ "$TAG_NAME" != "$FILE_VER" ]; then
echo "Tag $TAG_NAME does not match VERSION file $FILE_VER" >&2
tag="${GITHUB_REF_NAME}"
version="$(cat VERSION)"
if [[ "${tag}" != "${version}" ]]; then
echo "Tag ${tag} does not match VERSION ${version}" >&2
exit 1
fi
echo "version=$TAG_NAME" >> $GITHUB_OUTPUT
- name: Build
if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(_[A-Za-z0-9]+)?$ ]]; then
echo "Unsupported VERSION format: ${version}" >&2
exit 1
fi
prerelease=false
if [[ "${version}" == *"_"* ]]; then
prerelease=true
fi
title="${version//_/ }"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "title=${title}" >> "${GITHUB_OUTPUT}"
echo "prerelease=${prerelease}" >> "${GITHUB_OUTPUT}"

- name: Install test dependencies
run: |
make clean
make dist/keychain-$(cat VERSION).tar.gz
- name: Test space-in-home handling
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'

- name: Test
run: python -m pytest tests -q

- name: Build release artifacts
run: |
ver=$(cat VERSION)
./scripts/test-space-home.sh "$ver"
- name: Extract changelog section
make clean
make release-artifacts
./dist/keychain-${{ steps.version.outputs.version }}.pyz version 2>&1 | tee .version-smoke.txt
grep -F "keychain ${{ steps.version.outputs.version }}" .version-smoke.txt
./dist/keychain-${{ steps.version.outputs.version }}.pyz man --list >/dev/null

- name: Prepare release notes
run: |
ver=$(cat VERSION)
awk -v ver="$ver" '/^## keychain '"$ver"' /{f=1;print;next} /^## keychain / && f && $0 !~ ver {exit} f' ChangeLog.md > .release-notes.md
if [ ! -s .release-notes.md ]; then
echo "Failed to extract changelog for $ver" >&2
version="${{ steps.version.outputs.version }}"
awk -v ver="${version}" '
$0 == "## " ver {f=1; next}
/^## / && f {exit}
f {print}
' ChangeLog.md > .release-notes.md
if [[ ! -s .release-notes.md ]]; then
echo "Failed to extract ChangeLog.md notes for ${version}" >&2
exit 1
fi
- name: Upload artifacts (build only; manual publish step remains maintainer-driven)

- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: keychain-${{ steps.ver.outputs.version }}-artifacts
path: |
dist/keychain-${{ steps.ver.outputs.version }}.tar.gz
keychain
keychain.1
.release-notes.md
- name: Summary
name: keychain-${{ steps.version.outputs.version }}
path: |
dist/keychain-${{ steps.version.outputs.version }}.pyz
dist/SHA256SUMS
.release-notes.md

- name: Create draft GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
echo 'Artifacts prepared. Use make release or release-refresh locally to publish via API if desired.' >> $GITHUB_STEP_SUMMARY
version="${{ steps.version.outputs.version }}"
if gh release view "${version}" >/dev/null 2>&1; then
gh release upload "${version}" \
"dist/keychain-${version}.pyz" \
dist/SHA256SUMS \
--clobber
else
args=(
--draft
--title "Keychain ${{ steps.version.outputs.title }}"
--notes-file .release-notes.md
)
if [[ "${{ steps.version.outputs.prerelease }}" == "true" ]]; then
args+=(--prerelease)
fi
gh release create "${version}" \
"dist/keychain-${version}.pyz" \
dist/SHA256SUMS \
"${args[@]}"
fi
18 changes: 14 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
/keychain.1.orig
.idea/
keychain.iml
keychain
keychain.1
keychain.txt
keychain.spec
/keychain
/keychain.1
/keychain.txt
/lint_out.txt
/pytest_out.txt
/keychain.spec
/src/keychain/docs/_doc_texts.json
__pycache__/
*.pyc
.specstory/
.ci-artifacts*/
dist/
build/
*.egg-info/
*.pyz
*~
.coverage
22 changes: 22 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Pre-commit hooks. Install with ``pre-commit install`` (provided by the
# ``pip install -e .[lint]`` extra). Hooks run on staged files at commit
# time and in CI via the ``pre-commit/action`` GitHub Action.
#
# ruff-format auto-reformats (no --check; apply directly).
# ruff auto-fixes lint issues.
# Both run before the hygiene hooks so fixes are in place before those checks.
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
6 changes: 0 additions & 6 deletions .vscode/settings.json

This file was deleted.

Loading
Loading