@@ -1718,3 +1718,124 @@ test('release publication omits target overrides and uploads before publishing',
17181718 const prepare = workflow . jobs . prepare . steps . find ( ( entry ) => entry . id === 'meta' ) . run ;
17191719 assert . match ( prepare , / U P L O A D .* t r u e .* - z .* T A G _ S H A / ) ;
17201720} ) ;
1721+
1722+ test ( 'macOS release verification uses the pinned workflow revision without changing old-tag artifacts' , ( t ) => {
1723+ const workflow = yaml . parse ( readFileSync ( path . join ( repoRoot , '.github/workflows/desktop-package.yml' ) , 'utf8' ) ) ;
1724+ const step = workflow . jobs . package . steps . find ( entry => entry . name === 'Verify Apple signature and notarization' ) ;
1725+ const root = mkdtempSync ( path . join ( tmpdir ( ) , 'openbitfun-apple-policy-' ) ) ;
1726+ t . after ( ( ) => rmSync ( root , { recursive : true , force : true } ) ) ;
1727+ const source = path . join ( root , 'source' ) ;
1728+ const checkout = path . join ( root , 'checkout' ) ;
1729+ mkdirSync ( path . join ( source , 'scripts/ci' ) , { recursive : true } ) ;
1730+ const git = ( cwd , ...args ) => {
1731+ const result = spawnSync ( 'git' , args , { cwd, encoding : 'utf8' , windowsHide : true } ) ;
1732+ assert . equal ( result . status , 0 , result . stderr ) ;
1733+ return result . stdout . trim ( ) ;
1734+ } ;
1735+ const commit = ( ) => git ( source , '-c' , 'user.name=Release Fixture' , '-c' , 'user.email=release@example.invalid' ,
1736+ '-c' , 'commit.gpgsign=false' , 'commit' , '-qm' , 'Fixture' ) ;
1737+ const verifier = path . join ( source , 'scripts/ci/verify-macos-signing.sh' ) ;
1738+ const microphone = path . join ( source , 'scripts/ci/verify-macos-microphone.sh' ) ;
1739+ git ( source , 'init' , '-q' ) ;
1740+ git ( source , 'config' , 'core.autocrlf' , 'false' ) ;
1741+ writeFileSync ( verifier , 'exit 99\n' ) ;
1742+ writeFileSync ( microphone , 'exit 99\n' ) ;
1743+ git ( source , 'add' , '.' ) ;
1744+ commit ( ) ;
1745+ const oldSha = git ( source , 'rev-parse' , 'HEAD' ) ;
1746+ git ( source , 'tag' , 'fixture-old' ) ;
1747+ writeFileSync ( verifier , 'printf "workflow-revision:%s\\n" "$1" > policy-result\nbash "$(dirname "$0")/verify-macos-microphone.sh"\n' ) ;
1748+ writeFileSync ( microphone , 'printf "workflow-helper\\n" > helper-result\n' ) ;
1749+ git ( source , 'add' , '.' ) ;
1750+ commit ( ) ;
1751+ const workflowSha = git ( source , 'rev-parse' , 'HEAD' ) ;
1752+ writeFileSync ( verifier , 'exit 98\n' ) ;
1753+ git ( source , 'add' , '.' ) ;
1754+ commit ( ) ;
1755+ git ( root , 'clone' , '--no-local' , '--depth=1' , '--branch' , 'fixture-old' , source , checkout ) ;
1756+ const artifact = path . join ( checkout , 'signed-asset.bin' ) ;
1757+ writeFileSync ( artifact , 'signed immutable fixture' ) ;
1758+ const result = spawnSync ( 'bash' , [ '-c' , step . run
1759+ . replaceAll ( '${{ github.workflow_sha }}' , workflowSha )
1760+ . replaceAll ( '${{ matrix.platform.target }}' , 'fixture-target' ) ] , {
1761+ cwd : checkout , encoding : 'utf8' , windowsHide : true ,
1762+ } ) ;
1763+ assert . equal ( result . status , 0 , `${ result . stdout } \n${ result . stderr } ` ) ;
1764+ assert . equal ( readFileSync ( path . join ( checkout , 'policy-result' ) , 'utf8' ) , 'workflow-revision:fixture-target\n' ) ;
1765+ assert . equal ( readFileSync ( path . join ( checkout , 'helper-result' ) , 'utf8' ) , 'workflow-helper\n' ) ;
1766+ assert . equal ( git ( checkout , 'rev-parse' , 'HEAD' ) , oldSha ) ;
1767+ assert . equal ( readFileSync ( artifact , 'utf8' ) , 'signed immutable fixture' ) ;
1768+ } ) ;
1769+
1770+ for ( const scenario of [
1771+ { name : 'accepts a transient app ticket connection loss' , failures : 2 , network : true , attempts : 3 , status : 0 , submissions : 1 } ,
1772+ { name : 'retries DMG stapling without resubmitting notarization' , action : 'staple' , kind : 'dmg' , failures : 1 , network : true , attempts : 2 , status : 0 , submissions : 1 } ,
1773+ { name : 'retries DMG validation without resubmitting notarization' , kind : 'dmg' , failures : 2 , network : true , attempts : 3 , status : 0 , submissions : 1 } ,
1774+ { name : 'rejects exhausted ticket connection retries' , failures : 99 , network : true , attempts : 3 , status : 68 , submissions : 0 } ,
1775+ { name : 'rejects invalid tickets without retrying' , failures : 99 , network : false , attempts : 1 , status : 68 , submissions : 0 } ,
1776+ { name : 'preserves other stapler exit codes' , failures : 99 , network : true , ticketStatus : 65 , attempts : 1 , status : 65 , submissions : 0 } ,
1777+ { name : 'rejects signature errors before contacting Apple' , signatureStatus : 1 , attempts : 0 , status : 1 , submissions : 0 } ,
1778+ { name : 'preserves Gatekeeper rejection' , gatekeeperStatus : 3 , attempts : 1 , status : 3 , submissions : 0 } ,
1779+ { name : 'preserves notarization submission rejection' , notaryStatus : 4 , attempts : 1 , status : 4 , submissions : 1 } ,
1780+ ] ) {
1781+ test ( `macOS verification ${ scenario . name } ` , ( t ) => {
1782+ const cwd = mkdtempSync ( path . join ( tmpdir ( ) , 'openbitfun-apple-ticket-' ) ) ;
1783+ t . after ( ( ) => rmSync ( cwd , { recursive : true , force : true } ) ) ;
1784+ mkdirSync ( path . join ( cwd , 'scripts/ci' ) , { recursive : true } ) ;
1785+ copyFileSync ( path . join ( repoRoot , 'scripts/ci/verify-macos-signing.sh' ) , path . join ( cwd , 'scripts/ci/verify-macos-signing.sh' ) ) ;
1786+ writeFileSync ( path . join ( cwd , 'scripts/ci/verify-macos-microphone.sh' ) , 'printf "microphone\\n" >> "$FIXTURE_CALLS"\n' ) ;
1787+ const app = path . join ( cwd , 'target/fixture/release/bundle/macos/Test App.app' ) ;
1788+ mkdirSync ( app , { recursive : true } ) ;
1789+ writeFileSync ( path . join ( app , 'fixture' ) , 'fixture' ) ;
1790+ mkdirSync ( path . join ( cwd , 'target/fixture/release/bundle/dmg' ) , { recursive : true } ) ;
1791+ writeFileSync ( path . join ( cwd , 'target/fixture/release/bundle/dmg/Test App.dmg' ) , 'fixture' ) ;
1792+ const result = spawnSync ( 'bash' , [ '-c' , `
1793+ codesign() {
1794+ printf 'codesign %s\\n' "$*" >> "$FIXTURE_CALLS"
1795+ if [[ "$*" == *--verify* ]]; then return "$SIGNATURE_STATUS"; fi
1796+ printf 'Authority=Developer ID Application: Fixture\\nflags=0x10000(runtime)\\nTimestamp=fixture\\n'
1797+ }
1798+ file() { printf 'Mach-O fixture\\n'; }
1799+ spctl() { printf 'spctl %s\\n' "$*" >> "$FIXTURE_CALLS"; return "$GATEKEEPER_STATUS"; }
1800+ sleep() { printf 'sleep %s\\n' "$*" >> "$FIXTURE_CALLS"; }
1801+ xcrun() {
1802+ printf 'xcrun %s\\n' "$*" >> "$FIXTURE_CALLS"
1803+ if [[ "$1" == notarytool ]]; then return "$NOTARY_STATUS"; fi
1804+ local kind=app count=0
1805+ [[ "$3" == *.dmg ]] && kind=dmg
1806+ if [[ "$2" == "$TICKET_ACTION" && "$kind" == "$TICKET_KIND" ]]; then
1807+ [[ ! -f attempts ]] || read -r count < attempts
1808+ count=$((count + 1))
1809+ printf '%s\\n' "$count" > attempts
1810+ if [[ "$count" -le "$TICKET_FAILURES" ]]; then
1811+ if [[ "$TICKET_NETWORK" == true ]]; then
1812+ printf 'Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost."\\n' >&2
1813+ else
1814+ printf 'The ticket is invalid. The validate action failed! Error 68.\\n' >&2
1815+ fi
1816+ return "$TICKET_EXIT_CODE"
1817+ fi
1818+ fi
1819+ return 0
1820+ }
1821+ export -f codesign file spctl sleep xcrun
1822+ bash scripts/ci/verify-macos-signing.sh fixture
1823+ ` ] , { cwd, encoding : 'utf8' , windowsHide : true , env : { ...process . env ,
1824+ OPENBITFUN_APPLE_SIGNING_CONFIGURED : 'true' , FIXTURE_CALLS : path . join ( cwd , 'calls' ) ,
1825+ SIGNATURE_STATUS : String ( scenario . signatureStatus ?? 0 ) , GATEKEEPER_STATUS : String ( scenario . gatekeeperStatus ?? 0 ) ,
1826+ NOTARY_STATUS : String ( scenario . notaryStatus ?? 0 ) , APPLE_API_ISSUER : 'fixture' , APPLE_API_KEY : 'fixture' , APPLE_API_KEY_PATH : 'fixture' ,
1827+ TICKET_ACTION : scenario . action ?? 'validate' , TICKET_KIND : scenario . kind ?? 'app' ,
1828+ TICKET_FAILURES : String ( scenario . failures ?? 0 ) , TICKET_NETWORK : String ( scenario . network ?? false ) ,
1829+ TICKET_EXIT_CODE : String ( scenario . ticketStatus ?? 68 ) ,
1830+ } } ) ;
1831+ const calls = readFileSync ( path . join ( cwd , 'calls' ) , 'utf8' ) ;
1832+ assert . equal ( result . status , scenario . status , `${ result . stdout } \n${ result . stderr } ` ) ;
1833+ assert . equal ( ( calls . match ( / ^ x c r u n n o t a r y t o o l s u b m i t / gm) ?? [ ] ) . length , scenario . submissions , calls ) ;
1834+ assert . equal ( ( calls . match ( / ^ m i c r o p h o n e $ / gm) ?? [ ] ) . length , scenario . signatureStatus ? 0 : 1 , calls ) ;
1835+ const attempts = scenario . attempts === 0 ? 0 : Number ( readFileSync ( path . join ( cwd , 'attempts' ) , 'utf8' ) ) ;
1836+ assert . equal ( attempts , scenario . attempts , calls ) ;
1837+ assert . equal ( ( calls . match ( / ^ s l e e p / gm) ?? [ ] ) . length , Math . max ( 0 , scenario . attempts - 1 ) , calls ) ;
1838+ if ( scenario . status === 0 ) assert . match ( result . stdout , / V e r i f i e d D e v e l o p e r I D s i g n a t u r e s / ) ;
1839+ else assert . doesNotMatch ( result . stdout , / V e r i f i e d D e v e l o p e r I D s i g n a t u r e s / ) ;
1840+ } ) ;
1841+ }
0 commit comments