diff --git a/.githooks/pre-push b/.githooks/pre-push index 116bd3c..f40292c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -8,18 +8,32 @@ remote="$1" default="$(git symbolic-ref --quiet "refs/remotes/$remote/HEAD" 2>/dev/null | sed "s|^refs/remotes/$remote/||")" || default="" [ -n "$default" ] || default="$(git config init.defaultBranch || echo main)" +# scripts/agent records the commit it last checked in full, with a clean tree. +recorded="$(cat "$(git rev-parse --git-dir)/agent-checked" 2>/dev/null || :)" branches=0 -while read -r _local_ref _local_sha remote_ref _remote_sha; do +unchecked=0 +while read -r _local_ref local_sha remote_ref _remote_sha; do if [ "$remote_ref" = "refs/heads/$default" ] && git ls-remote --exit-code --heads "$remote" "$default" >/dev/null 2>&1; then echo "pre-push: refusing to push to $default; open a pull request (scripts/agent pr)" >&2 exit 1 fi - case "$remote_ref" in refs/heads/*) branches=$((branches + 1)) ;; esac + case "$remote_ref" in + refs/heads/*) + branches=$((branches + 1)) + [ "$local_sha" = "$recorded" ] || unchecked=$((unchecked + 1)) + ;; + esac done # A tag points at a merge commit CI already checked; checking the working tree again would test # whatever happens to be checked out, not what's tagged. [ "$branches" -gt 0 ] || exit 0 +# The same check already passed on exactly this commit; running it twice only doubles the wait. +if [ "$unchecked" -eq 0 ]; then + echo "pre-push: $recorded was checked by scripts/agent" + exit 0 +fi + echo "pre-push: full check" exec scripts/task check diff --git a/scripts/agent b/scripts/agent index 5f444c5..0117d3f 100755 --- a/scripts/agent +++ b/scripts/agent @@ -27,6 +27,15 @@ default_branch() { current_branch() { git rev-parse --abbrev-ref HEAD; } +# The full check, recorded against HEAD when the tree is clean, so the pre-push hook can skip running +# the same check again on exactly the commit being pushed. +check_and_record() { + "$root/scripts/task" check + if [ -z "$(git status --porcelain)" ]; then + git rev-parse HEAD >"$(git rev-parse --git-dir)/agent-checked" + fi +} + cmd="${1:-}" [ $# -gt 0 ] && shift @@ -82,7 +91,7 @@ pr) branch="$(current_branch)" [ "$branch" != "$(default_branch)" ] || die "on the default branch; start a branch with 'scripts/agent start'" echo "==> checking" - "$root/scripts/task" check + check_and_record merged="the pull request for $branch already merged; put new commits on a new branch ('scripts/agent start'), then run 'scripts/agent done' here" state="$(gh pr view "$branch" --json state --jq .state 2>/dev/null || :)" case "$state" in @@ -147,7 +156,7 @@ ship) # Test against what it will land on, not what it branched from. echo "==> $base moved; rebasing and checking again" git rebase --quiet "origin/$base" || die "conflicts with $base; resolve them, then run 'scripts/agent ship' again" - "$root/scripts/task" check + check_and_record # Auto-merge may have landed the pull request while the check ran, deleting the branch. merged() { [ "$(gh pr view "$branch" --json state --jq .state)" = "MERGED" ]; } merged && continue diff --git a/scripts/dev-cert b/scripts/dev-cert index a411eff..65636b8 100755 --- a/scripts/dev-cert +++ b/scripts/dev-cert @@ -1,35 +1,53 @@ #!/bin/sh set -eu -# Creates a self-signed code signing certificate in the login keychain, once, shared by every -# Common Tools app. +# Creates a self-signed code signing certificate in the login keychain, trusted for code signing, +# shared by every Common Tools app. Run it once per Mac; running it again repairs trust and resets +# this app's permission grants. # -# macOS ties permissions such as Accessibility, and login items, to an app's signature. Ad-hoc -# signatures change with every build, so every rebuild loses them. A certificate that stays put -# keeps them. scripts/task build signs with it whenever it's there. +# macOS ties permissions such as Accessibility and Screen Recording, and login items, to an app's +# signature. Ad-hoc signatures change with every build, so every rebuild loses them. A certificate +# that stays put keeps them. scripts/task build signs with it whenever it's valid. name="Common Tools Development" keychain="$HOME/Library/Keychains/login.keychain-db" - -if security find-certificate -c "$name" "$keychain" >/dev/null 2>&1; then - echo "==> '$name' is already in the login keychain; nothing to do" - exit 0 -fi +bundle_id="com.oddurs.commoncommand" +# The privacy permissions this app asks for, as tccutil names them. +permissions="" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -echo "==> generating '$name' (valid ten years)" -# The system LibreSSL writes PKCS#12 files that security import can read; OpenSSL 3 needs -legacy. -/usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ - -subj "/CN=$name" \ - -addext "basicConstraints=critical,CA:false" \ - -addext "keyUsage=critical,digitalSignature" \ - -addext "extendedKeyUsage=critical,codeSigning" \ - -keyout "$work/key.pem" -out "$work/cert.pem" -/usr/bin/openssl pkcs12 -export -inkey "$work/key.pem" -in "$work/cert.pem" \ - -name "$name" -out "$work/identity.p12" -passout pass:common-tools +if security find-certificate -c "$name" "$keychain" >/dev/null 2>&1; then + echo "==> '$name' is already in the login keychain" + security find-certificate -c "$name" -p "$keychain" >"$work/cert.pem" +else + echo "==> generating '$name' (valid ten years)" + # The system LibreSSL writes PKCS#12 files that security import can read; OpenSSL 3 needs -legacy. + /usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ + -subj "/CN=$name" \ + -addext "basicConstraints=critical,CA:false" \ + -addext "keyUsage=critical,digitalSignature" \ + -addext "extendedKeyUsage=critical,codeSigning" \ + -keyout "$work/key.pem" -out "$work/cert.pem" + /usr/bin/openssl pkcs12 -export -inkey "$work/key.pem" -in "$work/cert.pem" \ + -name "$name" -out "$work/identity.p12" -passout pass:common-tools + echo "==> importing into the login keychain, usable by codesign" + security import "$work/identity.p12" -k "$keychain" -P common-tools -T /usr/bin/codesign >/dev/null +fi + +# An untrusted self-signed identity isn't valid for signing; trust it for code signing only. +if security find-identity -v -p codesigning | grep -q "\"$name\""; then + echo "==> '$name' is trusted for code signing" +else + echo "==> trusting '$name' for code signing (macOS asks for your password)" + security add-trusted-cert -r trustRoot -p codeSign -k "$keychain" "$work/cert.pem" +fi -echo "==> importing into the login keychain, usable by codesign" -security import "$work/identity.p12" -k "$keychain" -P common-tools -T /usr/bin/codesign >/dev/null +# A permission granted to an earlier signature stays switched on in System Settings but no longer +# applies. Clear it, so the app asks again and the new grant sticks. +for service in $permissions; do + echo "==> resetting the $service permission for $bundle_id; grant it again when the app asks" + tccutil reset "$service" "$bundle_id" +done -echo "==> done. Rebuild with scripts/task build, then grant any permission an app asks for once more." +echo "==> done. Rebuild with scripts/task build or scripts/task run." diff --git a/scripts/make-icon.swift b/scripts/make-icon.swift index e2c71f4..80e319c 100644 --- a/scripts/make-icon.swift +++ b/scripts/make-icon.swift @@ -1,7 +1,9 @@ // Renders Resources/AppIcon.icns and docs/icon.png. Run: swift scripts/make-icon.swift // -// The Common Tools icon family: a blue squircle with a frosted glass glyph. Only the glyph differs -// between Common Stats, Common Keys, and Common Bar; keep the rest identical across the three repos. +// The Common Tools icon family: a blue squircle with a frosted glass glyph. Only the glyph is this +// app's own; the rest is the same in every app and comes from the standard in oddurs/common-tools. +// After changing the glyph, copy a 128 px render into the standard's Resources/CommonTools/ too, so +// every app's Settings shows the new icon. import AppKit // Shared look: Apple's 824 pt squircle on a 1024 canvas, a blue gradient body, and a frosted diff --git a/scripts/task b/scripts/task index bc8352b..a385116 100755 --- a/scripts/task +++ b/scripts/task @@ -38,7 +38,8 @@ test_flags() { # makes one, shared by every Common Tools app. sign() { identity="Common Tools Development" - if security find-certificate -c "$identity" >/dev/null 2>&1; then + # Only a valid identity: one that's present but not trusted for code signing makes codesign fail. + if security find-identity -v -p codesigning | grep -q "\"$identity\""; then echo "==> signing $1 with '$identity'" codesign --force --sign "$identity" --identifier "$2" --options runtime "$1" else