mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/mobile-launch-points-agent-launch
# Conflicts: # mobile/rpc-foundation/goldens/aivault-history-scan-fulfilled.json # mobile/rpc-foundation/goldens/aivault-history-scan-unsupported.json # mobile/rpc-foundation/goldens/aivault-history-scan-worktrees-late.json # mobile/rpc-foundation/goldens/aivault-history-screen-listed.json # mobile/rpc-foundation/goldens/aivault-history-screen-worktrees.json # mobile/rpc-foundation/goldens/aivault-resume-launch-create-refused.json # mobile/rpc-foundation/goldens/aivault-resume-launch-invalid-tab.json # mobile/rpc-foundation/goldens/aivault-resume-launch-locked.json # mobile/rpc-foundation/goldens/aivault-resume-launch-sent.json # mobile/rpc-foundation/goldens/aivault-resume-prepare-refused.json # mobile/rpc-foundation/goldens/aivault-resume-prepare-repin.json # mobile/rpc-foundation/goldens/aivault-resume-prepare-skipped.json # mobile/rpc-foundation/goldens/aivault-resume-prepare-unavailable.json # mobile/rpc-foundation/goldens/b1.json # mobile/rpc-foundation/goldens/b2.json # mobile/rpc-foundation/goldens/b3.json # mobile/rpc-foundation/goldens/browser-dialog-accepted.json # mobile/rpc-foundation/goldens/browser-dialog-dismissed.json # mobile/rpc-foundation/goldens/browser-keyboard-input.json # mobile/rpc-foundation/goldens/browser-pointer-click-accepted.json # mobile/rpc-foundation/goldens/browser-pointer-click-fallback.json # mobile/rpc-foundation/goldens/browser-wheel-scrolled.json # mobile/rpc-foundation/goldens/clipboard-image-attachment-anonymous.json # mobile/rpc-foundation/goldens/clipboard-image-attachment-blocked-before-send.json # mobile/rpc-foundation/goldens/clipboard-image-attachment-cancelled.json # mobile/rpc-foundation/goldens/clipboard-image-attachment-pasted.json # mobile/rpc-foundation/goldens/clipboard-image-attachment-upload-refused.json # mobile/rpc-foundation/goldens/clipboard-image-upload-aborts-on-chunk-failure.json # mobile/rpc-foundation/goldens/clipboard-image-upload-chunked.json # mobile/rpc-foundation/goldens/clipboard-image-upload-single-frame-fallback.json # mobile/rpc-foundation/goldens/clipboard-image-upload-start-refused.json # mobile/rpc-foundation/goldens/codex-reset-credit-consumed.json # mobile/rpc-foundation/goldens/codex-reset-credit-resumed.json # mobile/rpc-foundation/goldens/components-codex-capability.json # mobile/rpc-foundation/goldens/components-setup-ask.json # mobile/rpc-foundation/goldens/components-target-local.json # mobile/rpc-foundation/goldens/components-target-ssh.json # mobile/rpc-foundation/goldens/diff-review-branch-compare.json # mobile/rpc-foundation/goldens/diff-review-branch-file-diff.json # mobile/rpc-foundation/goldens/diff-review-notes-refused-before-compare.json # mobile/rpc-foundation/goldens/diff-review-refused-file-diff.json # mobile/rpc-foundation/goldens/diff-review-snapshot.json # mobile/rpc-foundation/goldens/diff-review-status-unavailable.json # mobile/rpc-foundation/goldens/diff-review-worktree-file-diff.json # mobile/rpc-foundation/goldens/file-tap-open-refused.json # mobile/rpc-foundation/goldens/file-tap-opens-worktree-file.json # mobile/rpc-foundation/goldens/file-tap-previews-absolute-artifact.json # mobile/rpc-foundation/goldens/file-tap-resolve-miss.json # mobile/rpc-foundation/goldens/file-tap-resolve-refused.json # mobile/rpc-foundation/goldens/files-explorer-legacy-fallback.json # mobile/rpc-foundation/goldens/files-explorer-readdir.json # mobile/rpc-foundation/goldens/files-ownership-local.json # mobile/rpc-foundation/goldens/files-ownership-ssh.json # mobile/rpc-foundation/goldens/files-preview-artifact-direct.json # mobile/rpc-foundation/goldens/files-preview-artifact-image-read.json # mobile/rpc-foundation/goldens/files-preview-artifact-image.json # mobile/rpc-foundation/goldens/files-preview-grant-refresh.json # mobile/rpc-foundation/goldens/files-preview-worktree-image-read.json # mobile/rpc-foundation/goldens/files-preview-worktree-image.json # mobile/rpc-foundation/goldens/files-preview-worktree-text-read.json # mobile/rpc-foundation/goldens/files-preview-worktree.json # mobile/rpc-foundation/goldens/files-save-blind.json # mobile/rpc-foundation/goldens/files-save-verified.json # mobile/rpc-foundation/goldens/files-tab-doc-shapes.json # mobile/rpc-foundation/goldens/home-host-accounts.json # mobile/rpc-foundation/goldens/home-host-stats.json # mobile/rpc-foundation/goldens/host-view-settings-sync.json # mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json # mobile/rpc-foundation/goldens/host-worktree-delete-refused.json # mobile/rpc-foundation/goldens/host-worktree-refresh-stream.json # mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json # mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json # mobile/rpc-foundation/goldens/inventory-lifecycle.json # mobile/rpc-foundation/goldens/inventory-repeat-query.json # mobile/rpc-foundation/goldens/lifecycle-b3.json # mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json # mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json # mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json # mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json # mobile/rpc-foundation/goldens/linear-select-workspace.json # mobile/rpc-foundation/goldens/live-worktree-name-stream.json # mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.create-1.json # mobile/rpc-foundation/goldens/matrix-agentsession.structured-create-agentsession.createsupport-1.json # mobile/rpc-foundation/goldens/matrix-agentsession.structured-launch-agentsession.createsupport-1.json # mobile/rpc-foundation/goldens/matrix-aivault.history-aivault.listsessions-1.json # mobile/rpc-foundation/goldens/matrix-aivault.history-screen-platform-status.json # mobile/rpc-foundation/goldens/matrix-aivault.history-screen-status.get-2.json # mobile/rpc-foundation/goldens/matrix-aivault.history-screen-worktree.ps-1.json # mobile/rpc-foundation/goldens/matrix-aivault.history-status.get-1.json # mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-session.tabs.createterminal-1.json # mobile/rpc-foundation/goldens/matrix-aivault.resume-launch-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-aivault.resume-preparation-aivault.preparesessionresume-1.json # mobile/rpc-foundation/goldens/matrix-browser.dialog-browser.dialogaccept-1.json # mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keyboardinserttext-1.json # mobile/rpc-foundation/goldens/matrix-browser.keyboard-browser.keypress-1.json # mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseclick-1.json # mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousedown-1.json # mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mousemove-1.json # mobile/rpc-foundation/goldens/matrix-browser.pointer-click-browser.mouseup-1.json # mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousemove-1.json # mobile/rpc-foundation/goldens/matrix-browser.wheel-browser.mousewheel-1.json # mobile/rpc-foundation/goldens/matrix-clipboard.image-attachment-clipboard.startimageupload-1.json # mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.saveimageastempfile-1.json # mobile/rpc-foundation/goldens/matrix-clipboard.image-upload-clipboard.startimageupload-1.json # mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json # mobile/rpc-foundation/goldens/matrix-components.codex-reset-credit-accounts.consumecodexresetcredit-1.json # mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json # mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json # mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json # mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json # mobile/rpc-foundation/goldens/matrix-components.new-workspace-repositories-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json # mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.list-1.json # mobile/rpc-foundation/goldens/matrix-files.explorer-screen-files.readdir-1.json # mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json # mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json # mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-artifact-image-files.readterminalartifactpreview-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json # mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-worktree-image-files.readpreview-1.json # mobile/rpc-foundation/goldens/matrix-files.preview-worktree-text-files.read-1.json # mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json # mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json # mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json # mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.open-1.json # mobile/rpc-foundation/goldens/matrix-files.terminal-path-tap-files.resolveterminalpath-1.json # mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json # mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-git.branch-diff-preview-git.branchdiff-1.json # mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json # mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json # mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json # mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.commitcompare-1.json # mobile/rpc-foundation/goldens/matrix-git.history-commit-files-git.history-1.json # mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json # mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json # mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.addprreviewcommentreply-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.deleteissuecommentbyslug-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.project.updateissuecommentbyslug-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-comment-mutation-github.resolvereviewthread-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.mergepr-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.removeprreviewers-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.requestprreviewers-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.rerunprchecks-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.setprautomerge-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-mutation-github.updateprstate-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.listassignableusers-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prcheckdetails-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prchecks-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.prforbranch-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.reposlug-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-github.workitemdetails-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-read-hostedreview.forbranch-1.json # mobile/rpc-foundation/goldens/matrix-github.pr-title-mutation-github.updateprtitle-1.json # mobile/rpc-foundation/goldens/matrix-home.host-accounts-accounts.list-1.json # mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json # mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-1.json # mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-2.json # mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-1-3.json # mobile/rpc-foundation/goldens/matrix-host-worktree-refresh-runtime.clientevents.subscribe-2-1.json # mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json # mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json # mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json # mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json # mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json # mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json # mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json # mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json # mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json # mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json # mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json # mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json # mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json # mobile/rpc-foundation/goldens/matrix-linear.select-workspace-picker-linear.selectworkspace-1.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-1.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-1-2.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-runtime.clientevents.subscribe-2-1.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-2.json # mobile/rpc-foundation/goldens/matrix-live-worktree-name-worktree.show-3.json # mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-app-js.json # mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-head.json # mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-index-tail.json # mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-fetch-mobileweb.bundle.manifest-1.json # mobile/rpc-foundation/goldens/matrix-mobileweb.bundle-manifest-mobileweb.bundle.manifest-1.json # mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-nativechat.image-paste-terminal.send-2.json # mobile/rpc-foundation/goldens/matrix-nativechat.image-upload-clipboard.startimageupload-1.json # mobile/rpc-foundation/goldens/matrix-nativechat.session-option-pick-settings.mutatenativechatsessionoptions-1.json # mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-nativechat.terminal-write-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.getmissedsince-1.json # mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-1.json # mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.subscribe-1-2.json # mobile/rpc-foundation/goldens/matrix-notifications.desktop-stream-notifications.unsubscribe-1.json # mobile/rpc-foundation/goldens/matrix-notifications.display-test-screen-notifications.testpush-1.json # mobile/rpc-foundation/goldens/matrix-notifications.push-dismissal-notifications.getmissedsince-1.json # mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.registerpush-1.json # mobile/rpc-foundation/goldens/matrix-notifications.push-registration-notifications.unregisterpush-1.json # mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-direct-status.json # mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.getendpoints-1.json # mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-pairing.provisionrelay-1.json # mobile/rpc-foundation/goldens/matrix-pairing.pre-profile-relay-status.json # mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json # mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-1.json # mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.getendpoints-2.json # mobile/rpc-foundation/goldens/matrix-relay.credential-rotation-pairing.provisionrelay-1.json # mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-1.json # mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.getendpoints-2.json # mobile/rpc-foundation/goldens/matrix-relay.direct-upgrade-pairing.provisionrelay-1.json # mobile/rpc-foundation/goldens/matrix-relay.pairing-recovery-pairing.getendpoints-1.json # mobile/rpc-foundation/goldens/matrix-session.browser-tab-create-browser.tabcreate-1.json # mobile/rpc-foundation/goldens/matrix-session.content-create-files.createfile-1.json # mobile/rpc-foundation/goldens/matrix-session.content-create-files.open-1.json # mobile/rpc-foundation/goldens/matrix-session.content-create-status.get-1.json # mobile/rpc-foundation/goldens/matrix-session.content-create-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-session.create-terminal-session.tabs.createterminal-1.json # mobile/rpc-foundation/goldens/matrix-session.create-terminal-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-notes-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-actions-worktree.set-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-base-ref-show.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-git.branchcompare-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-git.status-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-session.diff-review-review-show.json # mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-files.read-1.json # mobile/rpc-foundation/goldens/matrix-session.markdown-disk-fallback-markdown.readtab-1.json # mobile/rpc-foundation/goldens/matrix-session.markdown-save-markdown.savetab-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.readsession-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-1-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-page-nativechat.subscribe-2-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-readability-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-session.native-chat-stop-terminal.send-2.json # mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.branchcompare-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-git.status-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-branch-context-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prchecks-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-github.prforbranch-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-hostedreview.forbranch-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-sidebar-worktree.show-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-triage-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-session.pr-triage-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-session.review-branch-diff-git.branchdiff-1.json # mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-1.json # mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-2.json # mobile/rpc-foundation/goldens/matrix-session.review-file-diff-git.diff-3.json # mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.discard-1.json # mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-1.json # mobile/rpc-foundation/goldens/matrix-session.review-git-mutations-git.stage-2.json # mobile/rpc-foundation/goldens/matrix-session.review-send-sheet-session.tabs.list-1.json # mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-1.json # mobile/rpc-foundation/goldens/matrix-session.startup-worktree.activate-2.json # mobile/rpc-foundation/goldens/matrix-session.tab-activation-session.tabs.activate-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-activation-terminal.focus-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-close-session-session.tabs.close-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-close-terminal.close-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-documents-markdown.readtab-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-rename-terminal.rename-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json # mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json # mobile/rpc-foundation/goldens/matrix-session.tabs-stream-health-session.tabs.list-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-display-mode-terminal.setdisplaymode-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.clearbuffer-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-gesture-input-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-input-send-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-inventory-terminal.list-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-paste-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-paste-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-session.terminal-paste-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-session.worktree-connection-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-session.worktree-connection-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json # mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json # mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json # mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json # mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-preflight.detectagents-1.json # mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-settings.new-tab-local-agents-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.getterminalquickcommands-1.json # mobile/rpc-foundation/goldens/matrix-settings.quick-commands-settings.updateterminalquickcommands-1.json # mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json # mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json # mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json # mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json # mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json # mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json # mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json # mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json # mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json # mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json # mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json # mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json # mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json # mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json # mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.dictation.setup-1.json # mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.delete-1.json # mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.download-1.json # mobile/rpc-foundation/goldens/matrix-speech.setup-sheet-speech.models.list-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.addprreviewcomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.prfilecontents-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.rerunprchecks-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.resolvereviewthread-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-checks-files-github.setprfileviewed-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-comment-github-github.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-gitlab.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-comment-gitlab-mr-gitlab.addmrcomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-github-github.workitemdetails-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-gitlab-gitlab.workitemdetails-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.getissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-linear-linear.issuecomments-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listassignableusers-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-detail-metadata-github.listlabels-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-merge-gitlab-gitlab.mergemr-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-github-github.updatepr-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-gitlab.updateissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-metadata-gitlab-mr-gitlab.updatemr-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.addprreviewcommentreply-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-github.mergepr-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-reply-merge-linear.updateissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.prchecks-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-review-github-github.requestprreviewers-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-github.updateissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-gitlab.updateissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.item-status-gitlab-mr-gitlab.updatemrstate-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-connect-linear.connect-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.createissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-item-linear.getissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.listteams-1.json # mobile/rpc-foundation/goldens/matrix-tasks.linear-team-context-linear.teamstates-1.json # mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json # mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json # mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listaccessible-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.listviews-2.json # mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.resolveref-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-board-load-github.project.viewtable-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-repo-slugs-github.reposlug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.addissuecommentbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuebyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-issue-github.project.updateissuecommentbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-comments-pr-github.project.updatepullrequestbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-detail-github.project.workitemdetailsbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.clearitemfield-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateissuetypebyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-fields-github.project.updateitemfield-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.addprreviewcomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.mergepr-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.prfilecontents-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-files-merge-github.updateprstate-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listassignableusersbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listissuetypesbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-metadata-load-github.project.listlabelsbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.prchecks-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.requestprreviewers-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.rerunprchecks-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-review-checks-github.setprfileviewed-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addissuecomment-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.addprreviewcommentreply-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.project.deleteissuecommentbyslug-1.json # mobile/rpc-foundation/goldens/matrix-tasks.project-row-threads-github.resolvereviewthread-1.json # mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.countworkitems-1.json # mobile/rpc-foundation/goldens/matrix-tasks.provider-load-github.listworkitems-1.json # mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.listteams-1.json # mobile/rpc-foundation/goldens/matrix-tasks.provider-load-linear.status-1.json # mobile/rpc-foundation/goldens/matrix-tasks.provider-load-settings.update-1.json # mobile/rpc-foundation/goldens/matrix-tasks.route-repo-list-repo.list-1.json # mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json # mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json # mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json # mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json # mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-github.createissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-create-github-repo.update-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-create-gitlab-gitlab.createissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-create-linear-linear.createissue-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-items-gitlab.listworkitems-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-list-gitlab-todos-gitlab.todos-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.listissues-1.json # mobile/rpc-foundation/goldens/matrix-tasks.task-list-linear-linear.searchissues-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json # mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json # mobile/rpc-foundation/goldens/matrix-terminal.query-reply-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-terminal.raw-input-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-terminal.raw-input-terminal.send-1.json # mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-1.json # mobile/rpc-foundation/goldens/matrix-terminal.takeover-report-orchestration.workerterminaluserinput-2.json # mobile/rpc-foundation/goldens/matrix-terminal.viewport-refit-terminal.updateviewport-1.json # mobile/rpc-foundation/goldens/matrix-transport.capability-probe-status.get-1.json # mobile/rpc-foundation/goldens/matrix-transport.host-status-gates-status.get-1.json # mobile/rpc-foundation/goldens/matrix-transport.pairing-race-direct-status.json # mobile/rpc-foundation/goldens/matrix-transport.pairing-race-relay-status.json # mobile/rpc-foundation/goldens/matrix-worktree.agent-launch-create-agent.launch-1.json # mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json # mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json # mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json # mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json # mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json # mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json # mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json # mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json # mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json # mobile/rpc-foundation/goldens/mobile-web-bundle-build-changed.json # mobile/rpc-foundation/goldens/mobile-web-bundle-fetch-paged.json # mobile/rpc-foundation/goldens/mobile-web-bundle-manifest-read.json # mobile/rpc-foundation/goldens/mobile-web-bundle-unavailable.json # mobile/rpc-foundation/goldens/native-chat-image-paste-single.json # mobile/rpc-foundation/goldens/native-chat-image-paste-stops-on-rejection.json # mobile/rpc-foundation/goldens/native-chat-image-paste-trailing-image.json # mobile/rpc-foundation/goldens/native-chat-image-paste-two-images.json # mobile/rpc-foundation/goldens/native-chat-image-upload-cancelled.json # mobile/rpc-foundation/goldens/native-chat-image-upload-second-fails.json # mobile/rpc-foundation/goldens/native-chat-image-upload-single.json # mobile/rpc-foundation/goldens/native-chat-image-upload-start-refused.json # mobile/rpc-foundation/goldens/native-chat-image-upload-two.json # mobile/rpc-foundation/goldens/native-chat-page-earlier.json # mobile/rpc-foundation/goldens/native-chat-readability-local-repo.json # mobile/rpc-foundation/goldens/native-chat-readability-refused.json # mobile/rpc-foundation/goldens/native-chat-readability-remote-repo.json # mobile/rpc-foundation/goldens/native-chat-session-option-pick-empty.json # mobile/rpc-foundation/goldens/native-chat-session-option-pick-refused.json # mobile/rpc-foundation/goldens/native-chat-session-option-pick-written.json # mobile/rpc-foundation/goldens/native-chat-stop-accepted.json # mobile/rpc-foundation/goldens/native-chat-stop-both-rejected.json # mobile/rpc-foundation/goldens/native-chat-stop-delivery-unknown.json # mobile/rpc-foundation/goldens/native-chat-write-accepted.json # mobile/rpc-foundation/goldens/native-chat-write-clear-line.json # mobile/rpc-foundation/goldens/native-chat-write-delivery-unknown.json # mobile/rpc-foundation/goldens/native-chat-write-rejected.json # mobile/rpc-foundation/goldens/native-chat-write-typed-command.json # mobile/rpc-foundation/goldens/new-tab-local-agents.json # mobile/rpc-foundation/goldens/new-workspace-repositories-fulfilled.json # mobile/rpc-foundation/goldens/notifications-desktop-stream-closed.json # mobile/rpc-foundation/goldens/notifications-desktop-stream-replayed.json # mobile/rpc-foundation/goldens/notifications-desktop-stream.json # mobile/rpc-foundation/goldens/notifications-display-test-accepted.json # mobile/rpc-foundation/goldens/notifications-display-test-not-registered.json # mobile/rpc-foundation/goldens/notifications-display-test-rate-limited.json # mobile/rpc-foundation/goldens/notifications-display-test-unknown-reason.json # mobile/rpc-foundation/goldens/notifications-push-gateway-rejected.json # mobile/rpc-foundation/goldens/notifications-push-registered.json # mobile/rpc-foundation/goldens/pairing-pre-profile-direct-wins-and-provisions.json # mobile/rpc-foundation/goldens/pairing-pre-profile-provision-unsupported-saves-direct-host.json # mobile/rpc-foundation/goldens/pairing-pre-profile-times-out.json # mobile/rpc-foundation/goldens/pr-branch-identity.json # mobile/rpc-foundation/goldens/pr-branch-repo-context.json # mobile/rpc-foundation/goldens/pr-comment-mutation.json # mobile/rpc-foundation/goldens/pr-comment-resolve-unconfirmed.json # mobile/rpc-foundation/goldens/pr-mutation-in-band-failure.json # mobile/rpc-foundation/goldens/pr-mutation-status.json # mobile/rpc-foundation/goldens/pr-read-fork-routing.json # mobile/rpc-foundation/goldens/pr-read-surface.json # mobile/rpc-foundation/goldens/pr-read-upstream-error.json # mobile/rpc-foundation/goldens/pr-sidebar-checks-refused.json # mobile/rpc-foundation/goldens/pr-sidebar-load.json # mobile/rpc-foundation/goldens/pr-title-mutation.json # mobile/rpc-foundation/goldens/pr-title-unconfirmed.json # mobile/rpc-foundation/goldens/pr-triage-invalid-terminal.json # mobile/rpc-foundation/goldens/pr-triage-launch.json # mobile/rpc-foundation/goldens/pr-triage-send-locked.json # mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json # mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json # mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json # mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json # mobile/rpc-foundation/goldens/push-dismissal-tray-reconciled.json # mobile/rpc-foundation/goldens/quick-commands-load-refused.json # mobile/rpc-foundation/goldens/quick-commands-loaded-and-saved.json # mobile/rpc-foundation/goldens/quick-commands-save-refused-rolls-back.json # mobile/rpc-foundation/goldens/relay-direct-upgrade-commits.json # mobile/rpc-foundation/goldens/relay-direct-upgrade-unsupported-host-declines.json # mobile/rpc-foundation/goldens/relay-pairing-recovery-invite-authorizes.json # mobile/rpc-foundation/goldens/relay-pairing-recovery-resume-committed.json # mobile/rpc-foundation/goldens/relay-rotation-installs-and-commits.json # mobile/rpc-foundation/goldens/relay-rotation-resumes-committed-pending.json # mobile/rpc-foundation/goldens/review-branch-diff-shapes.json # mobile/rpc-foundation/goldens/review-create-terminal-refused.json # mobile/rpc-foundation/goldens/review-file-diff-shapes.json # mobile/rpc-foundation/goldens/review-git-mutations-run.json # mobile/rpc-foundation/goldens/review-mark-reviewed-persists.json # mobile/rpc-foundation/goldens/review-mark-reviewed-rolls-back.json # mobile/rpc-foundation/goldens/review-open-in-session.json # mobile/rpc-foundation/goldens/review-send-notes-heals-stale-input.json # mobile/rpc-foundation/goldens/review-send-sheet-lists-terminals.json # mobile/rpc-foundation/goldens/review-stage-file.json # mobile/rpc-foundation/goldens/review-stage-refused.json # mobile/rpc-foundation/goldens/sc-base-ref-default.json # mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json # mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json # mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json # mobile/rpc-foundation/goldens/sc-branch-diff-previewed.json # mobile/rpc-foundation/goldens/sc-changes-loaded.json # mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json # mobile/rpc-foundation/goldens/sc-commit-message-canceled.json # mobile/rpc-foundation/goldens/sc-commit-message-generated.json # mobile/rpc-foundation/goldens/sc-create-existing-review.json # mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json # mobile/rpc-foundation/goldens/sc-create-intent-unlisted-provider.json # mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json # mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json # mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json # mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json # mobile/rpc-foundation/goldens/sc-eligibility-fetched.json # mobile/rpc-foundation/goldens/sc-history-commit-files.json # mobile/rpc-foundation/goldens/sc-history-loaded.json # mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json # mobile/rpc-foundation/goldens/sc-pr-link-read.json # mobile/rpc-foundation/goldens/sc-pr-link-set.json # mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json # mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json # mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json # mobile/rpc-foundation/goldens/sc-prerequisite-publish.json # mobile/rpc-foundation/goldens/sc-prerequisite-push.json # mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json # mobile/rpc-foundation/goldens/sc-reveal-first-poll.json # mobile/rpc-foundation/goldens/sc-reveal-timeout.json # mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json # mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json # mobile/rpc-foundation/goldens/sc-review-commit-rejected.json # mobile/rpc-foundation/goldens/sc-review-commit.json # mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json # mobile/rpc-foundation/goldens/sc-review-status-normalized.json # mobile/rpc-foundation/goldens/schedules-b3.json # mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json # mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json # mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json # mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json # mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json # mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json # mobile/rpc-foundation/goldens/session-browser-tab-created.json # mobile/rpc-foundation/goldens/session-create-browser-refused.json # mobile/rpc-foundation/goldens/session-create-browser-tab.json # mobile/rpc-foundation/goldens/session-create-markdown-name-collision.json # mobile/rpc-foundation/goldens/session-create-markdown-note.json # mobile/rpc-foundation/goldens/session-create-terminal-ignores-a-second-create-in-flight.json # mobile/rpc-foundation/goldens/session-create-terminal-launches-an-agent-quick-command.json # mobile/rpc-foundation/goldens/session-create-terminal-refused.json # mobile/rpc-foundation/goldens/session-create-terminal-replaces-active.json # mobile/rpc-foundation/goldens/session-create-terminal-runs-a-quick-command.json # mobile/rpc-foundation/goldens/session-create-terminal-with-prompt.json # mobile/rpc-foundation/goldens/session-create-terminal-without-active-tab.json # mobile/rpc-foundation/goldens/session-create-terminal-without-handle.json # mobile/rpc-foundation/goldens/session-diff-notes-load-refused.json # mobile/rpc-foundation/goldens/session-diff-notes-loaded.json # mobile/rpc-foundation/goldens/session-file-tab-read.json # mobile/rpc-foundation/goldens/session-markdown-disk-read.json # mobile/rpc-foundation/goldens/session-markdown-disk-served.json # mobile/rpc-foundation/goldens/session-markdown-save-conflict.json # mobile/rpc-foundation/goldens/session-markdown-saved.json # mobile/rpc-foundation/goldens/session-markdown-tab-disk-fallback.json # mobile/rpc-foundation/goldens/session-markdown-tab-read.json # mobile/rpc-foundation/goldens/session-markdown-tab-refused.json # mobile/rpc-foundation/goldens/session-startup-both-activation-sites.json # mobile/rpc-foundation/goldens/session-startup-floating-route-skips-activation.json # mobile/rpc-foundation/goldens/session-startup-keeps-terminals-visible-on-reconnect.json # mobile/rpc-foundation/goldens/session-startup-refused-tab-load-still-loads-terminals.json # mobile/rpc-foundation/goldens/session-tab-activation-focus-and-activate.json # mobile/rpc-foundation/goldens/session-tab-activation-refused.json # mobile/rpc-foundation/goldens/session-tab-activation-transport-error.json # mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json # mobile/rpc-foundation/goldens/session-tab-close-session-tab.json # mobile/rpc-foundation/goldens/session-tab-close-terminal.json # mobile/rpc-foundation/goldens/session-tab-closed.json # mobile/rpc-foundation/goldens/session-tab-rename.json # mobile/rpc-foundation/goldens/session-tab-renamed.json # mobile/rpc-foundation/goldens/session-tabs-health-errored.json # mobile/rpc-foundation/goldens/session-tabs-health-reconciled.json # mobile/rpc-foundation/goldens/session-tabs-health-refused.json # mobile/rpc-foundation/goldens/session-tabs-health-stale-application-revision.json # mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-take-floor.json # mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-device-token.json # mobile/rpc-foundation/goldens/session-terminal-display-mode-auto-without-viewport.json # mobile/rpc-foundation/goldens/session-terminal-display-mode-drops-second-toggle.json # mobile/rpc-foundation/goldens/session-terminal-display-mode-to-desktop.json # mobile/rpc-foundation/goldens/session-terminal-list-dedupes-handles.json # mobile/rpc-foundation/goldens/session-terminal-list-empty-guarded.json # mobile/rpc-foundation/goldens/session-terminal-list-merged.json # mobile/rpc-foundation/goldens/session-terminal-list-refused.json # mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json # mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json # mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json # mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json # mobile/rpc-foundation/goldens/settings-home-coalesced.json # mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json # mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json # mobile/rpc-foundation/goldens/settings-home-providers-refused.json # mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json # mobile/rpc-foundation/goldens/settings-new-tab-refused.json # mobile/rpc-foundation/goldens/settings-new-tab-ssh.json # mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json # mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json # mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json # mobile/rpc-foundation/goldens/settings-repo-metadata-icons.json # mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json # mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json # mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json # mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json # mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json # mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json # mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json # mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json # mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json # mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json # mobile/rpc-foundation/goldens/settings-task-hydration-refused.json # mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json # mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json # mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json # mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json # mobile/rpc-foundation/goldens/settings-task-workspace-refused.json # mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json # mobile/rpc-foundation/goldens/settings-task-write.json # mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json # mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json # mobile/rpc-foundation/goldens/settings-workspace-context-refused.json # mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json # mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json # mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json # mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json # mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json # mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json # mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json # mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json # mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json # mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json # mobile/rpc-foundation/goldens/speech-setup-sheet-denied-to-mobile.json # mobile/rpc-foundation/goldens/speech-setup-sheet-fulfilled.json # mobile/rpc-foundation/goldens/speech-setup-sheet-legacy-desktop.json # mobile/rpc-foundation/goldens/speech-setup-sheet-model-vocabulary.json # mobile/rpc-foundation/goldens/structured-agent-session-created.json # mobile/rpc-foundation/goldens/structured-launch-created.json # mobile/rpc-foundation/goldens/structured-launch-definitive-refusal.json # mobile/rpc-foundation/goldens/structured-launch-replays-dropped-create.json # mobile/rpc-foundation/goldens/structured-launch-support-refused.json # mobile/rpc-foundation/goldens/structured-launch-unsupported.json # mobile/rpc-foundation/goldens/tasks-route-repo-list.json # mobile/rpc-foundation/goldens/terminal-gesture-flush-and-clear.json # mobile/rpc-foundation/goldens/terminal-input-send-accepted.json # mobile/rpc-foundation/goldens/terminal-input-send-refused.json # mobile/rpc-foundation/goldens/terminal-live-input-accepted.json # mobile/rpc-foundation/goldens/terminal-paste-accepted.json # mobile/rpc-foundation/goldens/terminal-paste-refused.json # mobile/rpc-foundation/goldens/terminal-query-reply-accepted.json # mobile/rpc-foundation/goldens/terminal-query-reply-unsubscribed.json # mobile/rpc-foundation/goldens/terminal-raw-input-refused.json # mobile/rpc-foundation/goldens/terminal-raw-input-reported.json # mobile/rpc-foundation/goldens/terminal-takeover-report-accepted.json # mobile/rpc-foundation/goldens/terminal-takeover-report-retried.json # mobile/rpc-foundation/goldens/terminal-viewport-refit-applied.json # mobile/rpc-foundation/goldens/terminal-viewport-refit-legacy-desktop.json # mobile/rpc-foundation/goldens/terminal-worktree-connection-resolved.json # mobile/rpc-foundation/goldens/tk-create-github.json # mobile/rpc-foundation/goldens/tk-create-gitlab.json # mobile/rpc-foundation/goldens/tk-create-linear.json # mobile/rpc-foundation/goldens/tk-item-checks-files.json # mobile/rpc-foundation/goldens/tk-item-comment-github.json # mobile/rpc-foundation/goldens/tk-item-comment-gitlab-mr.json # mobile/rpc-foundation/goldens/tk-item-comment-gitlab.json # mobile/rpc-foundation/goldens/tk-item-detail-github-reactions.json # mobile/rpc-foundation/goldens/tk-item-detail-github.json # mobile/rpc-foundation/goldens/tk-item-detail-gitlab-reactions.json # mobile/rpc-foundation/goldens/tk-item-detail-gitlab.json # mobile/rpc-foundation/goldens/tk-item-detail-linear.json # mobile/rpc-foundation/goldens/tk-item-detail-metadata.json # mobile/rpc-foundation/goldens/tk-item-merge-gitlab.json # mobile/rpc-foundation/goldens/tk-item-metadata-github.json # mobile/rpc-foundation/goldens/tk-item-metadata-gitlab-mr.json # mobile/rpc-foundation/goldens/tk-item-metadata-gitlab.json # mobile/rpc-foundation/goldens/tk-item-reply-merge.json # mobile/rpc-foundation/goldens/tk-item-review-github.json # mobile/rpc-foundation/goldens/tk-item-status-gitlab-mr.json # mobile/rpc-foundation/goldens/tk-item-status-gitlab.json # mobile/rpc-foundation/goldens/tk-linear-connect.json # mobile/rpc-foundation/goldens/tk-linear-item.json # mobile/rpc-foundation/goldens/tk-linear-team-context.json # mobile/rpc-foundation/goldens/tk-list-gitlab-items.json # mobile/rpc-foundation/goldens/tk-list-gitlab-todos.json # mobile/rpc-foundation/goldens/tk-list-linear.json # mobile/rpc-foundation/goldens/tk-project-board-load.json # mobile/rpc-foundation/goldens/tk-project-repo-slugs.json # mobile/rpc-foundation/goldens/tk-project-row-comments-issue.json # mobile/rpc-foundation/goldens/tk-project-row-comments-pr.json # mobile/rpc-foundation/goldens/tk-project-row-detail.json # mobile/rpc-foundation/goldens/tk-project-row-fields.json # mobile/rpc-foundation/goldens/tk-project-row-files-merge.json # mobile/rpc-foundation/goldens/tk-project-row-metadata-load.json # mobile/rpc-foundation/goldens/tk-project-row-review-checks.json # mobile/rpc-foundation/goldens/tk-project-row-threads.json # mobile/rpc-foundation/goldens/tk-provider-load.json # mobile/rpc-foundation/goldens/transport-capability-probe-cutover-reasks-fast.json # mobile/rpc-foundation/goldens/transport-capability-probe-non-string-capabilities-drop.json # mobile/rpc-foundation/goldens/transport-capability-probe-publishes.json # mobile/rpc-foundation/goldens/transport-capability-probe-refused-backs-off.json # mobile/rpc-foundation/goldens/transport-host-status-gates-drop-keeps-capabilities.json # mobile/rpc-foundation/goldens/transport-host-status-gates-ready.json # mobile/rpc-foundation/goldens/transport-host-status-gates-refused-degrades.json # mobile/rpc-foundation/goldens/transport-pairing-race-both-refused.json # mobile/rpc-foundation/goldens/transport-pairing-race-direct-completes-first.json # mobile/rpc-foundation/goldens/transport-pairing-race-relay-completes-first.json # mobile/rpc-foundation/goldens/transport-pairing-race-relay-wins-when-direct-refused.json # mobile/rpc-foundation/goldens/tw-capabilities-advertised.json # mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json # mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json # mobile/rpc-foundation/goldens/tw-create-retry-agent-launched.json # mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json # mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json # mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json # mobile/rpc-foundation/goldens/tw-create-retry-created.json # mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json # mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json # mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json # mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json # mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json # mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json # mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json # mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json # mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json # mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json # mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json # mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json # mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json # mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json # mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json # mobile/rpc-foundation/goldens/tw-workspace-source-presets.json # mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json # mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json # mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json # mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json # mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json # mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json # mobile/rpc-foundation/goldens/worktree-catalog-snapshot-unreadable.json # mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json # mobile/rpc-foundation/goldens/worktree-home-catalog.json # mobile/rpc-foundation/goldens/worktree-retired-names.json # mobile/rpc-foundation/pilot-scenarios.json
This commit is contained in:
@@ -2,6 +2,10 @@ name: Install Node dependencies
|
||||
description: Installs the Node toolchain and repository dependencies for CI jobs, with optional Electron archive caching.
|
||||
|
||||
inputs:
|
||||
cache-pnpm-verification:
|
||||
description: Restore pnpm's policy-checked lockfile verification record on Linux.
|
||||
required: false
|
||||
default: 'true'
|
||||
native-runtime:
|
||||
description: Native runtime to prepare after the script-free install (none, node, or electron).
|
||||
required: false
|
||||
@@ -24,6 +28,15 @@ inputs:
|
||||
default: 'false'
|
||||
|
||||
outputs:
|
||||
verification-cache-hit:
|
||||
description: Whether pnpm's verification record was restored.
|
||||
value: ${{ steps.verification-cache-restore.outputs.cache-hit }}
|
||||
verification-cache-path:
|
||||
description: The small pnpm-owned verification record, without registry metadata.
|
||||
value: ${{ steps.verification-cache.outputs.path }}
|
||||
verification-cache-key:
|
||||
description: Exact verification record key, also used by the isolated PR benchmark.
|
||||
value: ${{ steps.verification-cache.outputs.key }}
|
||||
node-version:
|
||||
description: Resolved Node.js version used for the install.
|
||||
value: ${{ steps.requested-node.outputs.node-version || steps.default-node.outputs.node-version }}
|
||||
@@ -86,6 +99,25 @@ runs:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
|
||||
|
||||
- name: Resolve pnpm verification cache
|
||||
id: verification-cache
|
||||
if: runner.os == 'Linux' && inputs.cache-pnpm-verification == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
POLICY_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc') }}
|
||||
run: |
|
||||
printf 'path=%s/lockfile-verified.jsonl\n' "$(pnpm cache path)" >> "$GITHUB_OUTPUT"
|
||||
printf 'key=pnpm-verification-v1-%s-%s-%s-%s\n' "$RUNNER_OS" "$RUNNER_ARCH" "$(pnpm --version)" "$POLICY_HASH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm verification record
|
||||
id: verification-cache-restore
|
||||
if: steps.verification-cache.outputs.key != ''
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.verification-cache.outputs.path }}
|
||||
key: ${{ steps.verification-cache.outputs.key }}
|
||||
|
||||
- name: Validate native runtime
|
||||
shell: bash
|
||||
env:
|
||||
@@ -120,6 +152,15 @@ runs:
|
||||
git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
|
||||
fi
|
||||
|
||||
# pnpm checks the cached record's policy and validity; never bypass verification.
|
||||
- name: Save pnpm verification record on main
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.verification-cache.outputs.key != '' && steps.verification-cache-restore.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ steps.verification-cache.outputs.path }}
|
||||
key: ${{ steps.verification-cache.outputs.key }}
|
||||
|
||||
- name: Resolve Electron package cache
|
||||
id: electron-package-cache
|
||||
if: inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'
|
||||
@@ -140,12 +181,19 @@ runs:
|
||||
printf 'version=%s\n' "$(node -p "require('./node_modules/electron/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Electron package archive
|
||||
if: inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'
|
||||
if: (github.event_name != 'pull_request' || runner.os != 'Linux') && steps.electron-package-cache.outputs.version != ''
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.electron-package-cache.outputs.cache-root }}
|
||||
key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }}
|
||||
|
||||
- name: Restore Electron package archive without saving
|
||||
if: github.event_name == 'pull_request' && runner.os == 'Linux' && steps.electron-package-cache.outputs.version != ''
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.electron-package-cache.outputs.cache-root }}
|
||||
key: electron-package-${{ runner.os }}-${{ runner.arch }}-${{ steps.electron-package-cache.outputs.version }}
|
||||
|
||||
# Why cached: `--ignore-scripts` leaves node-pty without build/Release, so
|
||||
# ensure-native-runtime node-gyp-compiles it in every job that asks for a runtime.
|
||||
# The artifacts are ABI-bound, so the key carries the target runtime, the resolved
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Restore xterm build dependencies
|
||||
description: Reuse installed dependencies for the pinned upstream checkout.
|
||||
outputs:
|
||||
cache-key:
|
||||
description: Exact dependency cache key.
|
||||
value: ${{ steps.restore.outputs.cache-primary-key }}
|
||||
cache-hit:
|
||||
description: Whether the exact installed dependency cache was restored.
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: runtime
|
||||
shell: bash
|
||||
run: |
|
||||
. /etc/os-release
|
||||
echo "image=$ID-$VERSION_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "node=$(node --version)" >> "$GITHUB_OUTPUT"
|
||||
- id: restore
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/node_modules
|
||||
key: xterm-dependencies-v1-${{ runner.os }}-${{ steps.runtime.outputs.image }}-${{ runner.arch }}-${{ steps.runtime.outputs.node }}-${{ hashFiles('config/patches/xterm-upstream.json', 'config/scripts/regenerate-xterm-patches.mjs', 'config/scripts/xterm-patch-text.mjs', '.github/actions/prepare-xterm-dependencies/action.yml') }}
|
||||
- name: Restore download cache on an installed-dependency miss
|
||||
if: steps.restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
||||
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
|
||||
@@ -2,6 +2,7 @@ name: Bun profile persistence
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'config/**'
|
||||
@@ -17,6 +18,8 @@ on:
|
||||
- '.github/actions/install-node-dependencies/**'
|
||||
- '.github/workflows/bun-profile-tests.yml'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 11 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -32,6 +35,8 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
should_run: ${{ steps.scope.outputs.should_run }}
|
||||
qualification: ${{ steps.scope.outputs.qualification }}
|
||||
runners: ${{ steps.scope.outputs.runners }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -56,7 +61,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-2022, windows-11-arm]
|
||||
os: ${{ fromJSON(needs.changes.outputs.runners || '["ubuntu-22.04","ubuntu-24.04-arm","macos-14","macos-15-intel","windows-2022","windows-11-arm"]') }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
@@ -82,9 +87,12 @@ jobs:
|
||||
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018
|
||||
|
||||
linux_glibc_floor:
|
||||
needs: changes
|
||||
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
|
||||
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
|
||||
needs: [changes, persistence]
|
||||
# A failed smoke already blocks qualification; missing scope still selects every platform.
|
||||
if: >-
|
||||
${{ !cancelled() && needs.persistence.result == 'success' &&
|
||||
needs.changes.outputs.should_run != 'false' &&
|
||||
needs.changes.outputs.qualification != 'false' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -107,9 +115,12 @@ jobs:
|
||||
- run: pnpm test:bun:profile --artifact
|
||||
|
||||
linux_musl:
|
||||
needs: changes
|
||||
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
|
||||
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
|
||||
needs: [changes, persistence]
|
||||
# A failed smoke already blocks qualification; missing scope still selects every platform.
|
||||
if: >-
|
||||
${{ !cancelled() && needs.persistence.result == 'success' &&
|
||||
needs.changes.outputs.should_run != 'false' &&
|
||||
needs.changes.outputs.qualification != 'false' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -54,6 +54,10 @@ jobs:
|
||||
with:
|
||||
native-runtime: node
|
||||
node-version: '24'
|
||||
cache-electron-package: 'true'
|
||||
|
||||
- name: Populate shared Electron archive
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
|
||||
- uses: ./.github/actions/prepare-git-compatibility
|
||||
|
||||
@@ -83,6 +87,9 @@ jobs:
|
||||
with:
|
||||
native-runtime: node
|
||||
node-version: '24'
|
||||
cache-electron-package: 'true'
|
||||
- name: Populate shared Electron archive
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
- name: Verify native cache is usable
|
||||
run: node config/scripts/ensure-native-runtime.mjs --check-only
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Clean closed PR caches
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
clean:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# No checkout: this runs trusted default-branch code, including for fork PRs.
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const ref = `refs/pull/${context.payload.pull_request.number}/merge`
|
||||
const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
|
||||
...context.repo, ref, per_page: 100
|
||||
})
|
||||
for (const cache of caches) {
|
||||
if (cache.ref !== ref) throw new Error(`Unexpected cache ref: ${cache.ref}`)
|
||||
try {
|
||||
await github.rest.actions.deleteActionsCacheById({
|
||||
...context.repo, cache_id: cache.id
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error
|
||||
}
|
||||
}
|
||||
core.info(`Removed ${caches.length} caches scoped to ${ref}`)
|
||||
@@ -0,0 +1,65 @@
|
||||
name: pnpm verification cache pilot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths: ['.github/workflows/ci-pnpm-verification-pilot.yml']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pnpm-verification-pilot-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
|
||||
jobs:
|
||||
seed:
|
||||
strategy:
|
||||
matrix:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
id: deps
|
||||
# PR caches are scoped to the PR merge ref; main never restores them.
|
||||
- uses: actions/cache/save@v5
|
||||
if: steps.deps.outputs.verification-cache-hit != 'true'
|
||||
with:
|
||||
path: ${{ steps.deps.outputs.verification-cache-path }}
|
||||
key: ${{ steps.deps.outputs.verification-cache-key }}
|
||||
|
||||
measure:
|
||||
needs: seed
|
||||
name: measure ${{ matrix.runner }} sample ${{ matrix.sample }} cache ${{ matrix.cache }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
sample: [1, 2, 3]
|
||||
cache: ['false', 'true']
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
id: deps
|
||||
with:
|
||||
cache-pnpm-verification: ${{ matrix.cache }}
|
||||
- name: Validate treatment and frozen install
|
||||
env:
|
||||
CACHE_ENABLED: ${{ matrix.cache }}
|
||||
CACHE_HIT: ${{ steps.deps.outputs.verification-cache-hit }}
|
||||
run: |
|
||||
if [ "$CACHE_ENABLED" = true ]; then test "$CACHE_HIT" = true; fi
|
||||
git diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml
|
||||
node -e "require.resolve('vitest'); require.resolve('electron')"
|
||||
@@ -0,0 +1,33 @@
|
||||
name: CI runner demand
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '23 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: ci-runner-demand
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
report:
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: config/scripts
|
||||
persist-credentials: false
|
||||
- name: Measure the previous complete 24 hours
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: node config/scripts/ci-runner-demand.mjs
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ci-runner-demand-${{ github.run_id }}
|
||||
path: ci-demand/
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Shared xterm dependency cache
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/ci-xterm-cache.yml'
|
||||
- '.github/actions/prepare-xterm-dependencies/**'
|
||||
- 'config/patches/xterm-upstream.json'
|
||||
- 'config/scripts/regenerate-xterm-patches.mjs'
|
||||
- 'config/scripts/xterm-patch-text.mjs'
|
||||
- 'package.json'
|
||||
# Refresh after Node patch releases or cache eviction without rebuilding on hits.
|
||||
schedule:
|
||||
- cron: '17 5 * * *'
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: xterm-cache-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
jobs:
|
||||
seed:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
package-manager-cache: false
|
||||
- uses: ./.github/actions/prepare-xterm-dependencies
|
||||
id: cache
|
||||
- name: Verify and populate dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$RUNNER_TEMP/xterm-patch-build"
|
||||
- uses: actions/cache/save@v5
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/node_modules
|
||||
key: ${{ steps.cache.outputs.cache-key }}
|
||||
- name: Preserve fallback downloads
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
||||
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
|
||||
@@ -32,9 +32,8 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
# Why: GitHub cron uses UTC; these slots map to 10am and 3pm
|
||||
# America/Phoenix for the default-branch E2E run.
|
||||
- cron: '0 17,22 * * *'
|
||||
# One complete daily reference run; targeted PR coverage remains unchanged.
|
||||
- cron: '0 17 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -201,7 +200,14 @@ jobs:
|
||||
node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards/assignment.json ci-shards/selected-discovery.json
|
||||
|
||||
- name: Run E2E tests (${{ matrix.shard_name }})
|
||||
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE: ci-shards/results.json
|
||||
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt --reporter=list,json
|
||||
|
||||
- name: Summarize E2E failures
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
run: node config/scripts/ci-e2e-failure-summary.mjs ci-shards/results.json
|
||||
|
||||
- name: Upload E2E shard assignment
|
||||
if: always()
|
||||
|
||||
@@ -61,7 +61,10 @@ concurrency:
|
||||
jobs:
|
||||
verify:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: 209s of this job is Vitest and nothing here needs x86: no Android SDK, emulator, gradle,
|
||||
# Hermes or Watchman, no docker, and no artifacts. The Gemfile.lock lists the generic `ruby`
|
||||
# platform, so frozen bundler installs without an aarch64-linux entry.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
env:
|
||||
# Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts
|
||||
@@ -177,7 +180,8 @@ jobs:
|
||||
|
||||
recording-pin:
|
||||
name: RPC recording pin
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: pure Node plus git; the golden comparison masks `platform`.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
||||
+73
-47
@@ -1,5 +1,5 @@
|
||||
name: PR Checks
|
||||
run-name: 'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
|
||||
run-name: "PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -59,12 +59,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why blob:none: full history is needed for the merge-base diff, but historical
|
||||
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
||||
# few this job actually reads on demand.
|
||||
fetch-depth: 0
|
||||
# Why depth 50 and not 0: every diff below resolves to HEAD^1, so only the merge commit
|
||||
# and a little slack are needed. Fetching all 8127 refs' commit graph cost ~20s here and
|
||||
# is charged to the start of all 22 jobs, since they all need this one.
|
||||
# Why blob:none stays: the sparse tree below is ~7 files, so there are no blobs to
|
||||
# materialize and no promisor refetch. Measured 9.5s -> 1.6s against 20.7s today.
|
||||
fetch-depth: 50
|
||||
filter: blob:none
|
||||
sparse-checkout: |
|
||||
/config/scripts/git-pull-request-diff-base.mjs
|
||||
/package.json
|
||||
/README.md
|
||||
/docs/readme/
|
||||
@@ -79,8 +82,9 @@ jobs:
|
||||
- name: Reject new root-level files and folders
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: node .github/scripts/check-root-directory-entries.mjs "$BASE_SHA" "$HEAD_SHA"
|
||||
run: |
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
node .github/scripts/check-root-directory-entries.mjs "$DIFF_BASE" HEAD
|
||||
|
||||
# The full Git index retains link targets outside the sparse working tree.
|
||||
- name: Check README local links
|
||||
@@ -99,12 +103,15 @@ jobs:
|
||||
id: filter
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why HEAD^1 and not --merge-base: HEAD is the pull request merge commit, so its first
|
||||
# parent is the base side already. Computing a merge base instead would require the
|
||||
# payload base SHA to be in the graph, which is what forced a full-history checkout.
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
# Why --no-renames: name-only rename detection can report only the destination.
|
||||
# A code file moved under docs/ must still expose its code-side deletion.
|
||||
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE_SHA" "$HEAD_SHA")"
|
||||
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
|
||||
echo "Changed paths:"
|
||||
printf '%s\n' "$CHANGED"
|
||||
printf '%s\n' "$CHANGED" | node config/scripts/pr-code-change-scope.mjs | tee -a "$GITHUB_OUTPUT"
|
||||
@@ -116,8 +123,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
CHANGED="$(git diff --name-only --diff-filter=AMCR --merge-base "$BASE" "$HEAD")"
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE")"
|
||||
CHANGED="$(git diff --name-only --diff-filter=AMCR "$DIFF_BASE" HEAD)"
|
||||
# Source routes are executable contracts so a test can prove exact
|
||||
# authorities, exclusions, and sentinels without evaluating workflow shell.
|
||||
TEST_FILES_JSON="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs)"
|
||||
@@ -131,7 +138,7 @@ jobs:
|
||||
# trigger on IME source rather than on a spec name in some route's list.
|
||||
NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)"
|
||||
echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR --merge-base "$BASE" "$HEAD")"
|
||||
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
|
||||
WSL_SOURCE_CHANGED="$(printf '%s\n' "$WSL_CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --wsl-source)"
|
||||
echo "wsl_source_changed=$WSL_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED"
|
||||
@@ -148,20 +155,35 @@ jobs:
|
||||
name: static analysis
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: measured 128s against 172s on ubuntu-latest, with every compute step faster --
|
||||
# type-aware 24s->15s, anti-slop 28->19s, localization extraction 67->46s, the orcad smoke
|
||||
# 39->14s. Both lint engines ship linux-arm64 and the Bun target follows process.arch, so
|
||||
# the whole toolchain resolves. Free for public repositories, same as the typecheck job.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why blob:none: full history is needed for the merge-base diff, but historical
|
||||
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
||||
# few this job actually reads on demand.
|
||||
fetch-depth: 0
|
||||
filter: blob:none
|
||||
# Why depth 50: the gates below diff against HEAD^1, so no merge base is computed and
|
||||
# the payload base SHA need not be in the graph.
|
||||
# Why no blob:none here, unlike code_paths: this job checks out all 30,226 files, and
|
||||
# the filter then forces a second promisor fetch of nearly every blob. Measured 23s
|
||||
# blobless against ~11s without it.
|
||||
fetch-depth: 50
|
||||
persist-credentials: false
|
||||
|
||||
# Why two guarded installs: the mixed root+mobile store entry is 537 MB against
|
||||
# 321 MB for root alone, and restoring it costs 8.6s against 4.6s. Most PRs skip the
|
||||
# mobile install below, so they were paying 216 MB for packages they never link. The
|
||||
# root-only key is also the one the hourly warmer reseeds. Only one of these runs.
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
if: needs.code_paths.outputs.mobile_dependencies != 'true'
|
||||
with:
|
||||
native-runtime: node
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
if: needs.code_paths.outputs.mobile_dependencies == 'true'
|
||||
with:
|
||||
native-runtime: node
|
||||
cache-dependency-path: |
|
||||
@@ -218,9 +240,9 @@ jobs:
|
||||
- name: Check VM runtime rollback compatibility
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
if git diff --quiet --merge-base "$BASE_SHA" "$HEAD_SHA" -- \
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
if git diff --quiet "$DIFF_BASE" HEAD -- \
|
||||
src/shared/ephemeral-vm-runtime-store.ts \
|
||||
src/shared/ephemeral-vm-runtime-feature-store.ts \
|
||||
src/shared/ephemeral-vm-runtime-rollback-projection.ts \
|
||||
@@ -252,10 +274,10 @@ jobs:
|
||||
background: true
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Detection failures run the full check; renames retain the removed input path.
|
||||
if git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA" > "$RUNNER_TEMP/localization-changes" &&
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" &&
|
||||
scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then
|
||||
echo "Localization extraction inputs are unchanged."
|
||||
else
|
||||
@@ -268,11 +290,11 @@ jobs:
|
||||
- name: Boot orcad and round-trip a terminal
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
run: |
|
||||
# Detection failures run the smoke; renames retain the removed input path.
|
||||
if git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA" > "$RUNNER_TEMP/orcad-smoke-changes" &&
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/orcad-smoke-changes" &&
|
||||
scope="$(node config/scripts/orcad-terminal-smoke-change-scope.mjs "$RUNNER_TEMP/orcad-smoke-changes")" && [ "$scope" = false ]; then
|
||||
echo "Orcad terminal smoke inputs are unchanged."
|
||||
else
|
||||
@@ -413,7 +435,8 @@ jobs:
|
||||
name: Codex index-heal contract
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.codex_index_heal_contract == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: @openai/codex ships @openai/codex-linux-arm64.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
env:
|
||||
CODEX_CLI_VERSION: '0.150.1'
|
||||
|
||||
@@ -448,7 +471,10 @@ jobs:
|
||||
name: xterm patch sync
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.xterm_patch_sync == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: the patch check rebuilds 4 packages x 2 builds and byte-compares against the
|
||||
# checked-in bundles. Those were generated on Linux x64 and reproduce byte-for-byte on
|
||||
# darwin-arm64, so the output is neither host-arch nor host-OS dependent.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -456,21 +482,14 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
# Why: the check rebuilds every package in the manifest from a pinned upstream
|
||||
# commit — @xterm/xterm and its three addons, each built twice (once unmodified to
|
||||
# prove the toolchain still reproduces the published bundles, once patched). Caching
|
||||
# the npm metadata and the shallow clone keeps the repeated cost to the builds
|
||||
# themselves; the key is the manifest, so a commit, package or toolchain bump
|
||||
# invalidates it.
|
||||
- name: Restore upstream xterm build inputs
|
||||
uses: actions/cache@v5
|
||||
# The generator uses Node built-ins and installs its own upstream toolchain.
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
${{ runner.temp }}/xterm-patch-build/upstream/.git
|
||||
key: xterm-upstream-${{ hashFiles('config/patches/xterm-upstream.json') }}
|
||||
node-version-file: package.json
|
||||
package-manager-cache: false
|
||||
|
||||
# Rebuild both pristine and patched bundles; reuse only the pinned toolchain.
|
||||
- uses: ./.github/actions/prepare-xterm-dependencies
|
||||
|
||||
- name: Verify xterm patches match the pinned upstream build
|
||||
env:
|
||||
@@ -481,7 +500,8 @@ jobs:
|
||||
name: shell contracts
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.shell_contracts == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: fish 4.x is published for noble/arm64 and zsh is in the arm64 archive.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
# Why: this job's cost is almost entirely package download, and a stalled mirror has
|
||||
# no wall-clock bound of its own. A successful run finishes in ~4.5 minutes, so this
|
||||
# is generous; it exists so a wedge fails the job instead of holding the whole run
|
||||
@@ -620,16 +640,19 @@ jobs:
|
||||
node-version: '24'
|
||||
|
||||
test:
|
||||
needs: [code_paths, test_native_cache]
|
||||
needs: [code_paths, test_native_cache, static_analysis, typecheck]
|
||||
# Honor cancellation while allowing the optional native-cache primer to skip.
|
||||
if: >-
|
||||
!cancelled() &&
|
||||
needs.code_paths.outputs.test == 'true' &&
|
||||
needs.static_analysis.result == 'success' &&
|
||||
needs.typecheck.result == 'success' &&
|
||||
(needs.test_native_cache.result == 'success' || needs.test_native_cache.result == 'skipped')
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
with:
|
||||
node_versions: '["24"]'
|
||||
runner: ubuntu-24.04-arm
|
||||
selection_mode: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
|
||||
|
||||
# Why a separate job: the test needs a real Chrome, and the sharded `test` matrix
|
||||
# would pay for it on every shard to run one file in whichever shard it landed in.
|
||||
@@ -751,7 +774,8 @@ jobs:
|
||||
name: cross-version wire compatibility
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.cross-version-wire == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: source-only: tagged checkout plus in-process vitest, no docker or browser.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
# Why fetch-depth 0: the harness extracts the newest release tag to skew
|
||||
@@ -783,6 +807,7 @@ jobs:
|
||||
tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
|
||||
tests/e2e/cross-version-wire/cross-version-worktree-identity-downgrade.unit.test.ts
|
||||
tests/e2e/cross-version-wire/cross-version-session-tabs-retirement-proof.unit.test.ts
|
||||
tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts
|
||||
tests/e2e/cross-version-wire/agent-session-unproven-release-downgrade.unit.test.ts
|
||||
tests/e2e/cross-version-wire/cross-version-worktree-ps-verdict.unit.test.ts
|
||||
|
||||
@@ -790,7 +815,8 @@ jobs:
|
||||
name: managed hooks on Node 18
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.managed_hook_node18 == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Why ARM: Node 18 publishes linux-arm64; the per-platform runtime files are read as data.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -813,7 +839,7 @@ jobs:
|
||||
|
||||
package:
|
||||
name: package
|
||||
needs: [code_paths]
|
||||
needs: [code_paths, static_analysis, typecheck]
|
||||
if: needs.code_paths.outputs.package == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Let the serial Docker gates reach their own deadlines and report cleanup failures.
|
||||
@@ -826,7 +852,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ~/.cache/electron-builder
|
||||
key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@@ -961,7 +987,7 @@ jobs:
|
||||
|
||||
package_windows:
|
||||
name: package (windows)
|
||||
needs: [code_paths]
|
||||
needs: [code_paths, static_analysis, typecheck]
|
||||
if: needs.code_paths.outputs.package_windows == 'true'
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 30
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED
|
||||
# Explicit review identities share workflow concurrency; legacy names use ordered cancellation.
|
||||
name: Pullfrog
|
||||
run-name: ${{ inputs.name || github.workflow }}
|
||||
run-name: ${{ inputs.name || github.workflow }}${{ inputs.pull_request_number && format(' | PR {0}', inputs.pull_request_number) || '' }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -11,21 +11,71 @@ on:
|
||||
type: string
|
||||
description: Run name
|
||||
|
||||
pull_request_number:
|
||||
type: string
|
||||
description: Optional PR identity for cancelling superseded reviews
|
||||
head_sha:
|
||||
type: string
|
||||
description: Optional expected PR head; stale reviews are skipped
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ inputs.pull_request_number && format('pullfrog-pr-{0}', inputs.pull_request_number) || format('pullfrog-run-{0}', github.run_id) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
review_scope:
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: write
|
||||
outputs:
|
||||
current: ${{ steps.scope.outputs.current }}
|
||||
number: ${{ steps.scope.outputs.number }}
|
||||
head: ${{ steps.scope.outputs.head }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: config/scripts/pullfrog-review-scope.cjs
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v8
|
||||
id: scope
|
||||
with:
|
||||
script: |
|
||||
const { reviewScope } = require('./config/scripts/pullfrog-review-scope.cjs')
|
||||
await reviewScope({ github, context, core })
|
||||
|
||||
pullfrog:
|
||||
needs: review_scope
|
||||
if: needs.review_scope.outputs.current == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Recheck review head before starting agent
|
||||
id: freshness
|
||||
if: needs.review_scope.outputs.number != ''
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
REVIEW_NUMBER: ${{ needs.review_scope.outputs.number }}
|
||||
REVIEW_HEAD: ${{ needs.review_scope.outputs.head }}
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: Number(process.env.REVIEW_NUMBER) })
|
||||
core.setOutput('current', pr.state === 'open' && pr.head.sha === process.env.REVIEW_HEAD)
|
||||
- name: Run agent
|
||||
if: needs.review_scope.outputs.number == '' || steps.freshness.outputs.current == 'true'
|
||||
uses: pullfrog/pullfrog@v0
|
||||
with:
|
||||
prompt: ${{ inputs.prompt }}
|
||||
|
||||
@@ -19,80 +19,18 @@ jobs:
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Why: release events run this file from the tagged commit, so load the module from the same commit.
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: config/scripts/release-policy.mjs
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- name: Enforce release policy
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const release = context.payload.release;
|
||||
const tag = release.tag_name;
|
||||
const author = release.author?.login;
|
||||
const number = "(?:0|[1-9][0-9]*)";
|
||||
const version = `${number}\\.${number}\\.${number}`;
|
||||
const stableTag = new RegExp(`^v${version}$`);
|
||||
const prereleaseTag = new RegExp(
|
||||
`^(?:v${version}-rc\\.${number}(?:\\.[0-9A-Za-z]+)?|mobile(?:-android)?-v${version})$`,
|
||||
);
|
||||
const expectedPrerelease = prereleaseTag.test(tag);
|
||||
const allowed = author === "github-actions[bot]" &&
|
||||
(stableTag.test(tag) || expectedPrerelease);
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
async function restoreLatestStable() {
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
const stable = releases
|
||||
.filter((candidate) =>
|
||||
!candidate.draft &&
|
||||
!candidate.prerelease &&
|
||||
candidate.author?.login === "github-actions[bot]" &&
|
||||
stableTag.test(candidate.tag_name),
|
||||
)
|
||||
.sort((left, right) => {
|
||||
const a = left.tag_name.slice(1).split(".").map(Number);
|
||||
const b = right.tag_name.slice(1).split(".").map(Number);
|
||||
return a.reduce((result, part, index) => result || part - b[index], 0);
|
||||
})
|
||||
.at(-1);
|
||||
if (stable) {
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: stable.id,
|
||||
make_latest: "true",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
if (release.prerelease !== expectedPrerelease) {
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: release.id,
|
||||
prerelease: expectedPrerelease,
|
||||
make_latest: expectedPrerelease ? "false" : "legacy",
|
||||
});
|
||||
}
|
||||
await restoreLatestStable();
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: release.id,
|
||||
draft: true,
|
||||
prerelease: true,
|
||||
make_latest: "false",
|
||||
});
|
||||
await restoreLatestStable();
|
||||
await github.rest.repos.deleteRelease({ owner, repo, release_id: release.id });
|
||||
try {
|
||||
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
}
|
||||
core.warning(`Deleted unauthorized release ${tag} created by ${author || "unknown"}.`);
|
||||
const { pathToFileURL } = await import("node:url")
|
||||
const { enforceReleasePolicy } = await import(
|
||||
pathToFileURL(`${process.env.GITHUB_WORKSPACE}/config/scripts/release-policy.mjs`).href
|
||||
)
|
||||
await enforceReleasePolicy({ github, context, core })
|
||||
|
||||
@@ -14,19 +14,48 @@ on:
|
||||
default: ubuntu-latest
|
||||
type: string
|
||||
|
||||
selection_mode:
|
||||
description: Shadow validates selection; selected applies it only to draft PRs.
|
||||
required: false
|
||||
default: shadow
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
- name: Plan unit selection
|
||||
id: plan
|
||||
env:
|
||||
ORCA_UNIT_SELECTION_MODE: ${{ inputs.selection_mode }}
|
||||
run: node config/scripts/ci-unit-plan.mjs
|
||||
- uses: actions/upload-artifact@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: unit-selection-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards/unit-selection.json
|
||||
retention-days: 14
|
||||
|
||||
test:
|
||||
name: tests node ${{ matrix.node }} ${{ matrix.shard }}/${{ matrix.shard_total }}
|
||||
needs: plan
|
||||
name: tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }}
|
||||
runs-on: ${{ inputs.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ${{ fromJSON(inputs.node_versions) }}
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shard_total: [8]
|
||||
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -43,6 +72,12 @@ jobs:
|
||||
- name: Install Electron package binary for tests
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: unit-selection-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards/
|
||||
|
||||
- name: Test shard
|
||||
env:
|
||||
ORCA_BALANCE_UNIT_SHARDS: '1'
|
||||
@@ -50,27 +85,7 @@ jobs:
|
||||
run: |
|
||||
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
|
||||
--exclude=src/main/daemon/shell-ready.test.ts \
|
||||
--exclude=src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
|
||||
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
--exclude=src/main/pty/omp-shell-wrapper-alias-safety.test.ts \
|
||||
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
--exclude=src/main/shell-startup-feature-channel.test.ts \
|
||||
--exclude=src/main/terminal-history-fish-session.node-pty.test.ts \
|
||||
--exclude=src/main/zsh-scoped-histfile.live-shell.test.ts \
|
||||
--exclude=src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \
|
||||
--exclude=src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
|
||||
--exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
||||
--exclude=src/shared/fish-query-reply-child-stdin.node-pty.test.ts \
|
||||
--exclude=src/shared/pty-reply-echo-shapes.node-pty.test.ts \
|
||||
--exclude=src/shared/startup-shell-portability.live-shell.test.ts \
|
||||
--exclude=src/shared/posix-command-path-lookup.test.ts \
|
||||
--exclude=tests/e2e/relay-region-compatibility.unit.test.ts \
|
||||
--exclude=tests/e2e/relay-region-correction.unit.test.ts \
|
||||
--exclude=tests/e2e/cross-version-wire/** \
|
||||
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
|
||||
--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }}
|
||||
|
||||
- name: Upload unit shard assignment
|
||||
if: always()
|
||||
@@ -78,11 +93,40 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
|
||||
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
selection_evidence:
|
||||
needs: test
|
||||
if: ${{ !cancelled() }}
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: config/scripts/ci-unit-selection-review.mjs
|
||||
sparse-checkout-cone-mode: false
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: unit-shard-node-*-attempt-${{ github.run_attempt }}
|
||||
path: unit-evidence/
|
||||
- name: Compare selection with full results
|
||||
continue-on-error: true
|
||||
run: node config/scripts/ci-unit-selection-review.mjs unit-evidence
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: unit-selection-review-attempt-${{ github.run_attempt }}
|
||||
path: unit-evidence/selection-review.json
|
||||
retention-days: 30
|
||||
|
||||
relay_integration:
|
||||
name: relay integration node ${{ matrix.node }}
|
||||
strategy:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.
|
||||
|
||||
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
@@ -179,6 +179,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="MiMo Code logo" width="16" valign="middle" /> MiMo Code</kbd></a>
|
||||
@@ -195,6 +196,7 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca.
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com"><kbd><img src="src/shared/agent-icons/freebuff.png" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="docs/assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a>
|
||||
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
|
||||
|
||||
Pair with your desktop app to monitor and steer your agents from your phone.
|
||||
|
||||
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA)
|
||||
- **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
|
||||
- **Android:** [Download APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Install guide](https://www.onorca.dev/docs/android-apk)
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,8 @@
|
||||
const release = require('./electron-builder.config.cjs')
|
||||
|
||||
// CI discards these packages after smoke tests; keep release compression unchanged.
|
||||
module.exports = {
|
||||
...release,
|
||||
deb: { ...release.deb, fpm: [...(release.deb.fpm ?? []), '--deb-compression-level=1'] },
|
||||
rpm: { ...release.rpm, fpm: [...(release.rpm.fpm ?? []), '--rpm-compression-level=1'] }
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ORCAD_ENTRY_POINT
|
||||
} from './orcad-entry-build.mjs'
|
||||
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
|
||||
import { bunProfileQualification } from './bun-profile-qualification.mjs'
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, '../..')
|
||||
const BUILD_SCRIPTS = [
|
||||
@@ -29,7 +30,9 @@ const ALWAYS_FILES = new Set([
|
||||
'tsconfig.json',
|
||||
'.github/workflows/bun-profile-tests.yml',
|
||||
'config/scripts/bun-profile-change-scope.mjs',
|
||||
'config/scripts/bun-profile-change-scope.test.mjs'
|
||||
'config/scripts/bun-profile-change-scope.test.mjs',
|
||||
'config/scripts/bun-profile-qualification.mjs',
|
||||
'config/scripts/bun-profile-qualification.test.mjs'
|
||||
])
|
||||
const ALWAYS_PREFIXES = [
|
||||
'.github/actions/install-node-dependencies/',
|
||||
@@ -110,7 +113,11 @@ export async function classifyBunProfileChanges(changedFiles, collect = collectB
|
||||
reason: matched ? `Runtime or test dependency changed: ${matched}` : 'No Bun inputs changed'
|
||||
}
|
||||
} catch (error) {
|
||||
return { shouldRun: true, reason: `Dependency graph unavailable: ${String(error)}` }
|
||||
return {
|
||||
shouldRun: true,
|
||||
graphUnavailable: true,
|
||||
reason: `Dependency graph unavailable: ${String(error)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +125,14 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
const changedFiles = readFileSync(process.argv[2], 'utf8').split('\0').filter(Boolean)
|
||||
const result = await classifyBunProfileChanges(changedFiles)
|
||||
console.log(result.reason)
|
||||
const output = `should_run=${String(result.shouldRun)}\n`
|
||||
let event = {}
|
||||
try {
|
||||
event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))
|
||||
} catch {
|
||||
// Missing event evidence retains full qualification.
|
||||
}
|
||||
const policy = bunProfileQualification(changedFiles, result, event)
|
||||
const output = `should_run=${result.shouldRun}\nqualification=${policy.qualification}\nrunners=${JSON.stringify(policy.runners)}\n`
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, output)
|
||||
} else {
|
||||
|
||||
@@ -78,6 +78,7 @@ it.each([
|
||||
'.npmrc',
|
||||
'tsconfig.json',
|
||||
'config/tsconfig.node.json',
|
||||
'config/scripts/bun-profile-qualification.mjs',
|
||||
'config/patches/node-pty@1.1.0.patch',
|
||||
'native/windows-registry/src/addon.cc',
|
||||
'.github/actions/install-node-dependencies/action.yml',
|
||||
@@ -140,12 +141,15 @@ it('keeps all ten platform jobs and runs them when detection is skipped or fails
|
||||
expect(workflow.jobs.changes.steps[0].with['persist-credentials']).toBe(false)
|
||||
const detect = workflow.jobs.changes.steps.find((step) => step.id === 'scope')
|
||||
expect(detect.run).toContain('git diff --name-only --no-renames -z HEAD^1 HEAD')
|
||||
let count = 0
|
||||
for (const jobName of ['persistence', 'linux_glibc_floor', 'linux_musl']) {
|
||||
expect(workflow.on.pull_request.types).toContain('ready_for_review')
|
||||
expect(workflow.on.schedule).toHaveLength(1)
|
||||
expect(workflow.jobs.persistence.strategy.matrix.os).toContain('needs.changes.outputs.runners')
|
||||
for (const jobName of ['linux_glibc_floor', 'linux_musl']) {
|
||||
const job = workflow.jobs[jobName]
|
||||
expect(job.needs).toBe('changes')
|
||||
expect(job.if).toBe("${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}")
|
||||
count += job.strategy.matrix.os.length
|
||||
expect(job.needs).toEqual(['changes', 'persistence'])
|
||||
expect(job.if).toContain("needs.persistence.result == 'success'")
|
||||
expect(job.if).toContain("needs.changes.outputs.qualification != 'false'")
|
||||
expect(job.if).toContain("needs.changes.outputs.should_run != 'false'")
|
||||
expect(job.strategy.matrix.os).toEqual(['ubuntu-22.04', 'ubuntu-24.04-arm'])
|
||||
}
|
||||
expect(count).toBe(10)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export const BUN_PERSISTENCE_RUNNERS = [
|
||||
'ubuntu-22.04',
|
||||
'ubuntu-24.04-arm',
|
||||
'macos-14',
|
||||
'macos-15-intel',
|
||||
'windows-2022',
|
||||
'windows-11-arm'
|
||||
]
|
||||
|
||||
const QUALIFICATION_PREFIXES = [
|
||||
'config/',
|
||||
'native/',
|
||||
'resources/',
|
||||
'.github/',
|
||||
'src/main/persistence/',
|
||||
'src/main/sqlite/',
|
||||
'src/main/orcad/',
|
||||
'src/main/providers/',
|
||||
'src/main/daemon/',
|
||||
'src/main/ssh/',
|
||||
'src/relay/',
|
||||
'src/shared/child-process/'
|
||||
]
|
||||
|
||||
export function bunProfileQualification(changedFiles, scope, event = {}) {
|
||||
const sensitive = changedFiles.some(
|
||||
(file) =>
|
||||
!file.includes('/') ||
|
||||
QUALIFICATION_PREFIXES.some((prefix) => file.startsWith(prefix)) ||
|
||||
/(?:^|[/.-])(?:windows|win32|wsl|macos|darwin|linux|posix|bun)(?:[/.-]|$)/i.test(file)
|
||||
)
|
||||
// Only a proven unrelated platform change in a draft may defer qualification.
|
||||
const full =
|
||||
event.pull_request?.draft !== true ||
|
||||
changedFiles.length === 0 ||
|
||||
scope.graphUnavailable === true ||
|
||||
sensitive
|
||||
return {
|
||||
qualification: full,
|
||||
runners: full ? BUN_PERSISTENCE_RUNNERS : ['ubuntu-22.04']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { BUN_PERSISTENCE_RUNNERS, bunProfileQualification } from './bun-profile-qualification.mjs'
|
||||
|
||||
const draft = { pull_request: { draft: true } }
|
||||
const scope = { shouldRun: true }
|
||||
|
||||
it('defers only platform qualification for ordinary draft runtime changes', () => {
|
||||
expect(
|
||||
bunProfileQualification(['src/main/runtime/rpc/methods/example.ts'], scope, draft)
|
||||
).toEqual({
|
||||
qualification: false,
|
||||
runners: ['ubuntu-22.04']
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'package.json',
|
||||
'native/windows-registry/src/addon.cc',
|
||||
'config/vitest.config.ts',
|
||||
'src/main/ssh/ssh-provider.ts',
|
||||
'src/main/providers/local-pty-provider.ts',
|
||||
'src/shared/child-process/run-process.ts',
|
||||
'src/main/persistence/profile-state/store.ts',
|
||||
'src/main/sqlite/database.ts',
|
||||
'src/main/orcad/entry.ts',
|
||||
'src/main/runtime/windows-terminal.ts',
|
||||
'src/shared/linux-glibc.ts',
|
||||
'src/main/daemon/entry.ts',
|
||||
'src/relay/index.ts',
|
||||
'src/main/wsl/runner.ts'
|
||||
])('retains all platforms for sensitive input %s', (file) => {
|
||||
expect(bunProfileQualification([file], scope, draft)).toEqual({
|
||||
qualification: true,
|
||||
runners: BUN_PERSISTENCE_RUNNERS
|
||||
})
|
||||
})
|
||||
|
||||
it('qualifies every ready commit, scheduled/manual runs and incomplete evidence', () => {
|
||||
const paths = ['src/main/runtime/rpc/methods/example.ts']
|
||||
for (const event of [{}, { pull_request: { draft: false } }]) {
|
||||
expect(bunProfileQualification(paths, scope, event).qualification).toBe(true)
|
||||
}
|
||||
expect(bunProfileQualification([], scope, draft).qualification).toBe(true)
|
||||
expect(
|
||||
bunProfileQualification(paths, { ...scope, graphUnavailable: true }, draft).qualification
|
||||
).toBe(true)
|
||||
})
|
||||
@@ -136,9 +136,14 @@ function resolveBase(root, requestedBase) {
|
||||
}
|
||||
|
||||
export function collectAddedLineRanges(root, requestedBase) {
|
||||
const base = resolveBase(root, requestedBase)
|
||||
const mergeBase = runGit(root, ['merge-base', base, 'HEAD']).trim()
|
||||
const comparisonBase = resolvePullRequestDiffBase(root, mergeBase)
|
||||
// On a pull_request checkout HEAD is the merge commit, so its first parent is the base side and
|
||||
// no merge base has to be computed. Resolving it first is what lets CI checkout shallowly: the
|
||||
// payload base SHA can lag HEAD^1 by any number of commits and need not be in the graph at all.
|
||||
// Off that ref (local runs) the requested base is an arbitrary branch tip, so the merge base is
|
||||
// still what isolates this branch's own lines.
|
||||
const comparisonBase =
|
||||
resolvePullRequestDiffBase(root, null) ??
|
||||
runGit(root, ['merge-base', resolveBase(root, requestedBase), 'HEAD']).trim()
|
||||
const changedFiles = splitNullDelimited(
|
||||
runGit(root, ['diff', '--name-only', '-z', '--diff-filter=ACMRTUB', comparisonBase, '--'])
|
||||
)
|
||||
@@ -174,7 +179,7 @@ export function collectAddedLineRanges(root, requestedBase) {
|
||||
const lineCount = readFileSync(absolutePath, 'utf8').split(/\r?\n/).length
|
||||
rangesByFile.set(file, [{ start: 1, end: lineCount }])
|
||||
}
|
||||
return { base, comparisonBase, rangesByFile }
|
||||
return { comparisonBase, rangesByFile }
|
||||
}
|
||||
|
||||
function parseOxlintOutput(stdout, label) {
|
||||
@@ -414,10 +419,12 @@ export function main(
|
||||
root = process.cwd(),
|
||||
requestedBase = process.argv.slice(2).find((argument) => argument !== '--')
|
||||
) {
|
||||
const { base, comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase)
|
||||
const { comparisonBase, rangesByFile } = collectAddedLineRanges(root, requestedBase)
|
||||
const files = [...rangesByFile.keys()]
|
||||
if (files.length === 0) {
|
||||
console.log(`Changed-code quality gate: no changed JavaScript or TypeScript since ${base}.`)
|
||||
console.log(
|
||||
`Changed-code quality gate: no changed JavaScript or TypeScript since ${comparisonBase.slice(0, 12)}.`
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -228,8 +228,14 @@ describe('root directory guard', () => {
|
||||
|
||||
expect(guardJob.if).toBeUndefined()
|
||||
expect(guardStep.if).toBeUndefined()
|
||||
expect(guardJob.steps[0].with['fetch-depth']).toBe(0)
|
||||
// Why >= 2 rather than 0: the guard compares the merge commit's first parent against the
|
||||
// merged tree, so it needs both parents present but no history beyond them. Depth 1 would
|
||||
// leave HEAD^1 unreachable and the guard would fail closed on every run.
|
||||
expect(guardJob.steps[0].with['fetch-depth']).toBeGreaterThanOrEqual(2)
|
||||
expect(guardStep.run).toContain('node .github/scripts/check-root-directory-entries.mjs')
|
||||
// The base side must come from the merge ref, not the event payload, or a shallow checkout
|
||||
// cannot resolve it.
|
||||
expect(guardStep.run).toContain('git-pull-request-diff-base.mjs')
|
||||
expect(workflow.jobs.verify.needs).toContain('code_paths')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('CI background step barriers', () => {
|
||||
it('joins package setup before reading outputs and preserves isolated native probes', () => {
|
||||
const steps = pr.jobs.package.steps
|
||||
// Parallel composites must not race to download their shared cache action on first use.
|
||||
const cacheAction = steps.findIndex((step) => step.uses === 'actions/cache@v5')
|
||||
const cacheAction = steps.findIndex((step) => step.uses === 'actions/cache/restore@v5')
|
||||
expect(cacheAction).toBeGreaterThanOrEqual(0)
|
||||
expect(cacheAction).toBeLessThan(
|
||||
steps.findIndex((step) => step.id === 'shutdown-fixture-cache')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { BUN_PERSISTENCE_RUNNERS } from './bun-profile-qualification.mjs'
|
||||
|
||||
const readWorkflow = (name) =>
|
||||
parse(readFileSync(new URL(`../../.github/workflows/${name}.yml`, import.meta.url), 'utf8'))
|
||||
@@ -15,7 +16,21 @@ it('warms the same Linux Node runtime the PR shards restore', () => {
|
||||
const primer = readWorkflow('pr').jobs.test_native_cache
|
||||
expect(arm['runs-on']).toBe(primer['runs-on'])
|
||||
expect(arm.steps.at(-1).run).toBe('node config/scripts/ensure-native-runtime.mjs --check-only')
|
||||
expect(install.with).toEqual(primer.steps.find((step) => step.uses === install.uses).with)
|
||||
expect(install.with).toMatchObject(primer.steps.find((step) => step.uses === install.uses).with)
|
||||
})
|
||||
|
||||
it('populates shared Electron archives on both Linux architectures without changing the Node ABI', () => {
|
||||
for (const name of ['warm', 'warm-linux-arm']) {
|
||||
const steps = workflow.jobs[name].steps
|
||||
const install = steps.find(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
expect(install.with['native-runtime']).toBe('node')
|
||||
expect(install.with['cache-electron-package']).toBe('true')
|
||||
const populate = steps.find((step) => step.name === 'Populate shared Electron archive')
|
||||
expect(populate.run).toBe('node config/scripts/install-electron-package-binary.mjs')
|
||||
expect(steps.indexOf(populate)).toBeGreaterThan(steps.indexOf(install))
|
||||
}
|
||||
})
|
||||
|
||||
it('publishes incremental state under a key and prefix that new PRs restore', () => {
|
||||
@@ -50,9 +65,8 @@ it('bounds warming to the required platforms and validates changes without grant
|
||||
|
||||
it('warms and probes both Windows images with the persistence job runtime', () => {
|
||||
const job = workflow.jobs['warm-windows']
|
||||
const persistence = readWorkflow('bun-profile-tests').jobs.persistence
|
||||
expect(job.strategy.matrix.os).toEqual(
|
||||
persistence.strategy.matrix.os.filter((os) => os.startsWith('windows-'))
|
||||
BUN_PERSISTENCE_RUNNERS.filter((os) => os.startsWith('windows-'))
|
||||
)
|
||||
expect(job['runs-on']).toBe('${{ matrix.os }}')
|
||||
expect(job.strategy['fail-fast']).toBe(false)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/ci-closed-pr-caches.yml', 'utf8'))
|
||||
const script = workflow.jobs.clean.steps[0].with.script
|
||||
const ref = 'refs/pull/123/merge'
|
||||
|
||||
function run(caches, remove = vi.fn().mockResolvedValue(undefined)) {
|
||||
const list = vi.fn()
|
||||
const paginate = vi.fn().mockResolvedValue(caches)
|
||||
const result = runInNewContext(`(async () => { ${script} })()`, {
|
||||
context: { repo: { owner: 'owner', repo: 'repo' }, payload: { pull_request: { number: 123 } } },
|
||||
github: {
|
||||
paginate,
|
||||
rest: { actions: { getActionsCacheList: list, deleteActionsCacheById: remove } }
|
||||
},
|
||||
core: { info: vi.fn() }
|
||||
})
|
||||
return { result, remove, paginate, list }
|
||||
}
|
||||
|
||||
it('uses default-branch code without checking out a closed PR', () => {
|
||||
expect(workflow.on).toEqual({ pull_request_target: { types: ['closed'] } })
|
||||
expect(workflow.permissions).toEqual({ actions: 'write' })
|
||||
expect(workflow.jobs.clean.steps).toHaveLength(1)
|
||||
expect(workflow.jobs.clean.steps[0].uses).toBe('actions/github-script@v8')
|
||||
})
|
||||
|
||||
it('lists and deletes only caches scoped to the closed merge ref', async () => {
|
||||
const { result, paginate, list, remove } = run([
|
||||
{ id: 12, ref },
|
||||
{ id: 13, ref }
|
||||
])
|
||||
await result
|
||||
expect(paginate).toHaveBeenCalledWith(list, { owner: 'owner', repo: 'repo', ref, per_page: 100 })
|
||||
expect(remove.mock.calls).toEqual([
|
||||
[{ owner: 'owner', repo: 'repo', cache_id: 12 }],
|
||||
[{ owner: 'owner', repo: 'repo', cache_id: 13 }]
|
||||
])
|
||||
})
|
||||
|
||||
it('refuses a default-branch cache even if the API returns one', async () => {
|
||||
const { result, remove } = run([{ id: 12, ref: 'refs/heads/main' }])
|
||||
await expect(result).rejects.toThrow('Unexpected cache ref')
|
||||
expect(remove).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('tolerates an eviction race but surfaces permission failures', async () => {
|
||||
await expect(
|
||||
run([{ id: 12, ref }], vi.fn().mockRejectedValue({ status: 404 })).result
|
||||
).resolves.toBeUndefined()
|
||||
await expect(
|
||||
run([{ id: 12, ref }], vi.fn().mockRejectedValue({ status: 403 })).result
|
||||
).rejects.toEqual({ status: 403 })
|
||||
})
|
||||
@@ -48,7 +48,11 @@ describe('CI dependency download caches', () => {
|
||||
action.runs.steps.findIndex((step) => step.name === 'Install dependencies')
|
||||
)
|
||||
const saves = action.runs.steps.filter((step) => step.uses === 'actions/cache/save@v5')
|
||||
expect(saves).toEqual([])
|
||||
expect(saves).toHaveLength(1)
|
||||
expect(saves[0].name).toBe('Save pnpm verification record on main')
|
||||
expect(saves[0].if).toContain("github.ref == 'refs/heads/main'")
|
||||
expect(saves[0].if).toContain("github.event_name != 'pull_request'")
|
||||
expect(saves[0].with.path).toBe('${{ steps.verification-cache.outputs.path }}')
|
||||
})
|
||||
|
||||
it('restores Windows packaging downloads from the release cache without a PR upload', () => {
|
||||
@@ -99,8 +103,8 @@ describe('CI dependency download caches', () => {
|
||||
expect(writer.with['restore-keys']).toBeUndefined()
|
||||
expect(writer.with['lookup-only']).toBe(true)
|
||||
expect(release.steps.indexOf(writer)).toBeGreaterThan(release.steps.indexOf(combined))
|
||||
// Retain PR fallback saves until a successful release seeds the default-branch entry.
|
||||
expect(consumer.uses).toBe('actions/cache@v5')
|
||||
expect(consumer.uses).toBe('actions/cache/restore@v5')
|
||||
expect(consumer.with['restore-keys'].trim()).toBe('electron-builder-linux-')
|
||||
expect(combined.uses).toBe('actions/cache@v5')
|
||||
expect(linux.eb_cache_path.trim().split('\n')).toEqual([
|
||||
'~/.cache/electron',
|
||||
@@ -109,6 +113,20 @@ describe('CI dependency download caches', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shares Electron archives with PRs without uploading PR-local copies', () => {
|
||||
const save = action.runs.steps.find((step) => step.name === 'Cache Electron package archive')
|
||||
const restore = action.runs.steps.find(
|
||||
(step) => step.name === 'Restore Electron package archive without saving'
|
||||
)
|
||||
expect(save.if).toContain("github.event_name != 'pull_request' || runner.os != 'Linux'")
|
||||
expect(restore.if).toContain("github.event_name == 'pull_request' && runner.os == 'Linux'")
|
||||
expect(save.if).toContain("steps.electron-package-cache.outputs.version != ''")
|
||||
expect(restore.if).toContain("steps.electron-package-cache.outputs.version != ''")
|
||||
expect(save.uses).toBe('actions/cache@v5')
|
||||
expect(restore.uses).toBe('actions/cache/restore@v5')
|
||||
expect(restore.with).toEqual(save.with)
|
||||
})
|
||||
|
||||
describe('release install targets', () => {
|
||||
const macCpuFlag = '--cpu=current,x64,arm64'
|
||||
// Both shapes: `run:` steps and steps wrapped in nick-fields/retry (`with.command`).
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { trackE2eFailures } from './ci-e2e-failure-tracking.mjs'
|
||||
|
||||
export function e2eFailureSummary(report) {
|
||||
const failures = []
|
||||
let passed = 0
|
||||
let skipped = 0
|
||||
function visit(suite, parents = [], parentFile) {
|
||||
const titles = [...parents, suite.title].filter(Boolean)
|
||||
const file = suite.file ?? parentFile
|
||||
for (const spec of suite.specs ?? []) {
|
||||
for (const test of spec.tests ?? []) {
|
||||
if (test.status === 'expected') {
|
||||
passed++
|
||||
continue
|
||||
}
|
||||
if (test.status === 'skipped') {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
const errors = test.results.flatMap((result) => result.errors ?? [])
|
||||
failures.push({
|
||||
file: spec.file ?? file,
|
||||
title: [...titles, spec.title].join(' › '),
|
||||
project: test.projectName,
|
||||
status: test.status,
|
||||
message: errors
|
||||
.map((error) => error.message ?? error.value ?? '')
|
||||
.join('\n')
|
||||
.slice(0, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const child of suite.suites ?? []) {
|
||||
visit(child, titles, file)
|
||||
}
|
||||
}
|
||||
for (const suite of report.suites ?? []) {
|
||||
visit(suite)
|
||||
}
|
||||
return { passed, skipped, failures, errors: report.errors ?? [] }
|
||||
}
|
||||
|
||||
export function renderE2eFailures(summary, records = [], now = new Date()) {
|
||||
const escape = (value) =>
|
||||
String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
const tracked = trackE2eFailures(summary.failures, records, now)
|
||||
const details = (failure) =>
|
||||
`<details><summary>${escape(failure.status)}: ${escape(failure.file)} — ${escape(failure.title)}</summary><pre>${escape(failure.message)}</pre></details>`
|
||||
return [
|
||||
'## E2E results',
|
||||
'',
|
||||
`${summary.passed} expected results; ${summary.skipped} skipped; ${summary.failures.length} unexpected/flaky results; ${summary.errors.length} run errors.`,
|
||||
'',
|
||||
`### Untracked failures (${tracked.untracked.length})`,
|
||||
'',
|
||||
...tracked.untracked.map(details),
|
||||
'',
|
||||
`### Tracked failures (${tracked.known.length})`,
|
||||
'',
|
||||
...tracked.known.flatMap((failure) => [
|
||||
details(failure),
|
||||
`Owner: ${escape(failure.tracking.owner)}; ${escape(failure.tracking.issue)}; expires ${escape(failure.tracking.expires)}.`
|
||||
]),
|
||||
...(tracked.invalid.length
|
||||
? [`${tracked.invalid.length} invalid/expired tracking entries were not used.`]
|
||||
: []),
|
||||
...summary.errors.map((error) => `<pre>${escape(error.message ?? error.value)}</pre>`),
|
||||
'',
|
||||
'Failures retain their original verdict. Repeated failures need a tracked owner, reproduction and review date; do not treat a red baseline as passing.',
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
try {
|
||||
const summary = e2eFailureSummary(JSON.parse(readFileSync(process.argv[2], 'utf8')))
|
||||
const records = JSON.parse(
|
||||
readFileSync(new URL('../e2e-failure-tracking.json', import.meta.url), 'utf8')
|
||||
)
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, renderE2eFailures(summary, records))
|
||||
} catch (error) {
|
||||
appendFileSync(
|
||||
process.env.GITHUB_STEP_SUMMARY,
|
||||
`E2E report unavailable (${error.code ?? 'invalid report'}); inspect the failing step and traces.\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { e2eFailureSummary, renderE2eFailures } from './ci-e2e-failure-summary.mjs'
|
||||
import { trackE2eFailures } from './ci-e2e-failure-tracking.mjs'
|
||||
|
||||
it('requires exact failures with an owner, issue and unexpired review date', () => {
|
||||
const failure = {
|
||||
file: 'test.spec.ts',
|
||||
title: 'case',
|
||||
project: 'electron',
|
||||
message: 'expected focus failed'
|
||||
}
|
||||
const record = {
|
||||
...failure,
|
||||
message: 'expected focus',
|
||||
owner: '@owner',
|
||||
issue: 'https://github.com/stablyai/orca/issues/123',
|
||||
expires: '2026-10-01'
|
||||
}
|
||||
const now = new Date('2026-09-28')
|
||||
expect(trackE2eFailures([failure], [null, record], now)).toMatchObject({
|
||||
known: [{ ...failure, tracking: record }],
|
||||
invalid: [null]
|
||||
})
|
||||
expect(trackE2eFailures([failure], null, now).untracked).toEqual([failure])
|
||||
for (const change of [
|
||||
{ expires: '2026-09-27' },
|
||||
{ expires: '2026-09-31' },
|
||||
{ owner: '' },
|
||||
{ issue: '' },
|
||||
{ title: 'different' },
|
||||
{ message: 'different' },
|
||||
{ project: 'other' }
|
||||
]) {
|
||||
expect(trackE2eFailures([failure], [{ ...record, ...change }], now).untracked).toEqual([
|
||||
failure
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps failure, flaky, skipped and startup-error evidence separate', () => {
|
||||
const report = {
|
||||
errors: [{ message: 'startup failed' }],
|
||||
suites: [
|
||||
{
|
||||
title: 'file',
|
||||
file: 'tests/e2e/test.spec.ts',
|
||||
suites: [
|
||||
{
|
||||
title: 'feature',
|
||||
specs: [
|
||||
{
|
||||
title: 'case',
|
||||
tests: [
|
||||
{ status: 'expected' },
|
||||
{ status: 'skipped' },
|
||||
{
|
||||
status: 'unexpected',
|
||||
projectName: 'electron-headless',
|
||||
results: [{ errors: [{ message: '<timeout>' }] }]
|
||||
},
|
||||
{
|
||||
status: 'flaky',
|
||||
results: [{ errors: [{ message: 'first try' }] }, { errors: [] }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const summary = e2eFailureSummary(report)
|
||||
expect(summary).toMatchObject({ passed: 1, skipped: 1, errors: report.errors })
|
||||
expect(summary.failures).toHaveLength(2)
|
||||
expect(summary.failures[0].title).toBe('file › feature › case')
|
||||
expect(renderE2eFailures(summary)).toContain('<timeout>')
|
||||
expect(renderE2eFailures(summary)).toContain('startup failed')
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
export function trackE2eFailures(failures, records, now = new Date()) {
|
||||
const known = []
|
||||
const untracked = []
|
||||
const invalid = []
|
||||
const active = (Array.isArray(records) ? records : [records]).filter((record) => {
|
||||
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
||||
invalid.push(record)
|
||||
return false
|
||||
}
|
||||
const expiry = new Date(`${record.expires}T23:59:59Z`)
|
||||
const valid =
|
||||
typeof record.file === 'string' &&
|
||||
typeof record.title === 'string' &&
|
||||
typeof record.message === 'string' &&
|
||||
record.message.length > 0 &&
|
||||
/^@[\w-]+(?:\/[\w-]+)?$/.test(record.owner ?? '') &&
|
||||
/^https:\/\/github\.com\/stablyai\/orca\/issues\/\d+$/.test(record.issue ?? '') &&
|
||||
/^\d{4}-\d{2}-\d{2}$/.test(record.expires ?? '') &&
|
||||
Number.isFinite(expiry.getTime()) &&
|
||||
expiry.toISOString().slice(0, 10) === record.expires &&
|
||||
expiry.getTime() >= now.getTime()
|
||||
if (!valid) {
|
||||
invalid.push(record)
|
||||
}
|
||||
return valid
|
||||
})
|
||||
for (const failure of failures) {
|
||||
const record = active.find(
|
||||
(entry) =>
|
||||
entry.file === failure.file &&
|
||||
entry.title === failure.title &&
|
||||
entry.project === failure.project &&
|
||||
failure.message.includes(entry.message)
|
||||
)
|
||||
if (record) {
|
||||
known.push({ ...failure, tracking: record })
|
||||
} else {
|
||||
untracked.push(failure)
|
||||
}
|
||||
}
|
||||
return { known, untracked, invalid }
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { runnerDemand, sampleWorkflowRuns } from './ci-runner-metrics.mjs'
|
||||
|
||||
async function api(path, env) {
|
||||
const response = await fetch(
|
||||
`${env.GITHUB_API_URL ?? 'https://api.github.com'}/repos/${env.GITHUB_REPOSITORY}/${path}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.GH_TOKEN}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
},
|
||||
signal: AbortSignal.timeout(30_000)
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Actions API: ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function collectRunnerDemand(env = process.env, request = (path) => api(path, env)) {
|
||||
const end = new Date(env.CI_METRICS_END ?? Date.now())
|
||||
end.setUTCMinutes(0, 0, 0)
|
||||
if (!Number.isFinite(end.getTime())) {
|
||||
throw new Error('Invalid window end')
|
||||
}
|
||||
const runs = []
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const until = new Date(end.getTime() - hour * 3_600_000 - 1000)
|
||||
const since = new Date(end.getTime() - (hour + 1) * 3_600_000)
|
||||
const range = `${since.toISOString()}..${until.toISOString()}`
|
||||
const path = `actions/runs?per_page=100&created=${encodeURIComponent(range)}`
|
||||
const first = await request(path)
|
||||
if (first.total_count > 1000) {
|
||||
throw new Error('Hourly run inventory exceeds API limit; split the interval')
|
||||
}
|
||||
runs.push(...first.workflow_runs)
|
||||
for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) {
|
||||
runs.push(...(await request(`${path}&page=${page}`)).workflow_runs)
|
||||
}
|
||||
}
|
||||
const samples = sampleWorkflowRuns(runs)
|
||||
// Bound API concurrency and preserve every selected observation, including zero-job runs.
|
||||
for (let index = 0; index < samples.length; index += 4) {
|
||||
await Promise.all(
|
||||
samples.slice(index, index + 4).map(async (sample) => {
|
||||
const path = `actions/runs/${sample.run.id}/jobs?per_page=100`
|
||||
const first = await request(path)
|
||||
sample.jobs = first.jobs
|
||||
for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) {
|
||||
sample.jobs.push(...(await request(`${path}&page=${page}`)).jobs)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
const report = {
|
||||
start: new Date(end.getTime() - 86_400_000).toISOString(),
|
||||
end: end.toISOString(),
|
||||
method:
|
||||
'Stratified by workflow/conclusion, six sampled runs per stratum; latest attempts only; incomplete jobs excluded',
|
||||
...runnerDemand(runs, samples)
|
||||
}
|
||||
return { report, runs, samples }
|
||||
}
|
||||
|
||||
export function demandMarkdown(report) {
|
||||
return [
|
||||
`## CI demand: ${report.start} to ${report.end}`,
|
||||
'',
|
||||
`Estimated full job duration for runs created in this window (not window-clipped occupancy); ${report.sampledRuns}/${report.populationRuns} runs sampled. ${report.method}.`,
|
||||
'',
|
||||
'| Workflow | Runs | Runner hours | Cancelled-run hours | Minutes/completed PR run |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
...report.workflows.map(
|
||||
(row) =>
|
||||
`| ${row.workflow} | ${row.runs} | ${(row.runnerMinutes / 60).toFixed(1)} | ${(row.cancelledRunnerMinutes / 60).toFixed(1)} | ${row.runnerMinutesPerCompletedPrRun?.toFixed(1) ?? '—'} |`
|
||||
),
|
||||
'',
|
||||
'| Runner labels | Hours | Queue/provisioning p95 minutes |',
|
||||
'| --- | ---: | ---: |',
|
||||
...report.pools.map(
|
||||
(pool) =>
|
||||
`| ${pool.label} | ${(pool.runnerMinutes / 60).toFixed(1)} | ${pool.queueP95Minutes?.toFixed(1) ?? '—'} |`
|
||||
),
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const evidence = await collectRunnerDemand()
|
||||
mkdirSync('ci-demand', { recursive: true })
|
||||
writeFileSync('ci-demand/evidence.json', JSON.stringify(evidence))
|
||||
const markdown = demandMarkdown(evidence.report)
|
||||
writeFileSync('ci-demand/report.md', markdown)
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
const minutesBetween = (start, end) => {
|
||||
const value = (Date.parse(end) - Date.parse(start)) / 60_000
|
||||
return Number.isFinite(value) ? Math.max(0, value) : 0
|
||||
}
|
||||
|
||||
const workflowPath = (run) => run.path.split('@')[0]
|
||||
const workflowKey = (run) => run.workflow_id ?? workflowPath(run)
|
||||
|
||||
export function weightedPercentile(values, percentile) {
|
||||
const sorted = [...values].sort((a, b) => a.value - b.value)
|
||||
const target = sorted.reduce((sum, item) => sum + item.weight, 0) * percentile
|
||||
let seen = 0
|
||||
for (const item of sorted) {
|
||||
seen += item.weight
|
||||
if (seen >= target) {
|
||||
return item.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function runnerDemand(runs, samples) {
|
||||
const workflows = new Map()
|
||||
const pools = new Map()
|
||||
for (const run of runs) {
|
||||
if (!workflows.has(workflowKey(run))) {
|
||||
workflows.set(workflowKey(run), {
|
||||
workflow: workflowPath(run),
|
||||
runs: 0,
|
||||
sampledRuns: 0,
|
||||
runnerMinutes: 0,
|
||||
cancelledRunnerMinutes: 0,
|
||||
completedPrRuns: 0,
|
||||
completedPrRunnerMinutes: 0
|
||||
})
|
||||
}
|
||||
workflows.get(workflowKey(run)).runs++
|
||||
}
|
||||
let incompleteJobs = 0
|
||||
for (const { run, jobs, weight } of samples) {
|
||||
const row = workflows.get(workflowKey(run))
|
||||
row.sampledRuns++
|
||||
for (const job of jobs) {
|
||||
if (!job.runner_name || !job.started_at) {
|
||||
continue
|
||||
}
|
||||
if (!job.completed_at) {
|
||||
incompleteJobs++
|
||||
continue
|
||||
}
|
||||
const duration = minutesBetween(job.started_at, job.completed_at)
|
||||
const weighted = duration * weight
|
||||
row.runnerMinutes += weighted
|
||||
if (run.conclusion === 'cancelled') {
|
||||
row.cancelledRunnerMinutes += weighted
|
||||
}
|
||||
if (run.event === 'pull_request' && ['success', 'failure'].includes(run.conclusion)) {
|
||||
row.completedPrRunnerMinutes += weighted
|
||||
}
|
||||
const label = [...job.labels].sort().join(', ') || 'unknown'
|
||||
if (!pools.has(label)) {
|
||||
pools.set(label, { label, runnerMinutes: 0, waits: [] })
|
||||
}
|
||||
const pool = pools.get(label)
|
||||
pool.runnerMinutes += weighted
|
||||
if (job.created_at) {
|
||||
pool.waits.push({ value: minutesBetween(job.created_at, job.started_at), weight })
|
||||
}
|
||||
}
|
||||
if (run.event === 'pull_request' && ['success', 'failure'].includes(run.conclusion)) {
|
||||
row.completedPrRuns += weight
|
||||
}
|
||||
}
|
||||
return {
|
||||
populationRuns: runs.length,
|
||||
sampledRuns: samples.length,
|
||||
incompleteJobs,
|
||||
workflows: [...workflows.values()]
|
||||
.map((row) => ({
|
||||
...row,
|
||||
runnerMinutesPerCompletedPrRun: row.completedPrRuns
|
||||
? row.completedPrRunnerMinutes / row.completedPrRuns
|
||||
: null
|
||||
}))
|
||||
.sort((a, b) => b.runnerMinutes - a.runnerMinutes),
|
||||
pools: [...pools.values()]
|
||||
.map(({ waits, ...pool }) => ({ ...pool, queueP95Minutes: weightedPercentile(waits, 0.95) }))
|
||||
.sort((a, b) => b.runnerMinutes - a.runnerMinutes)
|
||||
}
|
||||
}
|
||||
|
||||
export function sampleWorkflowRuns(runs, perStratum = 6, random = Math.random) {
|
||||
const groups = new Map()
|
||||
for (const run of runs) {
|
||||
const key = `${workflowKey(run)}:${run.conclusion ?? run.status}`
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, [])
|
||||
}
|
||||
groups.get(key).push(run)
|
||||
}
|
||||
return [...groups.values()].flatMap((group) => {
|
||||
const shuffled = [...group]
|
||||
for (let index = shuffled.length - 1; index > 0; index--) {
|
||||
const target = Math.floor(random() * (index + 1))
|
||||
;[shuffled[index], shuffled[target]] = [shuffled[target], shuffled[index]]
|
||||
}
|
||||
const selected = shuffled.slice(0, perStratum)
|
||||
return selected.map((run) => ({ run, weight: group.length / selected.length }))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { runnerDemand, sampleWorkflowRuns } from './ci-runner-metrics.mjs'
|
||||
import { collectRunnerDemand } from './ci-runner-demand.mjs'
|
||||
|
||||
const run = {
|
||||
id: 1,
|
||||
path: '.github/workflows/pr.yml',
|
||||
event: 'pull_request',
|
||||
conclusion: 'success'
|
||||
}
|
||||
const job = {
|
||||
runner_name: 'hosted',
|
||||
labels: ['ubuntu-latest'],
|
||||
created_at: '2026-09-27T00:00:00Z',
|
||||
started_at: '2026-09-27T00:03:00Z',
|
||||
completed_at: '2026-09-27T00:08:00Z'
|
||||
}
|
||||
|
||||
it('separates occupancy, queueing, cancellations and zero-job observations', () => {
|
||||
const cancelled = { ...run, id: 2, conclusion: 'cancelled' }
|
||||
const report = runnerDemand(
|
||||
[run, cancelled],
|
||||
[
|
||||
{ run, weight: 2, jobs: [job, { ...job, runner_name: null }] },
|
||||
{ run: cancelled, weight: 3, jobs: [job, { ...job, completed_at: null }] }
|
||||
]
|
||||
)
|
||||
expect(report.workflows[0]).toMatchObject({
|
||||
runnerMinutes: 25,
|
||||
cancelledRunnerMinutes: 15,
|
||||
runnerMinutesPerCompletedPrRun: 5
|
||||
})
|
||||
expect(report.pools[0].queueP95Minutes).toBe(3)
|
||||
expect(report.incompleteJobs).toBe(1)
|
||||
expect(runnerDemand([run], [{ run, weight: 1, jobs: [] }]).workflows[0].runnerMinutes).toBe(0)
|
||||
})
|
||||
|
||||
it('weights each workflow/outcome stratum back to the full inventory', () => {
|
||||
const runs = Array.from({ length: 20 }, (_, index) => ({
|
||||
...run,
|
||||
id: index,
|
||||
conclusion: index < 10 ? 'success' : 'failure'
|
||||
}))
|
||||
const sample = sampleWorkflowRuns(runs, 2, () => 0.5)
|
||||
expect(sample).toHaveLength(4)
|
||||
expect(sample.reduce((sum, row) => sum + row.weight, 0)).toBe(20)
|
||||
expect(new Set(sample.map((row) => row.run.id)).size).toBe(4)
|
||||
})
|
||||
|
||||
it('paginates jobs and bounds run discovery to 24 complete hours', async () => {
|
||||
const requests = []
|
||||
const result = await collectRunnerDemand(
|
||||
{ CI_METRICS_END: '2026-09-28T04:59:00Z' },
|
||||
async (path) => {
|
||||
requests.push(path)
|
||||
if (path.includes('/jobs')) {
|
||||
return {
|
||||
total_count: 101,
|
||||
jobs: path.includes('page=2') ? [job] : Array.from({ length: 100 }, () => job)
|
||||
}
|
||||
}
|
||||
return {
|
||||
total_count: requests.length === 1 ? 1 : 0,
|
||||
workflow_runs: requests.length === 1 ? [run] : []
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(result.report.start).toBe('2026-09-27T04:00:00.000Z')
|
||||
expect(result.report.end).toBe('2026-09-28T04:00:00.000Z')
|
||||
expect(requests.filter((path) => path.startsWith('actions/runs?'))).toHaveLength(24)
|
||||
expect(result.samples[0].jobs).toHaveLength(101)
|
||||
expect(result.report.workflows[0].runnerMinutes).toBe(505)
|
||||
})
|
||||
|
||||
it('refuses to publish a silently truncated inventory', async () => {
|
||||
await expect(
|
||||
collectRunnerDemand({}, async () => ({ total_count: 1001, workflow_runs: [] }))
|
||||
).rejects.toThrow('API limit')
|
||||
})
|
||||
|
||||
it('groups ref-qualified paths under the stable workflow ID', () => {
|
||||
const runs = [
|
||||
{ ...run, id: 1, workflow_id: 42, path: '.github/workflows/pr.yml@main' },
|
||||
{ ...run, id: 2, workflow_id: 42, path: '.github/workflows/pr.yml@feature' }
|
||||
]
|
||||
const sample = sampleWorkflowRuns(runs, 1, () => 0.5)
|
||||
expect(sample).toHaveLength(1)
|
||||
expect(sample[0].weight).toBe(2)
|
||||
expect(
|
||||
runnerDemand(
|
||||
runs,
|
||||
sample.map((row) => ({ ...row, jobs: [job] }))
|
||||
).workflows
|
||||
).toMatchObject([{ workflow: '.github/workflows/pr.yml', runs: 2, runnerMinutes: 10 }])
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { posix, join } from 'node:path'
|
||||
import ts from 'typescript-api'
|
||||
|
||||
const EXTENSIONS = [
|
||||
'',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.mjs',
|
||||
'.js',
|
||||
'.cjs',
|
||||
'.json',
|
||||
'/index.ts',
|
||||
'/index.tsx',
|
||||
'/index.js'
|
||||
]
|
||||
const INDIRECT_INPUT =
|
||||
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
|
||||
|
||||
function localPath(file, specifier) {
|
||||
if (specifier.startsWith('.')) {
|
||||
return posix.join(posix.dirname(file), specifier)
|
||||
}
|
||||
if (specifier.startsWith('@renderer/')) {
|
||||
return `src/renderer/src/${specifier.slice(10)}`
|
||||
}
|
||||
if (specifier.startsWith('@/')) {
|
||||
return `src/renderer/src/${specifier.slice(2)}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildUnitDependencyGraph(sources) {
|
||||
const reverse = new Map()
|
||||
const opaque = new Set()
|
||||
for (const [file, source] of sources) {
|
||||
if (INDIRECT_INPUT.test(source) || file.startsWith('config/') || file.startsWith('tests/')) {
|
||||
opaque.add(file)
|
||||
}
|
||||
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
|
||||
const path = localPath(file, imported.fileName)
|
||||
if (path === null) {
|
||||
continue
|
||||
}
|
||||
const resolved = EXTENSIONS.map((extension) => path + extension).find((candidate) =>
|
||||
sources.has(candidate)
|
||||
)
|
||||
if (!resolved) {
|
||||
opaque.add(file)
|
||||
continue
|
||||
}
|
||||
if (!reverse.has(resolved)) {
|
||||
reverse.set(resolved, new Set())
|
||||
}
|
||||
reverse.get(resolved).add(file)
|
||||
}
|
||||
}
|
||||
return { reverse, opaque }
|
||||
}
|
||||
|
||||
export function collectUnitDependencyGraph(root = process.cwd()) {
|
||||
const files = globSync(
|
||||
[
|
||||
'src/**/*.{ts,tsx,js,mjs,cjs,json}',
|
||||
'config/**/*.{ts,tsx,js,mjs,cjs,json}',
|
||||
'tests/**/*.{ts,tsx,js,mjs,cjs,json}'
|
||||
],
|
||||
{ cwd: root }
|
||||
)
|
||||
const sources = new Map(
|
||||
files.map((file) => [file.replaceAll('\\', '/'), readFileSync(join(root, file), 'utf8')])
|
||||
)
|
||||
return { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
|
||||
}
|
||||
|
||||
export function unitConsumers(seeds, reverse) {
|
||||
const result = new Set(seeds)
|
||||
for (const file of result) {
|
||||
for (const consumer of reverse.get(file) ?? []) {
|
||||
result.add(consumer)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { globSync } from 'node:fs'
|
||||
import { defaultExclude } from 'vitest/config'
|
||||
|
||||
export const UNIT_INCLUDE = [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'config/scripts/**/*.test.ts',
|
||||
'config/scripts/**/*.test.mjs',
|
||||
'tests/tools/**/*.test.mjs',
|
||||
'tests/e2e/**/*.unit.test.ts'
|
||||
]
|
||||
|
||||
export const UNIT_EXCLUDE = [
|
||||
...defaultExclude,
|
||||
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts',
|
||||
'src/main/daemon/shell-ready.test.ts',
|
||||
'src/main/daemon/node-pty-fd-leak.test.ts',
|
||||
'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts',
|
||||
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
|
||||
'src/main/pty/omp-shell-wrapper-alias-safety.test.ts',
|
||||
'src/main/pty/omp-shell-wrapper.node-pty.test.ts',
|
||||
'src/main/shell-startup-feature-channel.test.ts',
|
||||
'src/main/terminal-history-fish-session.node-pty.test.ts',
|
||||
'src/main/zsh-scoped-histfile.live-shell.test.ts',
|
||||
'src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts',
|
||||
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
|
||||
'src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts',
|
||||
'src/shared/fish-query-reply-child-stdin.node-pty.test.ts',
|
||||
'src/shared/pty-reply-echo-shapes.node-pty.test.ts',
|
||||
'src/shared/startup-shell-portability.live-shell.test.ts',
|
||||
'src/shared/posix-command-path-lookup.test.ts',
|
||||
'tests/e2e/relay-region-compatibility.unit.test.ts',
|
||||
'tests/e2e/relay-region-correction.unit.test.ts',
|
||||
'tests/e2e/cross-version-wire/**'
|
||||
]
|
||||
|
||||
export function discoverUnitFiles(root = process.cwd()) {
|
||||
return globSync(UNIT_INCLUDE, { cwd: root, exclude: UNIT_EXCLUDE })
|
||||
.map((file) => file.replaceAll('\\', '/'))
|
||||
.sort()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { collectUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { discoverUnitFiles } from './ci-unit-files.mjs'
|
||||
import { planUnitSelection } from './ci-unit-selection.mjs'
|
||||
import { readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
|
||||
|
||||
export function prepareUnitPlan(env = process.env) {
|
||||
const files = discoverUnitFiles()
|
||||
let plan
|
||||
try {
|
||||
const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf8'))
|
||||
if (env.GITHUB_EVENT_NAME !== 'pull_request') {
|
||||
throw new Error('Full reference run')
|
||||
}
|
||||
const diff = runProcessSync({
|
||||
program: 'git',
|
||||
args: ['diff', '--name-only', '--no-renames', '-z', 'HEAD^1', 'HEAD'],
|
||||
maxOutputBytes: 16 * 1024 * 1024
|
||||
})
|
||||
if (diff.code !== 0 || diff.timedOut) {
|
||||
throw new Error('Changed paths unavailable')
|
||||
}
|
||||
plan = planUnitSelection({
|
||||
files,
|
||||
changed: diff.stdout.split('\0').filter(Boolean),
|
||||
graph: collectUnitDependencyGraph(),
|
||||
timings: readTimingBaseline('unit').timings,
|
||||
event,
|
||||
mode: env.ORCA_UNIT_SELECTION_MODE
|
||||
})
|
||||
} catch (error) {
|
||||
plan = {
|
||||
version: 1,
|
||||
mode: 'shadow',
|
||||
selectionAvailable: false,
|
||||
reason: String(error),
|
||||
files,
|
||||
candidateFiles: files,
|
||||
executionFiles: files,
|
||||
shards: Array.from({ length: 8 }, (_, index) => ({ index: index + 1, count: 8 }))
|
||||
}
|
||||
}
|
||||
writeAssignment('ci-shards/unit-selection.json', plan)
|
||||
if (env.GITHUB_OUTPUT) {
|
||||
appendFileSync(env.GITHUB_OUTPUT, `shards=${JSON.stringify(plan.shards)}\n`)
|
||||
}
|
||||
if (env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(
|
||||
env.GITHUB_STEP_SUMMARY,
|
||||
`Unit selection: **${plan.mode}**; ${plan.candidateFiles.length}/${files.length} candidate files; ${plan.shards.length} execution shards. ${plan.reason}\n`
|
||||
)
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
prepareUnitPlan()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
|
||||
it.each([true, false])(
|
||||
'plans from a real Git diff, with parent evidence available: %s',
|
||||
(withParent) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-unit-plan-'))
|
||||
const git = (args) => {
|
||||
const result = runProcessSync({ program: 'git', args, cwd: root })
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
try {
|
||||
mkdirSync(join(root, 'src'))
|
||||
writeFileSync(join(root, 'src/value.ts'), 'export const value = 1')
|
||||
writeFileSync(join(root, 'src/consumer.test.ts'), "import './value'")
|
||||
writeFileSync(join(root, 'src/unrelated.test.ts'), 'export const unrelated = true')
|
||||
git(['init', '--quiet'])
|
||||
git(['add', 'src'])
|
||||
const commit = [
|
||||
'-c',
|
||||
'user.name=CI Test',
|
||||
'-c',
|
||||
'user.email=ci-test@example.invalid',
|
||||
'-c',
|
||||
'commit.gpgsign=false',
|
||||
'commit',
|
||||
'--quiet',
|
||||
'-m',
|
||||
'fixture'
|
||||
]
|
||||
git(commit)
|
||||
if (withParent) {
|
||||
writeFileSync(join(root, 'src/value.ts'), 'export const value = 2')
|
||||
git(['add', 'src'])
|
||||
git(commit)
|
||||
}
|
||||
const sourceSha = git(['rev-parse', 'HEAD'])
|
||||
const eventPath = join(root, 'event.json')
|
||||
writeFileSync(eventPath, JSON.stringify({ pull_request: { draft: true } }))
|
||||
const result = runProcessSync({
|
||||
program: process.execPath,
|
||||
args: [fileURLToPath(new URL('./ci-unit-plan.mjs', import.meta.url))],
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
ORCA_UNIT_SELECTION_MODE: 'selected',
|
||||
GITHUB_EVENT_NAME: 'pull_request',
|
||||
GITHUB_EVENT_PATH: eventPath,
|
||||
GITHUB_SHA: sourceSha,
|
||||
ORCA_SHARD_SOURCE_SHA: sourceSha,
|
||||
GITHUB_OUTPUT: join(root, 'outputs'),
|
||||
GITHUB_STEP_SUMMARY: join(root, 'summary')
|
||||
}
|
||||
})
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
const plan = JSON.parse(readFileSync(join(root, 'ci-shards/unit-selection.json'), 'utf8'))
|
||||
expect(plan.sourceSha).toBe(sourceSha)
|
||||
expect(plan.mode).toBe(withParent ? 'selected' : 'shadow')
|
||||
expect(plan.executionFiles).toEqual(
|
||||
withParent ? ['src/consumer.test.ts'] : ['src/consumer.test.ts', 'src/unrelated.test.ts']
|
||||
)
|
||||
expect(plan.reason).toBe(
|
||||
withParent
|
||||
? 'Transitive imports plus indirect-input consumers'
|
||||
: 'Error: Changed paths unavailable'
|
||||
)
|
||||
expect(plan.shards).toHaveLength(withParent ? 1 : 8)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
import { appendFileSync, globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export function reviewUnitSelection(records) {
|
||||
const groups = new Map()
|
||||
for (const { plan, timing } of records) {
|
||||
const key = JSON.stringify([
|
||||
timing.sourceSha,
|
||||
timing.runId,
|
||||
timing.runAttempt,
|
||||
timing.nodeVersion
|
||||
])
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, [])
|
||||
}
|
||||
groups.get(key).push({ plan, timing })
|
||||
}
|
||||
return [...groups.values()].map((group) => {
|
||||
const { plan, timing: first } = group[0]
|
||||
const selected = new Set(plan.candidateFiles)
|
||||
const files = new Set()
|
||||
const shards = new Set()
|
||||
const missedFailures = []
|
||||
let invalid = false
|
||||
let omittedMs = 0
|
||||
let totalMs = 0
|
||||
for (const { plan: other, timing } of group) {
|
||||
invalid ||=
|
||||
JSON.stringify(other) !== JSON.stringify(plan) ||
|
||||
!first.sourceSha ||
|
||||
plan.sourceSha !== first.sourceSha ||
|
||||
timing.unhandledErrors !== 0 ||
|
||||
!['passed', 'failed'].includes(timing.status) ||
|
||||
timing.shard.count !== first.shard.count ||
|
||||
shards.has(timing.shard.index) ||
|
||||
timing.shard.index < 1 ||
|
||||
timing.shard.index > first.shard.count
|
||||
shards.add(timing.shard.index)
|
||||
for (const [file, duration] of Object.entries(timing.timings)) {
|
||||
invalid ||=
|
||||
files.has(file) ||
|
||||
!Number.isFinite(duration) ||
|
||||
duration <= 0 ||
|
||||
!['passed', 'failed', 'skipped'].includes(timing.results?.[file])
|
||||
files.add(file)
|
||||
totalMs += duration
|
||||
if (!selected.has(file)) {
|
||||
omittedMs += duration
|
||||
if (timing.results?.[file] === 'failed') {
|
||||
missedFailures.push(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const complete =
|
||||
!invalid &&
|
||||
shards.size === first.shard.count &&
|
||||
JSON.stringify([...files].sort()) === JSON.stringify([...plan.files].sort())
|
||||
return {
|
||||
sourceSha: first.sourceSha,
|
||||
runId: first.runId,
|
||||
runAttempt: first.runAttempt,
|
||||
nodeVersion: first.nodeVersion,
|
||||
mode: plan.mode,
|
||||
completeFullRun: complete && plan.mode === 'shadow',
|
||||
selectionEvaluated: complete && plan.mode === 'shadow' && plan.selectionAvailable === true,
|
||||
reason: plan.reason,
|
||||
missedFailures,
|
||||
files: files.size,
|
||||
candidateFiles: selected.size,
|
||||
measuredWorkerMs: totalMs,
|
||||
potentiallyOmittedWorkerMs: omittedMs
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const directory = process.argv[2]
|
||||
if (!directory) {
|
||||
throw new Error('Usage: ci-unit-selection-review.mjs ARTIFACT_DIRECTORY')
|
||||
}
|
||||
const records = globSync('**/unit-timings.json', { cwd: directory }).map((file) => {
|
||||
const timing = JSON.parse(readFileSync(join(directory, file), 'utf8'))
|
||||
const plan = JSON.parse(
|
||||
readFileSync(
|
||||
join(directory, file.replace('unit-timings.json', 'unit-selection.json')),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
return { timing, plan }
|
||||
})
|
||||
if (!records.length) {
|
||||
throw new Error('No unit selection evidence')
|
||||
}
|
||||
const review = reviewUnitSelection(records)
|
||||
writeFileSync(join(directory, 'selection-review.json'), `${JSON.stringify(review, null, 2)}\n`)
|
||||
const summary = [
|
||||
'## Unit selection evidence',
|
||||
'',
|
||||
...review.map(
|
||||
(row) =>
|
||||
`- Node ${row.nodeVersion}: ${row.completeFullRun ? 'complete full reference' : 'incomplete or selected evidence'}; ${row.selectionEvaluated ? 'selection evaluated' : 'not selection-validation evidence'}; ${row.candidateFiles}/${row.files} candidate files; ${row.missedFailures.length} failures outside selection; ${(row.potentiallyOmittedWorkerMs / 60_000).toFixed(1)} potentially omitted worker-minutes. ${row.reason ?? ''}`
|
||||
),
|
||||
'',
|
||||
'Worker time overlaps across processes; it is not runner time. Promote only after representative complete references show no missed failures.',
|
||||
''
|
||||
].join('\n')
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary)
|
||||
}
|
||||
console.log(summary)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { reviewUnitSelection } from './ci-unit-selection-review.mjs'
|
||||
|
||||
const plan = {
|
||||
sourceSha: 'sha',
|
||||
mode: 'shadow',
|
||||
selectionAvailable: true,
|
||||
files: ['a', 'b'],
|
||||
candidateFiles: ['a']
|
||||
}
|
||||
|
||||
it('does not present full fallback runs as selection-validation evidence', () => {
|
||||
const records = [record(1, 'a', 'passed'), record(2, 'b', 'passed')].map((row) => ({
|
||||
...row,
|
||||
plan: { ...plan, selectionAvailable: false }
|
||||
}))
|
||||
expect(reviewUnitSelection(records)[0]).toMatchObject({
|
||||
completeFullRun: true,
|
||||
selectionEvaluated: false
|
||||
})
|
||||
})
|
||||
const record = (index, file, state) => ({
|
||||
plan,
|
||||
timing: {
|
||||
sourceSha: 'sha',
|
||||
runId: '1',
|
||||
runAttempt: '1',
|
||||
nodeVersion: '24',
|
||||
status: state,
|
||||
unhandledErrors: 0,
|
||||
shard: { index, count: 2 },
|
||||
timings: { [file]: 100 },
|
||||
results: { [file]: state }
|
||||
}
|
||||
})
|
||||
|
||||
it('finds omitted failures in a complete failing reference run', () => {
|
||||
expect(
|
||||
reviewUnitSelection([record(1, 'a', 'passed'), record(2, 'b', 'failed')])[0]
|
||||
).toMatchObject({ completeFullRun: true, missedFailures: ['b'], potentiallyOmittedWorkerMs: 100 })
|
||||
})
|
||||
|
||||
it('does not call incomplete, duplicate, interrupted or selected evidence a full reference', () => {
|
||||
const a = record(1, 'a', 'passed'),
|
||||
b = record(2, 'b', 'passed')
|
||||
for (const rows of [
|
||||
[a],
|
||||
[a, a],
|
||||
[a, { ...b, timing: { ...b.timing, unhandledErrors: 1 } }],
|
||||
[a, { ...b, timing: { ...b.timing, status: 'interrupted' } }],
|
||||
[a, b].map((row) => ({ ...row, plan: { ...plan, mode: 'selected' } }))
|
||||
]) {
|
||||
expect(reviewUnitSelection(rows).every((row) => !row.completeFullRun)).toBe(true)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { unitConsumers } from './ci-unit-dependency-graph.mjs'
|
||||
import { balanceFiles } from './ci-shard-assignment.mjs'
|
||||
|
||||
export function selectUnitFiles(files, changed, graph) {
|
||||
const full = (reason) => ({ files, reason, full: true })
|
||||
if (!changed.length) {
|
||||
return full('Missing changed-path evidence')
|
||||
}
|
||||
if (changed.some((file) => !file.startsWith('src/') || !graph.files.has(file))) {
|
||||
return full('Global, deleted, renamed or unknown input')
|
||||
}
|
||||
for (const file of changed) {
|
||||
const consumers = unitConsumers([file], graph.reverse)
|
||||
if (!files.some((test) => consumers.has(test))) {
|
||||
return full('No proven test coverage for changed inputs')
|
||||
}
|
||||
}
|
||||
const affected = unitConsumers([...changed, ...graph.opaque], graph.reverse)
|
||||
const selected = files.filter((file) => affected.has(file))
|
||||
if (!selected.length) {
|
||||
return full('No proven test coverage for changed inputs')
|
||||
}
|
||||
return {
|
||||
files: selected,
|
||||
reason: 'Transitive imports plus indirect-input consumers',
|
||||
full: false
|
||||
}
|
||||
}
|
||||
|
||||
export function planUnitSelection({ files, changed, graph, timings, event, mode = 'shadow' }) {
|
||||
const candidate = selectUnitFiles(files, changed, graph)
|
||||
const selected = mode === 'selected' && event?.pull_request?.draft === true && !candidate.full
|
||||
const executionFiles = selected ? candidate.files : files
|
||||
const totalMs = balanceFiles(executionFiles, 1, timings).shards[0].durationMs
|
||||
const count = selected
|
||||
? Math.max(1, Math.min(8, Math.ceil(totalMs / 900_000), executionFiles.length))
|
||||
: 8
|
||||
return {
|
||||
version: 1,
|
||||
mode: selected ? 'selected' : 'shadow',
|
||||
selectionAvailable: !candidate.full,
|
||||
reason: candidate.reason,
|
||||
files,
|
||||
candidateFiles: candidate.files,
|
||||
executionFiles,
|
||||
shards: Array.from({ length: count }, (_, index) => ({ index: index + 1, count }))
|
||||
}
|
||||
}
|
||||
|
||||
export function auditUnitSelection(plan, results) {
|
||||
const candidates = new Set(plan.candidateFiles)
|
||||
const omittedFailures = Object.entries(results)
|
||||
.filter(([file, state]) => state === 'failed' && !candidates.has(file))
|
||||
.map(([file]) => file)
|
||||
return {
|
||||
mode: plan.mode,
|
||||
discovered: plan.files.length,
|
||||
candidate: candidates.size,
|
||||
executed: Object.keys(results).length,
|
||||
omittedFailures
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { auditUnitSelection, planUnitSelection, selectUnitFiles } from './ci-unit-selection.mjs'
|
||||
|
||||
const sources = new Map(
|
||||
Object.entries({
|
||||
'src/leaf.ts': 'export const value = 1',
|
||||
'src/forward.ts': `export * from './leaf'`,
|
||||
'src/consumer.test.ts': `import './forward'`,
|
||||
'src/dynamic.test.ts': `import('./leaf')`,
|
||||
'src/require.test.ts': `require('./leaf')`,
|
||||
'src/scan.test.ts': `import { readFileSync } from 'node:fs'; readFileSync('src/leaf.ts')`,
|
||||
'src/indirect.ts': `import(pathFromSettings)`,
|
||||
'src/indirect.test.ts': `import './indirect'`,
|
||||
'src/unrelated.test.ts': 'export const test = 1',
|
||||
'src/renderer/src/view.tsx': 'export const value = 1',
|
||||
'src/view.test.ts': `import '@renderer/view'; import '@/view'`
|
||||
})
|
||||
)
|
||||
const graph = { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
|
||||
const files = [...sources.keys()].filter((file) => file.endsWith('.test.ts')).sort()
|
||||
|
||||
describe('conservative unit selection', () => {
|
||||
it('follows re-exports, literal dynamic imports and requires, retaining indirect readers', () => {
|
||||
expect(selectUnitFiles(files, ['src/leaf.ts'], graph).files).toEqual([
|
||||
'src/consumer.test.ts',
|
||||
'src/dynamic.test.ts',
|
||||
'src/indirect.test.ts',
|
||||
'src/require.test.ts',
|
||||
'src/scan.test.ts'
|
||||
])
|
||||
})
|
||||
|
||||
it('resolves renderer aliases and changed tests without executing their source', () => {
|
||||
expect(selectUnitFiles(files, ['src/renderer/src/view.tsx'], graph).files).toContain(
|
||||
'src/view.test.ts'
|
||||
)
|
||||
expect(selectUnitFiles(files, ['src/unrelated.test.ts'], graph).files).toContain(
|
||||
'src/unrelated.test.ts'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not mistake unrelated opaque readers for coverage of a new entry point', () => {
|
||||
const uncovered = { ...graph, files: new Set([...graph.files, 'src/entry.ts']) }
|
||||
expect(selectUnitFiles(files, ['src/entry.ts'], uncovered)).toMatchObject({ full: true, files })
|
||||
})
|
||||
|
||||
it.each(
|
||||
[
|
||||
[],
|
||||
['src/deleted.ts'],
|
||||
['src/deleted.ts', 'src/leaf.ts'],
|
||||
['pnpm-lock.yaml'],
|
||||
['config/vitest.config.ts']
|
||||
].map((changed) => ({ changed }))
|
||||
)('runs everything for incomplete/global evidence: $changed', ({ changed }) => {
|
||||
expect(selectUnitFiles(files, changed, graph)).toMatchObject({ files, full: true })
|
||||
})
|
||||
|
||||
it('keeps full coverage by default and on every non-draft commit', () => {
|
||||
const base = { files, changed: ['src/leaf.ts'], graph, timings: {} }
|
||||
for (const event of [
|
||||
{},
|
||||
{ pull_request: { draft: false } },
|
||||
{ pull_request: { draft: true } }
|
||||
]) {
|
||||
expect(planUnitSelection({ ...base, event }).executionFiles).toEqual(files)
|
||||
}
|
||||
expect(
|
||||
planUnitSelection({ ...base, mode: 'selected', event: { pull_request: { draft: false } } })
|
||||
.executionFiles
|
||||
).toEqual(files)
|
||||
const selected = planUnitSelection({
|
||||
...base,
|
||||
mode: 'selected',
|
||||
event: { pull_request: { draft: true } }
|
||||
})
|
||||
expect(selected.executionFiles).not.toContain('src/unrelated.test.ts')
|
||||
expect(selected.shards).toEqual([{ index: 1, count: 1 }])
|
||||
})
|
||||
|
||||
it('records failures that would have been missed while shadow runs remain full', () => {
|
||||
const plan = planUnitSelection({ files, changed: ['src/leaf.ts'], graph, timings: {} })
|
||||
expect(
|
||||
auditUnitSelection(plan, {
|
||||
'src/consumer.test.ts': 'failed',
|
||||
'src/unrelated.test.ts': 'failed',
|
||||
'src/view.test.ts': 'passed'
|
||||
}).omittedFailures
|
||||
).toEqual(['src/unrelated.test.ts'])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { relative } from 'node:path'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { BaseSequencer } from 'vitest/node'
|
||||
import { balanceFiles, readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
|
||||
|
||||
@@ -6,12 +7,46 @@ export default class TimingSequencer extends BaseSequencer {
|
||||
async shard(specs) {
|
||||
const { index, count } = this.ctx.config.shard
|
||||
const key = (spec) => relative(this.ctx.config.root, spec.moduleId).replaceAll('\\', '/')
|
||||
let execution = specs
|
||||
let selectionReason = 'Full suite: no verified selection plan'
|
||||
try {
|
||||
const plan = JSON.parse(
|
||||
readFileSync(
|
||||
process.env.ORCA_UNIT_SELECTION_PLAN ?? 'ci-shards/unit-selection.json',
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
if (
|
||||
plan.version !== 1 ||
|
||||
!plan.sourceSha ||
|
||||
plan.sourceSha !== process.env.ORCA_SHARD_SOURCE_SHA ||
|
||||
JSON.stringify([...plan.files].sort()) !== JSON.stringify(specs.map(key).sort()) ||
|
||||
!Array.isArray(plan.executionFiles) ||
|
||||
plan.executionFiles.some((file) => !plan.files.includes(file))
|
||||
) {
|
||||
throw new Error('Selection provenance or discovery differs')
|
||||
}
|
||||
const allowed = new Set(plan.executionFiles)
|
||||
if (allowed.size === 0) {
|
||||
throw new Error('Empty execution selection')
|
||||
}
|
||||
execution = specs.filter((spec) => allowed.has(key(spec)))
|
||||
selectionReason = plan.reason
|
||||
} catch (error) {
|
||||
console.log(`Running every discovered unit test: ${error.message}`)
|
||||
}
|
||||
const baseline = readTimingBaseline('unit')
|
||||
const assignment = balanceFiles(specs.map(key), count, baseline.timings, baseline.overheadMs)
|
||||
const assignment = balanceFiles(
|
||||
execution.map(key),
|
||||
count,
|
||||
baseline.timings,
|
||||
baseline.overheadMs
|
||||
)
|
||||
writeAssignment(process.env.ORCA_SHARD_MANIFEST ?? 'ci-shards/unit-assignment.json', {
|
||||
...assignment,
|
||||
baselineSha256: baseline.baselineSha256,
|
||||
selectedShard: index
|
||||
selectedShard: index,
|
||||
selectionReason
|
||||
})
|
||||
const selected = new Set(assignment.shards[index - 1].files)
|
||||
return specs.filter((spec) => selected.has(key(spec)))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import TimingSequencer from './ci-unit-sequencer.mjs'
|
||||
|
||||
let root
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
if (root) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['valid', 'stale', 'missing-file', 'missing-artifact'])(
|
||||
'preserves complete shard coverage with %s planning evidence',
|
||||
async (kind) => {
|
||||
root = mkdtempSync(join(tmpdir(), 'unit-sequencer-'))
|
||||
const files = ['src/a.test.ts', 'src/b.test.ts', 'src/c.test.ts', 'src/d.test.ts']
|
||||
const plan = {
|
||||
version: 1,
|
||||
sourceSha: kind === 'stale' ? 'old' : 'current',
|
||||
files: kind === 'missing-file' ? files.slice(1) : files,
|
||||
executionFiles: files.slice(0, 2)
|
||||
}
|
||||
const planPath = join(root, 'selection.json')
|
||||
if (kind !== 'missing-artifact') {
|
||||
writeFileSync(planPath, JSON.stringify(plan))
|
||||
}
|
||||
vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', planPath)
|
||||
vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'current')
|
||||
vi.stubEnv('ORCA_SHARD_MANIFEST', join(root, 'assignment.json'))
|
||||
const assigned = []
|
||||
for (const index of [1, 2]) {
|
||||
const sequencer = new TimingSequencer({ config: { root, shard: { index, count: 2 } } })
|
||||
const specs = files.map((file) => ({ moduleId: join(root, file) }))
|
||||
assigned.push(...(await sequencer.shard(specs)).map((spec) => spec.moduleId))
|
||||
}
|
||||
expect(assigned.sort()).toEqual(
|
||||
(kind === 'valid' ? files.slice(0, 2) : files).map((file) => join(root, file)).sort()
|
||||
)
|
||||
expect(new Set(assigned).size).toBe(assigned.length)
|
||||
}
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
import { relative } from 'node:path'
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { writeAssignment } from './ci-shard-assignment.mjs'
|
||||
import { auditUnitSelection } from './ci-unit-selection.mjs'
|
||||
|
||||
export function moduleDuration(diagnostic) {
|
||||
return Math.max(
|
||||
@@ -20,12 +22,19 @@ export default class UnitTimingReporter {
|
||||
}
|
||||
|
||||
onTestRunEnd(modules, errors, reason) {
|
||||
const results = Object.fromEntries(
|
||||
modules.map((module) => [
|
||||
relative(this.ctx.config.root, module.moduleId).replaceAll('\\', '/'),
|
||||
module.state?.() ?? 'unknown'
|
||||
])
|
||||
)
|
||||
writeAssignment(process.env.ORCA_UNIT_TIMING_REPORT ?? 'ci-shards/unit-timings.json', {
|
||||
metric: 'module-duration-v1',
|
||||
nodeVersion: process.versions.node,
|
||||
shard: this.ctx.config.shard ?? { index: 1, count: 1 },
|
||||
status: reason,
|
||||
unhandledErrors: errors.length,
|
||||
results,
|
||||
timings: Object.fromEntries(
|
||||
modules.map((module) => [
|
||||
relative(this.ctx.config.root, module.moduleId).replaceAll('\\', '/'),
|
||||
@@ -33,5 +42,23 @@ export default class UnitTimingReporter {
|
||||
])
|
||||
)
|
||||
})
|
||||
try {
|
||||
const plan = JSON.parse(
|
||||
readFileSync(
|
||||
process.env.ORCA_UNIT_SELECTION_PLAN ?? 'ci-shards/unit-selection.json',
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const audit = auditUnitSelection(plan, results)
|
||||
writeAssignment('ci-shards/unit-selection-audit.json', audit)
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(
|
||||
process.env.GITHUB_STEP_SUMMARY,
|
||||
`Unit selection (${audit.mode}): ${audit.candidate}/${audit.discovered} candidate files; ${audit.omittedFailures.length} failures outside selection.\n`
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Timing evidence remains useful when selection planning was unavailable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import { describe, expect, it } from 'vitest'
|
||||
* around `gtk_init()` while FontConfig warmed up on a pool thread, and below
|
||||
* glibc 2.41 that frees `environ` under a concurrent `getenv()` — a launch-time
|
||||
* use-after-free on every Ubuntu we support (stablyai/orca#20081). 43.7.0 stops
|
||||
* freeing the published `environ`. A downgrade past it re-ships that crash, and
|
||||
* nothing else in the tree would notice.
|
||||
* freeing the published `environ`. 43.7.0 itself threw `Invalid guestInstanceId` when a
|
||||
* loaded `<webview>` left the DOM, which blanked browser tabs React re-inserted
|
||||
* (electron/electron#53989); 43.7.4 fixes it. A downgrade past either re-ships the bug,
|
||||
* and nothing else in the tree would notice.
|
||||
*/
|
||||
const MINIMUM_ELECTRON_VERSION = '43.7.0'
|
||||
const MINIMUM_ELECTRON_VERSION = '43.7.4'
|
||||
|
||||
function parseVersion(specifier: string): [number, number, number] {
|
||||
const match = /(\d+)\.(\d+)\.(\d+)/.exec(specifier)
|
||||
@@ -35,15 +37,17 @@ describe('electron runtime floor', () => {
|
||||
it.each([
|
||||
['42.9.0', false],
|
||||
['43.6.0', false],
|
||||
['43.7.0', true],
|
||||
['43.7.1', true],
|
||||
['43.7.0', false],
|
||||
['43.7.3', false],
|
||||
['43.7.4', true],
|
||||
['43.7.5', true],
|
||||
['43.8.0', true],
|
||||
['44.0.0', true]
|
||||
])('reads %s as meeting the floor: %s', (specifier, expected) => {
|
||||
expect(meetsRuntimeFloor(specifier)).toBe(expected)
|
||||
})
|
||||
|
||||
it('pins Electron at or above the glibc environ-race fix', () => {
|
||||
it('pins Electron at or above the glibc environ-race and webview-detach fixes', () => {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../package.json'), 'utf-8')
|
||||
) as { devDependencies: Record<string, string> }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export function selectPullRequestDiffBase(requestedBase, headParents, eventName) {
|
||||
if (eventName === 'pull_request' && headParents.length >= 2) {
|
||||
@@ -21,3 +22,15 @@ export function resolvePullRequestDiffBase(
|
||||
.split(/\s+/)
|
||||
return selectPullRequestDiffBase(requestedBase, headParents, eventName)
|
||||
}
|
||||
|
||||
// Why a CLI: the workflow's inline gates used `git diff --merge-base "$BASE_SHA" "$HEAD_SHA"`,
|
||||
// which needs the payload base to be reachable and so forced a full-history checkout. Printing
|
||||
// the resolved base lets those steps diff against `HEAD^1` with the same logic the Node gates
|
||||
// already use, instead of each one open-coding it.
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const resolved = resolvePullRequestDiffBase(process.cwd(), process.argv[2])
|
||||
if (!resolved) {
|
||||
throw new Error('No diff base: pass the pull request base SHA, or run on a merge ref.')
|
||||
}
|
||||
process.stdout.write(`${resolved}\n`)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export const NEVER_TRANSLATE_VALUES = new Set([
|
||||
'Claude Agent Teams',
|
||||
'Cline',
|
||||
'Codebuff',
|
||||
'Freebuff',
|
||||
'Codex',
|
||||
'Command Code',
|
||||
'Cursor',
|
||||
|
||||
@@ -43,12 +43,13 @@ it('preserves deleted and renamed inputs and falls back to extraction on detecti
|
||||
(candidate) => candidate.name === 'Verify localization extraction'
|
||||
)
|
||||
expect(step.env).toEqual({
|
||||
BASE_SHA: '${{ github.event.pull_request.base.sha }}',
|
||||
HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
|
||||
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
||||
})
|
||||
expect(step.run).toContain(
|
||||
'git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA"'
|
||||
)
|
||||
// The base side comes from the merge ref's first parent, so the gate needs no merge base and
|
||||
// works on a shallow checkout. The payload head SHA is no longer read: HEAD is the merged tree
|
||||
// the gate is actually deciding about.
|
||||
expect(step.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
|
||||
expect(step.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD')
|
||||
expect(step.run).not.toContain('--diff-filter')
|
||||
expect(step.run).toContain('&& [ "$scope" = false ]; then')
|
||||
expect(step.run).toMatch(/else\s+pnpm run verify:localization-extraction\s+fi/)
|
||||
|
||||
@@ -120,6 +120,8 @@ const BUNDLER_CACHE_PATHS = ['metro-cache', '.expo', 'node_modules/.cache']
|
||||
const REVIEWED_COMPUTED_PATHS = [
|
||||
'${{ steps.electron-package-cache.outputs.cache-root }}',
|
||||
'${{ steps.pnpm-store.outputs.path }}',
|
||||
// Only pnpm's lockfile-verified.jsonl record, never Metro transforms.
|
||||
'${{ steps.verification-cache.outputs.path }}',
|
||||
"${{ github.event_name != 'pull_request' && 'pnpm' || '' }} store"
|
||||
]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createBundleServer,
|
||||
installShellDouble,
|
||||
readBridgeFaultGrant,
|
||||
readBridgePagePainted,
|
||||
readBridgeProtocolVersion,
|
||||
readShellCsp
|
||||
} from './mobile-web-app-render-harness.mjs'
|
||||
@@ -110,6 +111,7 @@ let routeChunks = {}
|
||||
let cspHeader = null
|
||||
let bridgeVersion = null
|
||||
let faultGrant = null
|
||||
let paintName = null
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!bundles) {
|
||||
@@ -118,6 +120,7 @@ beforeAll(async () => {
|
||||
cspHeader = await readShellCsp()
|
||||
bridgeVersion = await readBridgeProtocolVersion()
|
||||
faultGrant = await readBridgeFaultGrant()
|
||||
paintName = await readBridgePagePainted()
|
||||
scratch = await mkdtemp(join(tmpdir(), 'orca-mobile-web-app-agent-history-'))
|
||||
const built = await buildMobileWebAppBundle({ outDir: join(scratch, 'bundle') })
|
||||
routeChunks = built.routeChunks
|
||||
@@ -227,9 +230,11 @@ async function openRoute(route, awaitText, options = {}) {
|
||||
return opened
|
||||
}
|
||||
|
||||
/** Every grant-gated notify the page posted, whole and in order, as the shell received them. */
|
||||
function readNotifies(page) {
|
||||
return page.evaluate(() => globalThis.__orcaRenderCheckNotifies ?? [])
|
||||
/** Every grant-gated notify the page posted, whole and in order, as the shell received them. The
|
||||
* paint report is not one: every page posts it once its route screen has a frame. */
|
||||
async function readNotifies(page) {
|
||||
const notifies = await page.evaluate(() => globalThis.__orcaRenderCheckNotifies ?? [])
|
||||
return notifies.filter((frame) => frame.name !== paintName)
|
||||
}
|
||||
|
||||
describeRender('the agent-history route in a real browser', () => {
|
||||
|
||||
@@ -302,9 +302,6 @@ describeDrawer('the device Back key reaching a sheet on the page', () => {
|
||||
host: SHELL_HOST,
|
||||
storage: {},
|
||||
faultGrant,
|
||||
// What a shell that knows the lane offers, and the frame it would send. Without the first
|
||||
// the page posts no claim at all, which is the older-shell arm the unit suites pin.
|
||||
accepts: [backNames.claim],
|
||||
backFrame: backNames.frame
|
||||
})
|
||||
await page.goto(`${origin}/`, { waitUntil: 'load' })
|
||||
|
||||
@@ -164,9 +164,9 @@ export async function readBrowserFrameQuality() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The page's client-identity placeholder and the `init.accepts` name that unlocks it, read from
|
||||
* the module that declares both. A rig carrying its own copy would go on passing after the real
|
||||
* pair moved, which is the whole reason every other constant here is read rather than retyped.
|
||||
* The page's client-identity placeholder, read from the module that declares it. A rig carrying its
|
||||
* own copy would go on passing after the real one moved, which is the whole reason every other
|
||||
* constant here is read rather than retyped.
|
||||
*/
|
||||
export async function readBridgePageClientIdentity() {
|
||||
const source = await readFile(
|
||||
@@ -180,10 +180,7 @@ export async function readBridgePageClientIdentity() {
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
return {
|
||||
placeholder: read('BRIDGE_PAGE_CLIENT_ID'),
|
||||
accept: read('BRIDGE_PAGE_CLIENT_IDENTITY_ACCEPT')
|
||||
}
|
||||
return { placeholder: read('BRIDGE_PAGE_CLIENT_ID') }
|
||||
}
|
||||
|
||||
/** The grant the shell offers every page, read from the same source for the same reason. */
|
||||
@@ -264,7 +261,6 @@ export function installShellDouble({
|
||||
grants,
|
||||
pageRoutes = null,
|
||||
pageRouteGrants = null,
|
||||
accepts = null,
|
||||
backFrame = null,
|
||||
replies,
|
||||
streams = [],
|
||||
@@ -323,9 +319,6 @@ export function installShellDouble({
|
||||
// Omitted when the caller names none, which is the older-shell case the page falls back
|
||||
// on: an absent field is not an empty one, and the page reads the difference.
|
||||
...(pageRouteGrants === null ? {} : { pageRouteGrants }),
|
||||
// Omitted when a check names none, which is the shell that performs no swap and the
|
||||
// state every other rig in this directory runs in.
|
||||
...(accepts === null ? {} : { accepts }),
|
||||
// Omitted for a shell too old to name one, which is the case the page has a panel for.
|
||||
...(route === null ? {} : { route }),
|
||||
...(host === null ? {} : { host }),
|
||||
|
||||
@@ -135,8 +135,7 @@ async function openPage({
|
||||
shellHost = SHELL_HOST,
|
||||
shellStorage = {},
|
||||
shellGrants,
|
||||
shellPageRoutes = null,
|
||||
shellAccepts = null
|
||||
shellPageRoutes = null
|
||||
} = {}) {
|
||||
const page = await browser.newPage({ viewport: { width: 390, height: 844 } })
|
||||
if (shellRoute !== undefined) {
|
||||
@@ -151,8 +150,7 @@ async function openPage({
|
||||
storage: shellStorage,
|
||||
faultGrant,
|
||||
grants: shellGrants ?? [faultGrant],
|
||||
pageRoutes: shellPageRoutes,
|
||||
accepts: shellAccepts
|
||||
pageRoutes: shellPageRoutes
|
||||
})
|
||||
}
|
||||
const errors = []
|
||||
@@ -587,11 +585,7 @@ describeRender('the Route A page in a real browser', () => {
|
||||
})
|
||||
)
|
||||
try {
|
||||
// The one case that advertises the report, because it is the only one asserting on it.
|
||||
const opened = await openPage({
|
||||
shellRoute: { pathname: HOST_ROUTE },
|
||||
shellAccepts: [paintName]
|
||||
})
|
||||
const opened = await openPage({ shellRoute: { pathname: HOST_ROUTE } })
|
||||
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
|
||||
await opened.page.waitForFunction(
|
||||
() => document.documentElement.dataset.orcaWebEntry === 'mounted',
|
||||
@@ -639,7 +633,6 @@ describeRender('the Route A page in a real browser', () => {
|
||||
const route = `${HOST_ROUTE}/pr/render-check-tree`
|
||||
const opened = await openPage({
|
||||
shellRoute: { pathname: route },
|
||||
shellAccepts: [paintName],
|
||||
shellPageRoutes: [HOST_ROUTE_PATTERN, '/h/[hostId]/pr/[worktreeId]']
|
||||
})
|
||||
await opened.page.goto(`${origin}/`, { waitUntil: 'load' })
|
||||
|
||||
@@ -148,8 +148,6 @@ async function openSessionWithTerminalTab() {
|
||||
faultGrant,
|
||||
grants: [faultGrant, ...sessionGrants()],
|
||||
pageRoutes: PAGE_ROUTE_PATTERNS,
|
||||
// The page claims an identity only against a shell that says it swaps one in.
|
||||
accepts: [pageClientIdentity.accept],
|
||||
streams: ['session.tabs.subscribe', 'terminal.subscribe'],
|
||||
replies: {
|
||||
'worktree.show': { worktree: { id: WORKTREE, name: WORKTREE, path: `/tmp/${WORKTREE}` } },
|
||||
|
||||
@@ -57,6 +57,8 @@ import { TerminalWebView } from ${JSON.stringify(componentPath)}
|
||||
|
||||
export default function TerminalProbeRoute() {
|
||||
const handleRef = useRef(null)
|
||||
// The terminal frame as React Native laid it out, which is what the session fits.
|
||||
const frameRef = useRef({ width: 0, height: 0 })
|
||||
const [mounted, setMounted] = useState(true)
|
||||
const onSelectionCopy = useCallback((text) => {
|
||||
globalThis.__orcaTerminalCopied = text
|
||||
@@ -71,10 +73,11 @@ export default function TerminalProbeRoute() {
|
||||
globalThis.__orcaTerminalEngineErrors = globalThis.__orcaTerminalEngineErrors ?? []
|
||||
globalThis.__orcaTerminalBeforeInput = []
|
||||
globalThis.__orcaTerminalProbe = {
|
||||
init: (cols, rows, data) => handleRef.current?.init(cols, rows, data, false, []),
|
||||
init: (cols, rows, initialData) =>
|
||||
handleRef.current?.init({ cols, rows, initialData, oscLinks: [], frame: frameRef.current }),
|
||||
write: (data) => handleRef.current?.write(data),
|
||||
selectAll: () => handleRef.current?.doSelectAll(),
|
||||
measure: () => handleRef.current?.measureFitDimensions(),
|
||||
fit: () => handleRef.current?.fitDimensions(frameRef.current),
|
||||
awaitReady: () => handleRef.current?.awaitReady(),
|
||||
setMounted: (next) => setMounted(next)
|
||||
}
|
||||
@@ -90,14 +93,21 @@ export default function TerminalProbeRoute() {
|
||||
}, [])
|
||||
return (
|
||||
<View testID="terminal-probe" style={{ flex: 1 }}>
|
||||
{mounted ? (
|
||||
<TerminalWebView
|
||||
ref={handleRef}
|
||||
onWebReady={onWebReady}
|
||||
onEngineError={onEngineError}
|
||||
onSelectionCopy={onSelectionCopy}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={{ flex: 1 }}
|
||||
onLayout={(event) => {
|
||||
frameRef.current = event.nativeEvent.layout
|
||||
}}
|
||||
>
|
||||
{mounted ? (
|
||||
<TerminalWebView
|
||||
ref={handleRef}
|
||||
onWebReady={onWebReady}
|
||||
onEngineError={onEngineError}
|
||||
onSelectionCopy={onSelectionCopy}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{/* The shape the terminal's live input takes on the page: xterm's own textarea is inert by
|
||||
the document's design, so this is where typed text arrives. */}
|
||||
<TextInput testID="terminal-live-input" style={{ fontSize: 16 }} />
|
||||
|
||||
@@ -480,6 +480,14 @@ describeRender(
|
||||
})
|
||||
await openProbeTerminal(page)
|
||||
expect(documentChunk, 'the document was served as its own chunk').not.toBe(null)
|
||||
// Ready is notified with the replay's fit still a frame away. Left pending, that fit
|
||||
// commits the resized box below, the resize refit then has nothing to do, and there is no
|
||||
// frame to hold; so the document settles first. Since the terminal is built before
|
||||
// ready, the fixture returns fast enough to land inside that frame.
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
)
|
||||
|
||||
await page.evaluate((chunk) => {
|
||||
const state = globalThis.__orcaScheduler
|
||||
@@ -627,14 +635,14 @@ describeRender(
|
||||
await page.close()
|
||||
}, 300_000)
|
||||
|
||||
it('measures a fit through the handle and records what beforeinput reports', async () => {
|
||||
it('fits through the handle from the ready box and records what beforeinput reports', async () => {
|
||||
const { page } = await openTerminal()
|
||||
await openProbeTerminal(page)
|
||||
|
||||
// The handle's own round trip: a measure is a command in and a notify back, and on the page
|
||||
// both halves are direct calls rather than a bridge. Null would mean the document answered
|
||||
// nothing, or answered a grid too small to fit.
|
||||
const fit = await page.evaluate(() => globalThis.__orcaTerminalProbe.measure())
|
||||
// The fit is the app's own, from the cell box the document put in web-ready, against the
|
||||
// frame React Native laid out, as the session's is. Null would mean the ready carried no box,
|
||||
// or a grid too small to fit.
|
||||
const fit = await page.evaluate(() => globalThis.__orcaTerminalProbe.fit())
|
||||
expect(fit).not.toBeNull()
|
||||
expect(fit.cols).toBeGreaterThanOrEqual(20)
|
||||
expect(fit.rows).toBeGreaterThanOrEqual(8)
|
||||
|
||||
@@ -68,12 +68,10 @@ export function hashedAsset(bytes, extension) {
|
||||
}
|
||||
}
|
||||
|
||||
function readIntegerConstant(source, name) {
|
||||
function readIntegerConstant(source, name, file = 'src/shared/protocol-version.ts') {
|
||||
const match = new RegExp(`export const ${name} = (\\d+)`).exec(source)
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`[mobile-web-bundle-manifest] ${name} not found in src/shared/protocol-version.ts`
|
||||
)
|
||||
throw new Error(`[mobile-web-bundle-manifest] ${name} not found in ${file}`)
|
||||
}
|
||||
return Number.parseInt(match[1], 10)
|
||||
}
|
||||
@@ -94,6 +92,13 @@ export async function readProtocolWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
/** The page's build number, parsed for the same reason the protocol window is. */
|
||||
export async function readPageVersion() {
|
||||
const file = join('src', 'shared', 'mobile-web-bundle', 'manifest-contract.ts')
|
||||
const source = await readFile(join(projectDir, file), 'utf8')
|
||||
return readIntegerConstant(source, 'MOBILE_WEB_PAGE_VERSION', file)
|
||||
}
|
||||
|
||||
export async function readDesktopVersion() {
|
||||
const packageJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf8'))
|
||||
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
||||
@@ -124,6 +129,7 @@ export async function writeMobileWebBundleTree({
|
||||
desktopVersion,
|
||||
minCompatibleRuntimeProtocolVersion: protocolWindow.minCompatibleRuntimeProtocolVersion,
|
||||
runtimeProtocolVersion: protocolWindow.runtimeProtocolVersion,
|
||||
pageVersion: await readPageVersion(),
|
||||
entrypoint: MOBILE_WEB_BUNDLE_ENTRYPOINT,
|
||||
totalBytes: assets.reduce((total, asset) => total + asset.byteLength, 0),
|
||||
assets,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
computeMobileWebBundleBuildId,
|
||||
contentTypeForExtension,
|
||||
hashedAsset,
|
||||
serializeMobileWebBundleAssets
|
||||
serializeMobileWebBundleAssets,
|
||||
sha256Hex,
|
||||
writeMobileWebBundleTree
|
||||
} from './mobile-web-bundle-manifest.mjs'
|
||||
import { MobileWebBundleManifestSchema } from '../../src/shared/mobile-web-bundle/manifest-contract.js'
|
||||
|
||||
describe('computeMobileWebBundleBuildId', () => {
|
||||
const assets = [
|
||||
@@ -55,3 +61,30 @@ describe('hashedAsset', () => {
|
||||
expect(() => hashedAsset(Buffer.from('body'), 'wasm')).toThrow(/no content type registered/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('writeMobileWebBundleTree', () => {
|
||||
it('stamps the page version the contract names, which the shell floors on', async () => {
|
||||
const outDir = await mkdtemp(join(tmpdir(), 'mobile-web-bundle-manifest-'))
|
||||
try {
|
||||
const bytes = Buffer.from('<!doctype html>\n', 'utf8')
|
||||
const entry = {
|
||||
path: 'index.html',
|
||||
bytes,
|
||||
sha256: sha256Hex(bytes),
|
||||
byteLength: bytes.byteLength,
|
||||
contentType: contentTypeForExtension('html')
|
||||
}
|
||||
await writeMobileWebBundleTree({
|
||||
outDir,
|
||||
written: [entry],
|
||||
desktopVersion: '1.0.0',
|
||||
protocolWindow: { runtimeProtocolVersion: 3, minCompatibleRuntimeProtocolVersion: 3 }
|
||||
})
|
||||
const manifest = JSON.parse(await readFile(join(outDir, 'manifest.json'), 'utf8'))
|
||||
expect(Number.isInteger(manifest.pageVersion)).toBe(true)
|
||||
expect(MobileWebBundleManifestSchema.parse(manifest).pageVersion).toBe(manifest.pageVersion)
|
||||
} finally {
|
||||
await rm(outDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,12 +97,12 @@ it('only skips the unchanged smoke after a successful diff and dependency analys
|
||||
)
|
||||
expect(step.env).toEqual({
|
||||
BASE_SHA: '${{ github.event.pull_request.base.sha }}',
|
||||
HEAD_SHA: '${{ github.event.pull_request.head.sha }}',
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
})
|
||||
expect(step.run).toContain(
|
||||
'git diff --name-only --no-renames -z --merge-base "$BASE_SHA" "$HEAD_SHA"'
|
||||
)
|
||||
// See the localization gate: HEAD^1 removes the merge-base computation, so a shallow checkout
|
||||
// is enough and the payload head SHA is unused.
|
||||
expect(step.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
|
||||
expect(step.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD')
|
||||
expect(step.run).not.toContain('--diff-filter')
|
||||
expect(step.run).toContain('&& [ "$scope" = false ]; then')
|
||||
expect(step.run).toMatch(/else\s+pnpm run smoke:orcad-terminal\s+fi/)
|
||||
|
||||
@@ -24,7 +24,7 @@ export function linuxFormatArguments({ format, appDirectory, outputDirectory })
|
||||
}
|
||||
return [
|
||||
'--config',
|
||||
'config/electron-builder.config.cjs',
|
||||
'config/electron-builder-pr-linux.config.cjs',
|
||||
'--linux',
|
||||
format,
|
||||
'--x64',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
@@ -55,7 +56,7 @@ it('preserves configured hooks, architecture, names, and PR compression with exa
|
||||
})
|
||||
expect(args).toEqual([
|
||||
'--config',
|
||||
'config/electron-builder.config.cjs',
|
||||
'config/electron-builder-pr-linux.config.cjs',
|
||||
'--linux',
|
||||
'deb',
|
||||
'--x64',
|
||||
@@ -209,3 +210,17 @@ describe('independent Linux package formats', () => {
|
||||
expect(readdirSync(outputDirectory)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
it('changes only CI deb/rpm compression levels without mutating release settings or hooks', () => {
|
||||
const require = createRequire(import.meta.url)
|
||||
const release = require('../electron-builder.config.cjs')
|
||||
const before = { deb: { ...release.deb }, rpm: { ...release.rpm } }
|
||||
const ci = require('../electron-builder-pr-linux.config.cjs')
|
||||
expect(release.deb).toEqual(before.deb)
|
||||
expect(release.rpm).toEqual(before.rpm)
|
||||
expect(ci).toEqual({
|
||||
...release,
|
||||
deb: { ...release.deb, fpm: [...(release.deb.fpm ?? []), '--deb-compression-level=1'] },
|
||||
rpm: { ...release.rpm, fpm: [...(release.rpm.fpm ?? []), '--rpm-compression-level=1'] }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,6 +152,8 @@ const CROSS_VERSION_WIRE_PREFIXES = [
|
||||
'src/main/native-chat/agent-session-journal/',
|
||||
'src/main/native-chat/agent-session-wire/',
|
||||
'src/main/runtime/agent-session-record-store',
|
||||
'src/main/runtime/agent-session-recovery-capsule',
|
||||
'src/shared/agent-session-resume-marker',
|
||||
'src/main/runtime/rpc/dispatcher',
|
||||
'src/main/runtime/rpc/methods/agent-launch',
|
||||
'src/main/runtime/rpc/methods/ai-vault.ts',
|
||||
|
||||
@@ -530,7 +530,11 @@ describe('PR Checks skip wiring', () => {
|
||||
)
|
||||
expect(classify.run).toContain('--diff-filter=ACDMR')
|
||||
expect(classify.run).toContain('--no-renames')
|
||||
expect(classify.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
|
||||
// HEAD is the merge commit, so HEAD^1 is the base side and no merge base is computed.
|
||||
// That is what lets this job check out shallowly, which every other job waits on.
|
||||
expect(classify.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"')
|
||||
expect(classify.run).toContain('"$DIFF_BASE" HEAD')
|
||||
expect(classify.run).not.toContain('--merge-base "$')
|
||||
expect(classify.run).toContain('node config/scripts/pr-code-change-scope.mjs')
|
||||
expect(classify.run).toContain('tee -a "$GITHUB_OUTPUT"')
|
||||
expect(prWorkflow.jobs.code_paths.outputs.should_run).toBe(
|
||||
@@ -582,12 +586,21 @@ describe('PR Checks skip wiring', () => {
|
||||
|
||||
it('gates each expensive job on its classifier and cache prerequisite', () => {
|
||||
for (const jobName of expensiveJobs.filter((jobName) => jobName !== 'test')) {
|
||||
expect(prWorkflow.jobs[jobName].needs, jobName).toEqual(['code_paths'])
|
||||
expect(prWorkflow.jobs[jobName].needs, jobName).toEqual(
|
||||
['package', 'package_windows'].includes(jobName)
|
||||
? ['code_paths', 'static_analysis', 'typecheck']
|
||||
: ['code_paths']
|
||||
)
|
||||
expect(prWorkflow.jobs[jobName].if, jobName).toBe(
|
||||
`needs.code_paths.outputs.${jobName} == 'true'`
|
||||
)
|
||||
}
|
||||
expect(prWorkflow.jobs.test.needs).toEqual(['code_paths', 'test_native_cache'])
|
||||
expect(prWorkflow.jobs.test.needs).toEqual([
|
||||
'code_paths',
|
||||
'test_native_cache',
|
||||
'static_analysis',
|
||||
'typecheck'
|
||||
])
|
||||
expect(prWorkflow.jobs.test.if).toContain("needs.code_paths.outputs.test == 'true'")
|
||||
expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'success'")
|
||||
expect(prWorkflow.jobs.test.if).toContain("needs.test_native_cache.result == 'skipped'")
|
||||
|
||||
@@ -238,7 +238,7 @@ describe('PR E2E gate contract', () => {
|
||||
})
|
||||
|
||||
it('scopes detection to the PR range so base drift cannot false-trigger', () => {
|
||||
expect(filterStep.run).toContain('--merge-base "$BASE" "$HEAD"')
|
||||
expect(filterStep.run).toMatch(/diff-base\.mjs "\$BASE"[\s\S]*"\$DIFF_BASE" HEAD/)
|
||||
expect(filterStep.run).toContain('set -euo pipefail')
|
||||
})
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('PR E2E gate contract', () => {
|
||||
})
|
||||
|
||||
it('scopes the VM rollback oracle to the PR range and recipe schema authorities', () => {
|
||||
expect(rollbackStep.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
|
||||
expect(rollbackStep.run).toMatch(/diff-base\.mjs "\$BASE_SHA"[\s\S]*"\$DIFF_BASE" HEAD --/)
|
||||
expect(rollbackStep.run).toContain('src/shared/ephemeral-vm-recipes.ts')
|
||||
expect(rollbackStep.run).toContain('src/shared/orca-yaml-hook-types.ts')
|
||||
expect(selectPrE2eSpecs(['src/shared/ephemeral-vm-recipes.ts'])).toEqual([
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export function prCheckRunTitle({ number, sourceSha, workflowSha }) {
|
||||
return `PR ${number} | source ${sourceSha} | workflow ${workflowSha}`
|
||||
export function prCheckRunTitle({ number, sourceSha, workflowSha, unitMode = 'full' }) {
|
||||
return `PR ${number} | source ${sourceSha} | workflow ${workflowSha} | unit ${unitMode}`
|
||||
}
|
||||
|
||||
export function reusablePrCheckRun(runs, identity) {
|
||||
|
||||
@@ -45,6 +45,12 @@ describe('ready-for-review required check reuse', () => {
|
||||
it('reuses a completed success only for the identical PR, merge source and workflow', () => {
|
||||
expect(reusablePrCheckRun([passed], identity)).toBe(passed)
|
||||
expect(reusablePrCheckRun([], identity)).toBeUndefined()
|
||||
expect(
|
||||
reusablePrCheckRun(
|
||||
[{ ...passed, display_title: prCheckRunTitle({ ...identity, unitMode: 'selected' }) }],
|
||||
identity
|
||||
)
|
||||
).toBeUndefined()
|
||||
for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha', 'runId']) {
|
||||
const changed = key === 'number' ? 43 : key === 'runId' ? '123' : 'd'.repeat(40)
|
||||
expect(reusablePrCheckRun([passed], { ...identity, [key]: changed }), key).toBeUndefined()
|
||||
@@ -121,7 +127,7 @@ describe('ready-for-review required check reuse', () => {
|
||||
|
||||
it('keeps required skips conditional on proof and leaves advisory routing eligible', () => {
|
||||
expect(workflow['run-name']).toBe(
|
||||
'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
|
||||
"PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}"
|
||||
)
|
||||
expect(workflow.on.pull_request.types).toContain('ready_for_review')
|
||||
const detector = workflow.jobs.code_paths
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { existsSync, globSync, readFileSync } from 'node:fs'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { UNIT_EXCLUDE } from './ci-unit-files.mjs'
|
||||
import { mobileWebCheckArgs } from './run-mobile-web-app-checks.mjs'
|
||||
import { MOBILE_WEB_APP_DEPENDENCIES_REQUIRED_ENV } from './mobile-web-app-bundle-dependencies.mjs'
|
||||
|
||||
@@ -105,15 +106,13 @@ describe('PR workflow parallelism', () => {
|
||||
expect(nodeNextWorkflow.on.schedule).toHaveLength(1)
|
||||
expect(nodeNextWorkflow.on.workflow_dispatch).toBeNull()
|
||||
expect(sharedTest.strategy.matrix.node).toBe('${{ fromJSON(inputs.node_versions) }}')
|
||||
expect(sharedTest.strategy.matrix.shard).toEqual(
|
||||
Array.from({ length: 8 }, (_, index) => index + 1)
|
||||
)
|
||||
expect(sharedTest.strategy.matrix.shard_total).toEqual([8])
|
||||
expect(sharedTest.strategy.matrix.shard).toBe('${{ fromJSON(needs.plan.outputs.shards) }}')
|
||||
expect(sharedTest.needs).toBe('plan')
|
||||
expect(installStep.with['node-version']).toBe('${{ matrix.node }}')
|
||||
expect(installStep.with['cache-electron-package']).toBe('true')
|
||||
expect(testStep.run).toContain('--shard=${{ matrix.shard }}/${{ matrix.shard_total }}')
|
||||
expect(testStep.run).toContain('--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }}')
|
||||
for (const testFile of nativeShellContractFiles) {
|
||||
expect(testStep.run).toContain(`--exclude=${testFile}`)
|
||||
expect(UNIT_EXCLUDE).toContain(testFile)
|
||||
}
|
||||
expect(primerInstall.with['native-runtime']).toBe('node')
|
||||
expect(primerInstall.with['node-version']).toBe('24')
|
||||
@@ -339,9 +338,10 @@ describe('PR workflow parallelism', () => {
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
|
||||
for (const jobName of ['typecheck', 'git_compatibility', 'xterm_patch_sync']) {
|
||||
for (const jobName of ['typecheck', 'git_compatibility']) {
|
||||
expect(installFor(jobName).with, jobName).toBeUndefined()
|
||||
}
|
||||
expect(installFor('xterm_patch_sync')).toBeUndefined()
|
||||
expect(installFor('static_analysis').with['native-runtime']).toBe('node')
|
||||
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
|
||||
expect(sharedTestInstall.with['native-runtime']).toBe('node')
|
||||
@@ -456,7 +456,9 @@ describe('PR workflow parallelism', () => {
|
||||
expect(electronCacheResolution.if).toBe(
|
||||
"inputs.native-runtime == 'electron' || inputs.cache-electron-package == 'true'"
|
||||
)
|
||||
expect(electronCache.if).toBe(electronCacheResolution.if)
|
||||
expect(electronCache.if).toBe(
|
||||
"(github.event_name != 'pull_request' || runner.os != 'Linux') && steps.electron-package-cache.outputs.version != ''"
|
||||
)
|
||||
expect(electronCache.uses).toBe('actions/cache@v5')
|
||||
expect(electronCache.with.key).toContain('steps.electron-package-cache.outputs.version')
|
||||
expect(dependencyAction.inputs['cache-electron-package'].default).toBe('false')
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
function reviewNumber(inputs) {
|
||||
if (/^[1-9]\d*$/.test(inputs.pull_request_number ?? '')) {
|
||||
return Number(inputs.pull_request_number)
|
||||
}
|
||||
const match =
|
||||
/ \| PR ([1-9]\d*)$/.exec(inputs.name ?? '') ??
|
||||
/^Review (?:new commits on )?#([1-9]\d*) \[[\w-]+\]$/.exec(inputs.name ?? '')
|
||||
return match ? Number(match[1]) : null
|
||||
}
|
||||
|
||||
function reviewRunOrder(runs, number, runId) {
|
||||
const related = runs.filter((run) => reviewNumber({ name: run.display_title }) === number)
|
||||
return {
|
||||
superseded: related.some((run) => run.id > runId),
|
||||
older: related
|
||||
.filter(
|
||||
(run) =>
|
||||
run.id < runId &&
|
||||
['queued', 'in_progress', 'waiting', 'pending', 'requested'].includes(run.status)
|
||||
)
|
||||
.map((run) => run.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function reviewScope({ github, context, core }) {
|
||||
const inputs = context.payload.inputs ?? {}
|
||||
const number = reviewNumber(inputs)
|
||||
core.setOutput('current', 'true')
|
||||
if (!number || !Number.isSafeInteger(number)) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number })
|
||||
if (pr.state !== 'open' || (inputs.head_sha && inputs.head_sha !== pr.head.sha)) {
|
||||
core.setOutput('current', 'false')
|
||||
return
|
||||
}
|
||||
core.setOutput('head', pr.head.sha)
|
||||
core.setOutput('number', String(number))
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
...context.repo,
|
||||
workflow_id: 'pullfrog.yml',
|
||||
event: 'workflow_dispatch',
|
||||
per_page: 100
|
||||
})
|
||||
const order = reviewRunOrder(data.workflow_runs, number, context.runId)
|
||||
if (order.superseded) {
|
||||
core.setOutput('current', 'false')
|
||||
return
|
||||
}
|
||||
// Run IDs, unlike scope-job completion order, cannot let an older review cancel a newer one.
|
||||
for (const runId of order.older) {
|
||||
try {
|
||||
await github.rest.actions.cancelWorkflowRun({ ...context.repo, run_id: runId })
|
||||
} catch (error) {
|
||||
core.warning(`Could not cancel older review ${runId}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(`Review identity unavailable; leaving this task independent: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { reviewNumber, reviewRunOrder, reviewScope }
|
||||
@@ -0,0 +1,109 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import scope from './pullfrog-review-scope.cjs'
|
||||
|
||||
it('groups a current review by PR while leaving other agent tasks independent', async () => {
|
||||
const get = vi.fn(async () => ({ data: { state: 'open', head: { sha: 'current' } } }))
|
||||
const core = { setOutput: vi.fn(), warning: vi.fn() }
|
||||
const context = {
|
||||
repo: { owner: 'stablyai', repo: 'orca' },
|
||||
runId: 7,
|
||||
payload: { inputs: { name: 'Review #42 [abc]' } }
|
||||
}
|
||||
const github = {
|
||||
rest: {
|
||||
pulls: { get },
|
||||
actions: { listWorkflowRuns: async () => ({ data: { workflow_runs: [] } }) }
|
||||
}
|
||||
}
|
||||
await scope.reviewScope({ core, context, github })
|
||||
expect(get).toHaveBeenCalledWith({ owner: 'stablyai', repo: 'orca', pull_number: 42 })
|
||||
expect(core.setOutput).toHaveBeenCalledWith('head', 'current')
|
||||
get.mockClear()
|
||||
await scope.reviewScope({
|
||||
core,
|
||||
github,
|
||||
context: { ...context, payload: { inputs: { name: 'Investigate #42' } } }
|
||||
})
|
||||
expect(get).not.toHaveBeenCalled()
|
||||
expect(core.setOutput).toHaveBeenLastCalledWith('current', 'true')
|
||||
})
|
||||
|
||||
it('coalesces only explicit or recognized PR review identities', () => {
|
||||
expect(scope.reviewNumber({ name: 'Review #23532 [85jpk]' })).toBe(23532)
|
||||
expect(scope.reviewNumber({ name: 'Review new commits on #22727 [24lpq]' })).toBe(22727)
|
||||
expect(scope.reviewNumber({ pull_request_number: '123' })).toBe(123)
|
||||
for (const name of ['Fix #123', 'Review #12; echo test', 'Review #123', '', 'Review #0 [abc]']) {
|
||||
expect(scope.reviewNumber({ name })).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('skips closed or explicitly stale reviews and tolerates lookup failure', async () => {
|
||||
for (const data of [
|
||||
{ state: 'closed', head: { sha: 'a' } },
|
||||
{ state: 'open', head: { sha: 'b' } }
|
||||
]) {
|
||||
const core = { setOutput: vi.fn(), warning: vi.fn() }
|
||||
await scope.reviewScope({
|
||||
core,
|
||||
context: {
|
||||
repo: {},
|
||||
runId: 1,
|
||||
payload: { inputs: { pull_request_number: '1', head_sha: 'a' } }
|
||||
},
|
||||
github: { rest: { pulls: { get: async () => ({ data }) } } }
|
||||
})
|
||||
expect(core.setOutput).toHaveBeenCalledWith('current', 'false')
|
||||
}
|
||||
const core = { setOutput: vi.fn(), warning: vi.fn() }
|
||||
await scope.reviewScope({
|
||||
core,
|
||||
context: { repo: {}, runId: 2, payload: { inputs: { pull_request_number: '1' } } },
|
||||
github: {
|
||||
rest: {
|
||||
pulls: {
|
||||
get: async () => {
|
||||
throw new Error('offline')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(core.setOutput).toHaveBeenCalledWith('current', 'true')
|
||||
expect(core.warning).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('a delayed older scope cannot cancel or replace a newer review', async () => {
|
||||
const runs = [
|
||||
{ id: 6, display_title: 'Review #42 [a]', status: 'in_progress' },
|
||||
{ id: 8, display_title: 'Custom review | PR 42', status: 'queued' },
|
||||
{ id: 5, display_title: 'Investigate #42', status: 'in_progress' },
|
||||
{ id: 4, display_title: 'Review #43 [other]', status: 'in_progress' }
|
||||
]
|
||||
for (const runId of [7, 9]) {
|
||||
const cancelWorkflowRun = vi.fn(async () => ({}))
|
||||
const core = { setOutput: vi.fn(), warning: vi.fn() }
|
||||
await scope.reviewScope({
|
||||
context: {
|
||||
repo: { owner: 'stablyai', repo: 'orca' },
|
||||
runId,
|
||||
payload: { inputs: { name: 'Review #42 [current]' } }
|
||||
},
|
||||
core,
|
||||
github: {
|
||||
rest: {
|
||||
pulls: { get: async () => ({ data: { state: 'open', head: { sha: 'head' } } }) },
|
||||
actions: {
|
||||
listWorkflowRuns: async () => ({ data: { workflow_runs: runs } }),
|
||||
cancelWorkflowRun
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (runId === 7) {
|
||||
expect(cancelWorkflowRun).not.toHaveBeenCalled()
|
||||
expect(core.setOutput).toHaveBeenCalledWith('current', 'false')
|
||||
} else {
|
||||
expect(cancelWorkflowRun.mock.calls.map(([args]) => args.run_id)).toEqual([6, 8])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -236,6 +236,7 @@ function ensureDependencies(upstreamRoot, manifest) {
|
||||
const stamp = path.join(upstreamRoot, 'node_modules', '.orca-xterm-install-stamp')
|
||||
const want = `${manifest.upstream.commit}\n${statSync(lockfile).size}\n`
|
||||
if (existsSync(stamp) && readFileSync(stamp, 'utf8') === want) {
|
||||
assertToolchain(upstreamRoot, manifest)
|
||||
return
|
||||
}
|
||||
run('npm', ['ci'], {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Enforced by .github/workflows/release-policy.yml on release events.
|
||||
|
||||
const BOT_LOGIN = 'github-actions[bot]'
|
||||
const BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
const NUMBER = '(?:0|[1-9][0-9]*)'
|
||||
const VERSION = `${NUMBER}\\.${NUMBER}\\.${NUMBER}`
|
||||
const STABLE_TAG = new RegExp(`^v${VERSION}$`)
|
||||
const RC_TAG = new RegExp(`^v${VERSION}-rc\\.${NUMBER}(?:\\.[0-9A-Za-z]+)?$`)
|
||||
const MOBILE_TAG = new RegExp(`^mobile(?:-android)?-v${VERSION}$`)
|
||||
|
||||
export function isPrereleaseTag(tag) {
|
||||
return RC_TAG.test(tag) || MOBILE_TAG.test(tag)
|
||||
}
|
||||
|
||||
export function compareStableTags(left, right) {
|
||||
const a = left.slice(1).split('.').map(Number)
|
||||
const b = right.slice(1).split('.').map(Number)
|
||||
return a.reduce((result, part, index) => result || part - b[index], 0)
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
// Why: Cut Release creates the annotated tag and its `release: vX` commit, but the
|
||||
// release object keeps whoever created the draft first, which can be a person.
|
||||
export async function isCutByReleasePipeline(github, { owner, repo }, tag) {
|
||||
if (!STABLE_TAG.test(tag) && !RC_TAG.test(tag)) {
|
||||
return false
|
||||
}
|
||||
let ref
|
||||
try {
|
||||
;({ data: ref } = await github.rest.git.getRef({ owner, repo, ref: `tags/${tag}` }))
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
return false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
if (ref.object.type !== 'tag') {
|
||||
return false
|
||||
}
|
||||
const { data: tagObject } = await github.rest.git.getTag({
|
||||
owner,
|
||||
repo,
|
||||
tag_sha: ref.object.sha
|
||||
})
|
||||
if (tagObject.tagger?.email !== BOT_EMAIL || tagObject.object.type !== 'commit') {
|
||||
return false
|
||||
}
|
||||
const { data: commit } = await github.rest.git.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: tagObject.object.sha
|
||||
})
|
||||
const subject = commit.message.split('\n')[0]
|
||||
return new RegExp(`^release: ${escapeRegExp(tag)}(?: \\[rc-slot:[^\\]]+\\])?$`).test(subject)
|
||||
}
|
||||
|
||||
async function isAuthorizedRelease(github, repoRef, release) {
|
||||
const tag = release.tag_name
|
||||
if (!STABLE_TAG.test(tag) && !isPrereleaseTag(tag)) {
|
||||
return false
|
||||
}
|
||||
return release.author?.login === BOT_LOGIN || isCutByReleasePipeline(github, repoRef, tag)
|
||||
}
|
||||
|
||||
// Why: the highest authorized stable release must be Latest; GitHub's own pick
|
||||
// (and the old bot-author-only filter) can move it to an older version.
|
||||
export async function restoreLatestStable(github, repoRef) {
|
||||
const releases = await github.paginate(github.rest.repos.listReleases, {
|
||||
...repoRef,
|
||||
per_page: 100
|
||||
})
|
||||
const candidates = releases
|
||||
.filter((release) => !release.draft && !release.prerelease && STABLE_TAG.test(release.tag_name))
|
||||
.sort((left, right) => compareStableTags(right.tag_name, left.tag_name))
|
||||
for (const release of candidates) {
|
||||
if (!(await isAuthorizedRelease(github, repoRef, release))) {
|
||||
continue
|
||||
}
|
||||
await github.rest.repos.updateRelease({
|
||||
...repoRef,
|
||||
release_id: release.id,
|
||||
make_latest: 'true'
|
||||
})
|
||||
return release.tag_name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function enforceReleasePolicy({ github, context, core }) {
|
||||
const { release, action } = context.payload
|
||||
const tag = release.tag_name
|
||||
const author = release.author?.login
|
||||
const repoRef = context.repo
|
||||
const expectedPrerelease = isPrereleaseTag(tag)
|
||||
|
||||
if (await isAuthorizedRelease(github, repoRef, release)) {
|
||||
if (release.prerelease !== expectedPrerelease) {
|
||||
await github.rest.repos.updateRelease({
|
||||
...repoRef,
|
||||
release_id: release.id,
|
||||
prerelease: expectedPrerelease,
|
||||
make_latest: expectedPrerelease ? 'false' : 'legacy'
|
||||
})
|
||||
}
|
||||
await restoreLatestStable(github, repoRef)
|
||||
return
|
||||
}
|
||||
|
||||
// Why: editing notes on a live release is not a publication; deleting on edit
|
||||
// took down the v1.4.216 release and its tag.
|
||||
if (action !== 'published') {
|
||||
core.warning(
|
||||
`Release ${tag} by ${author || 'unknown'} is outside release policy; left as-is on "${action}".`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
...repoRef,
|
||||
release_id: release.id,
|
||||
draft: true,
|
||||
prerelease: true,
|
||||
make_latest: 'false'
|
||||
})
|
||||
await restoreLatestStable(github, repoRef)
|
||||
await github.rest.repos.deleteRelease({ ...repoRef, release_id: release.id })
|
||||
try {
|
||||
await github.rest.git.deleteRef({ ...repoRef, ref: `tags/${tag}` })
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
core.warning(`Deleted unauthorized release ${tag} created by ${author || 'unknown'}.`)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { enforceReleasePolicy, restoreLatestStable } from './release-policy.mjs'
|
||||
|
||||
const BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
const repoRef = { owner: 'stablyai', repo: 'orca' }
|
||||
|
||||
function notFound() {
|
||||
return Object.assign(new Error('Not Found'), { status: 404 })
|
||||
}
|
||||
|
||||
// tags: { [tag]: { tagger, subject } } for annotated tags; missing tags 404.
|
||||
function createGithub({ releases = [], tags = {} } = {}) {
|
||||
const rest = {
|
||||
repos: {
|
||||
listReleases: vi.fn(),
|
||||
updateRelease: vi.fn(async () => ({ data: {} })),
|
||||
deleteRelease: vi.fn(async () => ({ data: {} }))
|
||||
},
|
||||
git: {
|
||||
getRef: vi.fn(async ({ ref }) => {
|
||||
const tag = ref.replace(/^tags\//, '')
|
||||
if (!tags[tag]) {
|
||||
throw notFound()
|
||||
}
|
||||
return { data: { object: { type: 'tag', sha: `tag-${tag}` } } }
|
||||
}),
|
||||
getTag: vi.fn(async ({ tag_sha }) => {
|
||||
const tag = tag_sha.replace(/^tag-/, '')
|
||||
return {
|
||||
data: {
|
||||
tagger: { email: tags[tag].tagger ?? BOT_EMAIL },
|
||||
object: { type: 'commit', sha: `commit-${tag}` }
|
||||
}
|
||||
}
|
||||
}),
|
||||
getCommit: vi.fn(async ({ commit_sha }) => {
|
||||
const tag = commit_sha.replace(/^commit-/, '')
|
||||
return { data: { message: `${tags[tag].subject ?? `release: ${tag}`}\n` } }
|
||||
}),
|
||||
deleteRef: vi.fn(async () => ({ data: {} }))
|
||||
}
|
||||
}
|
||||
return { rest, paginate: vi.fn(async () => releases) }
|
||||
}
|
||||
|
||||
function release(tag, author, overrides = {}) {
|
||||
return {
|
||||
id: Number(tag.replace(/\D/g, '')),
|
||||
tag_name: tag,
|
||||
author: { login: author },
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function run(github, payloadRelease, action) {
|
||||
const core = { warning: vi.fn() }
|
||||
const context = { payload: { release: payloadRelease, action }, repo: repoRef }
|
||||
return enforceReleasePolicy({ github, context, core }).then(() => core)
|
||||
}
|
||||
|
||||
describe('release policy', () => {
|
||||
it('keeps a pipeline-cut release whose draft a person created', async () => {
|
||||
const r216 = release('v1.4.216', 'Jinwoo-H')
|
||||
const github = createGithub({
|
||||
releases: [r216, release('v1.4.214', 'github-actions[bot]')],
|
||||
tags: { 'v1.4.216': {}, 'v1.4.214': {} }
|
||||
})
|
||||
|
||||
await run(github, r216, 'edited')
|
||||
|
||||
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
|
||||
expect(github.rest.git.deleteRef).not.toHaveBeenCalled()
|
||||
expect(github.rest.repos.updateRelease).toHaveBeenCalledWith({
|
||||
...repoRef,
|
||||
release_id: r216.id,
|
||||
make_latest: 'true'
|
||||
})
|
||||
})
|
||||
|
||||
it('never deletes an unauthorized release on edit', async () => {
|
||||
const qa = release('v1.4.300', 'someone')
|
||||
const github = createGithub({ releases: [qa] })
|
||||
|
||||
const core = await run(github, qa, 'edited')
|
||||
|
||||
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
|
||||
expect(github.rest.git.deleteRef).not.toHaveBeenCalled()
|
||||
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('left as-is'))
|
||||
})
|
||||
|
||||
it('deletes a published release whose tag was not cut by the pipeline', async () => {
|
||||
const qa = release('v1.4.300', 'someone')
|
||||
const github = createGithub({
|
||||
releases: [qa, release('v1.4.215', 'nwparker')],
|
||||
tags: { 'v1.4.300': { tagger: 'someone@example.com' }, 'v1.4.215': {} }
|
||||
})
|
||||
|
||||
await run(github, qa, 'published')
|
||||
|
||||
expect(github.rest.repos.deleteRelease).toHaveBeenCalledWith({ ...repoRef, release_id: qa.id })
|
||||
expect(github.rest.git.deleteRef).toHaveBeenCalledWith({ ...repoRef, ref: 'tags/v1.4.300' })
|
||||
})
|
||||
|
||||
it('rejects a bot-tagged tag whose commit is not a release commit', async () => {
|
||||
const qa = release('v1.4.300', 'someone')
|
||||
const github = createGithub({ tags: { 'v1.4.300': { subject: 'fix: something' } } })
|
||||
|
||||
await run(github, qa, 'published')
|
||||
|
||||
expect(github.rest.repos.deleteRelease).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts scheduled RC release commits', async () => {
|
||||
const rc = release('v1.4.217-rc.1', 'someone', { prerelease: true })
|
||||
const github = createGithub({
|
||||
tags: { 'v1.4.217-rc.1': { subject: 'release: v1.4.217-rc.1 [rc-slot:2026-09-28-15]' } }
|
||||
})
|
||||
|
||||
await run(github, rc, 'published')
|
||||
|
||||
expect(github.rest.repos.deleteRelease).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('restores Latest to the highest pipeline-cut stable, not only bot-authored ones', async () => {
|
||||
const github = createGithub({
|
||||
releases: [
|
||||
release('v1.4.214', 'github-actions[bot]'),
|
||||
release('v1.4.215', 'nwparker'),
|
||||
release('v1.4.216-rc.0', 'github-actions[bot]', { prerelease: true })
|
||||
],
|
||||
tags: { 'v1.4.214': {}, 'v1.4.215': {} }
|
||||
})
|
||||
|
||||
await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.215')
|
||||
expect(github.rest.repos.updateRelease).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('compares versions numerically when picking Latest', async () => {
|
||||
const github = createGithub({
|
||||
releases: [
|
||||
release('v1.4.99', 'github-actions[bot]'),
|
||||
release('v1.4.100', 'github-actions[bot]')
|
||||
]
|
||||
})
|
||||
|
||||
await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.100')
|
||||
})
|
||||
})
|
||||
@@ -388,7 +388,9 @@ async function main() {
|
||||
child.kill('SIGTERM')
|
||||
const exited = await Promise.race([
|
||||
new Promise((r) => child.on('exit', () => r(true))),
|
||||
new Promise((r) => setTimeout(() => r(false), SHUTDOWN_TIMEOUT_MS))
|
||||
// unref'd: the loser of this race must not hold the event loop open after the
|
||||
// winner already decided. The timer still bounds the wait.
|
||||
new Promise((r) => setTimeout(() => r(false), SHUTDOWN_TIMEOUT_MS).unref())
|
||||
])
|
||||
if (!exited) {
|
||||
child.kill('SIGKILL')
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const readYaml = (path) => parse(readFileSync(new URL(path, import.meta.url), 'utf8'))
|
||||
const action = readYaml('../../.github/actions/prepare-xterm-dependencies/action.yml')
|
||||
const workflow = readYaml('../../.github/workflows/ci-xterm-cache.yml')
|
||||
const restore = action.runs.steps.find((step) => step.id === 'restore')
|
||||
|
||||
it('separates toolchains and restores dependencies without a stale fallback or build results', () => {
|
||||
for (const input of [
|
||||
'runner.os',
|
||||
'runtime.outputs.image',
|
||||
'runner.arch',
|
||||
'runtime.outputs.node',
|
||||
'xterm-upstream.json',
|
||||
'regenerate-xterm-patches.mjs',
|
||||
'xterm-patch-text.mjs',
|
||||
'prepare-xterm-dependencies/action.yml'
|
||||
]) {
|
||||
expect(restore.with.key).toContain(input)
|
||||
}
|
||||
expect(restore.uses).toBe('actions/cache/restore@v5')
|
||||
expect(restore.with['restore-keys']).toBeUndefined()
|
||||
expect(restore.with.path.trim().split('\n')).toEqual([
|
||||
'${{ runner.temp }}/xterm-patch-build/upstream/.git',
|
||||
'${{ runner.temp }}/xterm-patch-build/upstream/node_modules'
|
||||
])
|
||||
})
|
||||
|
||||
it('publishes only from main after a successful fresh verification, and skips work on hits', () => {
|
||||
expect(workflow.on.pull_request).toBeUndefined()
|
||||
expect(workflow.on.push.branches).toEqual(['main'])
|
||||
expect(workflow.jobs.seed.if).toBe("github.ref == 'refs/heads/main'")
|
||||
expect(workflow.permissions).toEqual({ contents: 'read' })
|
||||
const steps = workflow.jobs.seed.steps
|
||||
const verify = steps.find((step) => step.name === 'Verify and populate dependencies')
|
||||
const save = steps.find((step) => step.uses === 'actions/cache/save@v5')
|
||||
expect(verify.if).toBe("steps.cache.outputs.cache-hit != 'true'")
|
||||
expect(save.if).toBe(verify.if)
|
||||
expect(save.with.path).toBe(restore.with.path)
|
||||
expect(save.with.key).toBe('${{ steps.cache.outputs.cache-key }}')
|
||||
expect(steps.indexOf(save)).toBeGreaterThan(steps.indexOf(verify))
|
||||
expect(verify.run).toContain('regenerate-xterm-patches.mjs --check')
|
||||
const pr = readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync.steps
|
||||
expect(workflow.jobs.seed['runs-on']).toBe(
|
||||
readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync['runs-on']
|
||||
)
|
||||
expect(pr.some((step) => step.uses === './.github/actions/prepare-xterm-dependencies')).toBe(true)
|
||||
expect(pr.some((step) => /cache(?:\/save)?@/.test(step.uses ?? ''))).toBe(false)
|
||||
expect(pr.at(-1).run).toContain('regenerate-xterm-patches.mjs --check')
|
||||
})
|
||||
|
||||
it('restores download fallback only on a miss and preserves its established key and paths', () => {
|
||||
const fallback = action.runs.steps.at(-1)
|
||||
const save = workflow.jobs.seed.steps.at(-1)
|
||||
expect(fallback.if).toBe("steps.restore.outputs.cache-hit != 'true'")
|
||||
expect(fallback.uses).toBe('actions/cache/restore@v5')
|
||||
expect(fallback.with).toEqual(save.with)
|
||||
expect(save.if).toBe("steps.cache.outputs.cache-hit != 'true'")
|
||||
})
|
||||
|
||||
it('runs the standalone generator without installing unrelated Orca dependencies', () => {
|
||||
const steps = readYaml('../../.github/workflows/pr.yml').jobs.xterm_patch_sync.steps
|
||||
expect(steps.some((step) => step.uses === './.github/actions/install-node-dependencies')).toBe(
|
||||
false
|
||||
)
|
||||
expect(steps.find((step) => step.uses === 'actions/setup-node@v6').with).toEqual({
|
||||
'node-version-file': 'package.json',
|
||||
'package-manager-cache': false
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,9 @@
|
||||
"../src/main/agent-state-file-reader.ts",
|
||||
"../src/main/agent-hooks/grok-replay-guard.ts",
|
||||
"../src/main/claude/hook-script.ts",
|
||||
"../src/main/claude/claude-session-end-hook-capability.ts",
|
||||
"../src/main/claude/claude-hook-event-versions.ts",
|
||||
"../src/main/claude/claude-managed-hook-events.ts",
|
||||
"../src/main/qoder/hook-service.ts",
|
||||
"../src/main/agent-hooks/hook-stdin-contract.ts",
|
||||
"../src/main/agent-hooks/hook-post-command.ts",
|
||||
"../src/main/agent-hooks/hook-config-write-path.ts",
|
||||
@@ -18,6 +20,7 @@
|
||||
"../src/main/agent-hooks/managed-agent-hook-controls.ts",
|
||||
"../src/main/agent-hooks/managed-agent-hook-registry.ts",
|
||||
"../src/main/agent-hooks/managed-hook-script-refresh.ts",
|
||||
"../src/main/agent-hooks/managed-hooks-json-events.ts",
|
||||
"../src/main/agent-hooks/posix-hook-command.ts",
|
||||
"../src/main/agent-hooks/runtime-home-hook-command.ts",
|
||||
"../src/main/orca-profiles/profile-storage-paths.ts",
|
||||
@@ -207,6 +210,9 @@
|
||||
"../src/main/in-flight-run-dedupe.ts",
|
||||
"../src/main/kimi/hook-service.ts",
|
||||
"../src/main/kimi/kimi-hook-config-toml.ts",
|
||||
"../src/main/dsh/dsh-home-patch.ts",
|
||||
"../src/main/dsh/hook-service.ts",
|
||||
"../src/main/dsh/hook-settings.ts",
|
||||
"../src/main/muse/hook-config-json.ts",
|
||||
"../src/main/muse/hook-service.ts",
|
||||
"../src/main/muse/hook-settings.ts",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"./scripts/vitest-host-ports-setup.ts",
|
||||
"../src/main/**/*",
|
||||
"../src/renderer/src/lib/skill-freshness-display-status.ts",
|
||||
"../src/renderer/src/components/native-chat/native-chat-resolution-receipt.ts",
|
||||
"../src/renderer/src/components/native-chat/structured-agent-question-projection.ts",
|
||||
"../src/preload/**/*",
|
||||
"../src/shared/**/*",
|
||||
"../src/relay/**/*",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { UNIT_INCLUDE, UNIT_EXCLUDE } from './scripts/ci-unit-files.mjs'
|
||||
import TimingSequencer from './scripts/ci-unit-sequencer.mjs'
|
||||
|
||||
const windowsTestWorkerOptions = process.platform === 'win32' ? { maxWorkers: 4 } : {}
|
||||
@@ -33,14 +34,8 @@ export default defineConfig({
|
||||
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
|
||||
resolve('config/scripts/vitest-host-ports-setup.ts')
|
||||
],
|
||||
include: [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'config/scripts/**/*.test.ts',
|
||||
'config/scripts/**/*.test.mjs',
|
||||
'tests/tools/**/*.test.mjs',
|
||||
'tests/e2e/**/*.unit.test.ts'
|
||||
],
|
||||
include: UNIT_INCLUDE,
|
||||
...(process.env.ORCA_BALANCE_UNIT_SHARDS === '1' ? { exclude: UNIT_EXCLUDE } : {}),
|
||||
// Why: the full suite runs heavy TS transforms plus real git/http fixtures;
|
||||
// the Vitest 5s defaults are too tight for the slowest integration cases.
|
||||
hookTimeout: 60_000,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 81m">
|
||||
<title>downloads: 81m</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="downloads: 82m">
|
||||
<title>downloads: 82m</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text x="37" y="15" fill="#010101" fill-opacity=".3">downloads</text>
|
||||
<text x="37" y="14">downloads</text>
|
||||
<text x="90" y="15" fill="#010101" fill-opacity=".3">81m</text>
|
||||
<text x="90" y="14">81m</text>
|
||||
<text x="90" y="15" fill="#010101" fill-opacity=".3">82m</text>
|
||||
<text x="90" y="14">82m</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 935 B After Width: | Height: | Size: 935 B |
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 384 KiB |
@@ -179,6 +179,7 @@ Funciona con **cualquier agente CLI** — si corre en una terminal, corre en Orc
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a>
|
||||
@@ -193,6 +194,7 @@ Funciona con **cualquier agente CLI** — si corre en una terminal, corre en Orc
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
Surveillez et pilotez vos agents depuis votre téléphone — soyez notifié quand un agent termine, et envoyez des instructions de suivi où que vous soyez.
|
||||
|
||||
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
[App Store iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
@@ -183,6 +183,7 @@ Fonctionne avec **n'importe quel agent CLI** — s'il tourne dans un terminal, i
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Logo Cursor" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="Logo GitHub Copilot" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Logo Muse" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="Logo ZCode" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="Logo OpenCode" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="Logo MiMo Code" width="16" valign="middle" /> MiMo Code</kbd></a>
|
||||
@@ -199,6 +200,7 @@ Fonctionne avec **n'importe quel agent CLI** — s'il tourne dans un terminal, i
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Logo Charm" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Logo Cline" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Logo Codebuff" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Logo Freebuff" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Logo Command Code" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Logo Continue" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Logo Droid" width="16" valign="middle" /> Droid</kbd></a>
|
||||
@@ -236,7 +238,7 @@ yay -S stably-orca-bin
|
||||
|
||||
Associez-la à l'app de bureau pour surveiller et piloter vos agents depuis votre téléphone.
|
||||
|
||||
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [rejoindre TestFlight](https://testflight.apple.com/join/YjeGMQBA)
|
||||
- **iOS :** [Télécharger sur l'App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
|
||||
- **Android :** [Télécharger l'APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
|
||||
|
||||
---
|
||||
|
||||
@@ -179,6 +179,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a>
|
||||
@@ -193,6 +194,7 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다.
|
||||
|
||||
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
|
||||
[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile)
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
@@ -179,6 +179,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="MiMo Code logo" width="16" valign="middle" /> MiMo Code</kbd></a>
|
||||
@@ -195,6 +196,7 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a>
|
||||
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
|
||||
|
||||
데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요.
|
||||
|
||||
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217) 또는 [TestFlight 참여](https://testflight.apple.com/join/YjeGMQBA)
|
||||
- **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217)
|
||||
- **Android:** [APK 0.0.50 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [설치 가이드](https://www.onorca.dev/docs/android-apk)
|
||||
|
||||
---
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
Monitore e conduza seus agentes pelo celular — receba uma notificação quando um agente terminar e envie instruções de acompanhamento de qualquer lugar.
|
||||
|
||||
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK Android 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile)
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
@@ -179,6 +179,7 @@ Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca.
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Logotipo do Cursor" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="Logotipo do GitHub Copilot" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Logotipo do Muse" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="Logotipo do ZCode" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="Logotipo do OpenCode" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://mimo.xiaomi.com/coder"><kbd><img src="https://www.google.com/s2/favicons?domain=mimo.xiaomi.com&sz=64" alt="Logotipo do MiMo Code" width="16" valign="middle" /> MiMo Code</kbd></a>
|
||||
@@ -195,6 +196,7 @@ Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca.
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Logotipo do Charm" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Logotipo do Cline" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Logotipo do Codebuff" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Logotipo do Freebuff" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Logotipo do Command Code" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Logotipo do Continue" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Logotipo do Droid" width="16" valign="middle" /> Droid</kbd></a>
|
||||
@@ -231,7 +233,7 @@ yay -S stably-orca-bin
|
||||
|
||||
Conecte ao app desktop para monitorar e conduzir seus agentes pelo celular.
|
||||
|
||||
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [entrar no TestFlight](https://testflight.apple.com/join/YjeGMQBA)
|
||||
- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217)
|
||||
- **Android:** [Baixar APK 0.0.50](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.50/app-release.apk)
|
||||
|
||||
---
|
||||
|
||||
@@ -179,6 +179,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智
|
||||
<a href="https://cursor.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=cursor.com&sz=64" alt="Cursor logo" width="16" valign="middle" /> Cursor</kbd></a>
|
||||
<a href="https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"><kbd><img src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="GitHub Copilot logo" width="16" valign="middle" /> GitHub Copilot</kbd></a>
|
||||
<a href="https://dev.meta.ai/docs/muse-code"><kbd><img src="../../src/shared/agent-icons/muse.png" alt="Muse logo" width="16" valign="middle" /> Muse</kbd></a>
|
||||
<a href="https://deepseek-harness.github.io/deepseek-harness/"><kbd><img src="../../src/shared/agent-icons/dsh.png" alt="DeepSeek Harness logo" width="16" valign="middle" /> DeepSeek Harness</kbd></a>
|
||||
<a href="https://zcode.z.ai/en/docs"><kbd><img src="../../src/shared/agent-icons/zcode.png" alt="ZCode logo" width="16" valign="middle" /> ZCode</kbd></a>
|
||||
<a href="https://opencode.ai/docs/cli/"><kbd><img src="https://www.google.com/s2/favicons?domain=opencode.ai&sz=64" alt="OpenCode logo" width="16" valign="middle" /> OpenCode</kbd></a>
|
||||
<a href="https://ampcode.com/manual#install"><kbd><img src="https://www.google.com/s2/favicons?domain=ampcode.com&sz=64" alt="Amp logo" width="16" valign="middle" /> Amp</kbd></a>
|
||||
@@ -193,6 +194,7 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智
|
||||
<a href="https://github.com/charmbracelet/crush"><kbd><img src="https://www.google.com/s2/favicons?domain=charm.sh&sz=64" alt="Charm logo" width="16" valign="middle" /> Charm</kbd></a>
|
||||
<a href="https://docs.cline.bot/cline-cli/overview"><kbd><img src="https://www.google.com/s2/favicons?domain=cline.bot&sz=64" alt="Cline logo" width="16" valign="middle" /> Cline</kbd></a>
|
||||
<a href="https://www.codebuff.com/docs/help/quick-start"><kbd><img src="https://www.google.com/s2/favicons?domain=codebuff.com&sz=64" alt="Codebuff logo" width="16" valign="middle" /> Codebuff</kbd></a>
|
||||
<a href="https://freebuff.com/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=freebuff.com&sz=64" alt="Freebuff logo" width="16" valign="middle" /> Freebuff</kbd></a>
|
||||
<a href="https://commandcode.ai/docs/quickstart"><kbd><img src="https://www.google.com/s2/favicons?domain=commandcode.ai&sz=64" alt="Command Code logo" width="16" valign="middle" /> Command Code</kbd></a>
|
||||
<a href="https://docs.continue.dev/guides/cli"><kbd><img src="https://www.google.com/s2/favicons?domain=continue.dev&sz=64" alt="Continue logo" width="16" valign="middle" /> Continue</kbd></a>
|
||||
<a href="https://docs.factory.ai/cli/getting-started/quickstart"><kbd><img src="../assets/droid-logo.svg" alt="Droid logo" width="16" valign="middle" /> Droid</kbd></a>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# CI demand rollout
|
||||
|
||||
This implements the September 28 runner-demand analysis. The baseline inventory
|
||||
covered September 27 04:00–September 28 04:00 UTC: 4,028 workflow runs, with
|
||||
463 stratified job samples. Estimated occupancy was 1,081 runner-hours, dominated
|
||||
by PR unit shards (414 hours) and Bun qualification (262 hours). These are
|
||||
sampled sums of job durations across different runner pools, not billing totals
|
||||
or a guaranteed forecast of savings.
|
||||
|
||||
## What runs now
|
||||
|
||||
| Work | Ordinary draft update | Ready PR / final checks | Main reference |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------- | --------------------------------------- |
|
||||
| Static analysis and types | Immediately | Immediately | Existing workflows |
|
||||
| Unit suite | Full, with shadow selection evidence | Full | Existing daily Node 24/26 x86 suite |
|
||||
| Packages | After successful static analysis and types | Same | Existing release workflows |
|
||||
| Bun persistence | Linux x64 for ordinary runtime changes; all six platforms for sensitive inputs | All six platforms when Bun inputs changed | Full nightly qualification at 11:30 UTC |
|
||||
| Bun glibc/musl qualification | Sensitive inputs only, after persistence succeeds | Both architectures after persistence succeeds | Both architectures |
|
||||
| E2E | Existing targeted routing | Existing targeted routing | One complete run at 17:00 UTC |
|
||||
|
||||
Bun-sensitive inputs include root/toolchain files, configuration, native code,
|
||||
resources, platform-specific paths, persistence, SQLite, orcad, providers,
|
||||
daemon, SSH, relay, and child-process code. Dependency discovery failure, a
|
||||
missing event or an incomplete diff retains full qualification. An unrelated
|
||||
change still skips Bun through the existing dependency classifier. No native
|
||||
artifact is shared across platforms or ABIs. The routine draft reduction is an
|
||||
explicit coverage-placement change; it does not assert identical per-update
|
||||
coverage. Every non-draft synchronize event and the ready-for-review event
|
||||
restores full qualification.
|
||||
|
||||
Expensive PR jobs wait for static/type success. This reduces fan-out for failed
|
||||
or rapidly superseded commits without sleeping on a runner. Successful isolated
|
||||
PRs pay the extra stage latency. Existing per-PR cancellation remains in place.
|
||||
Package assertions, native boundaries, SSH/folder coverage, cache warming and
|
||||
slow-test assertions are retained.
|
||||
|
||||
## Unit selection rollout
|
||||
|
||||
`ci-unit-plan.mjs` discovers the same include/exclude set as Vitest and follows
|
||||
static imports, re-exports, literal dynamic imports, CommonJS requires and the
|
||||
renderer aliases. Consumers of indirect filesystem/process inputs remain in the
|
||||
candidate set, as do script/tool tests. Global configuration changes, deletions,
|
||||
renames involving removed paths, unknown inputs and graph failures run the full
|
||||
suite. A shard verifies the plan's source SHA and complete discovery list before
|
||||
using it. Missing/stale artifacts fall back to full coverage, even if that means
|
||||
running the suite on fewer shards.
|
||||
|
||||
The initial policy is **shadow**, with all eight shards retained. The first
|
||||
local inventory matched Vitest exactly (9,950 files at validation); representative
|
||||
source changes retained roughly 88% of files because of indirect input readers.
|
||||
That is evidence for conservative coverage, not evidence of the analysis's
|
||||
hypothetical 50% unit-work reduction. Improvements to indirect dependency
|
||||
modeling should be demonstrated against full results before expanding selection.
|
||||
|
||||
Every shard uploads `unit-selection.json`, `unit-timings.json` (including module
|
||||
outcomes), and its assignment. The evidence job combines these into
|
||||
`unit-selection-review-attempt-N/selection-review.json`, reporting:
|
||||
|
||||
- Whether every discovered file appeared once across a complete reference run.
|
||||
- Failures outside the candidate set, including failures in otherwise red runs.
|
||||
- Measured worker time that selection would omit; worker times overlap and are
|
||||
not runner occupancy or a prediction of wall-clock savings.
|
||||
|
||||
Missing/duplicate shards, stale plans, interrupted runs and unhandled errors do
|
||||
not count as complete references. Diagnostic upload/report failures do not make
|
||||
tests pass and do not independently fail successful tests.
|
||||
|
||||
After representative complete shadow runs show no missed failures, set repository
|
||||
variable `ORCA_UNIT_SELECTION_MODE=selected` to enable selection **only for draft
|
||||
PRs**. Keep full ready-PR checks and the daily compatibility suite. Inspect at
|
||||
least a week's evidence across renderer, main, shared, SSH and fixture changes
|
||||
before promotion, including red runs rather than only successful examples.
|
||||
Unknown variable values retain shadow mode. Unset the variable or set it to
|
||||
`shadow` to roll back immediately. Selected runs use one to eight timing-balanced
|
||||
shards based on retained work.
|
||||
|
||||
Ready-for-review result reuse includes `unit full` in its source/workflow
|
||||
identity. A green selected draft cannot satisfy the final full check, even if
|
||||
the repository variable changes between runs. An already successful _full_
|
||||
identical-source check can still be reused.
|
||||
|
||||
To inspect downloaded shard artifacts locally:
|
||||
|
||||
```sh
|
||||
node config/scripts/ci-unit-selection-review.mjs ARTIFACT_DIRECTORY
|
||||
```
|
||||
|
||||
## Review automation
|
||||
|
||||
Pullfrog recognizes the existing `Review #N [id]` and
|
||||
`Review new commits on #N [id]` dispatch names. Explicit dispatchers may provide
|
||||
`pull_request_number` and `head_sha`. Explicit PR identities share concurrency at the workflow boundary. Legacy review
|
||||
names use a bounded lookup of the latest 100 dispatches and cancel only lower
|
||||
run IDs for the same PR; a delayed older scope cannot cancel a newer review.
|
||||
Unrecognized tasks are never grouped. The scope job alone has Actions write
|
||||
permission for ordered cancellation. Closed PRs and explicitly stale heads
|
||||
are skipped. A second head check prevents starting an agent after its queued
|
||||
head has changed. Unrecognized agent tasks remain independent; lookup failures
|
||||
also retain an independent task rather than cancelling unrelated work.
|
||||
|
||||
This does not introduce a fixed debounce interval or remove final reviews.
|
||||
Dispatchers should supply `head_sha` for reliable stale-at-dispatch detection;
|
||||
legacy names identify a PR but do not prove which head the prompt describes.
|
||||
|
||||
## E2E signal
|
||||
|
||||
The daily reference still executes all shards and keeps original verdicts. Each
|
||||
shard uploads Playwright JSON and publishes expected, skipped, unexpected, flaky
|
||||
and startup-error counts with the failing test names/messages. Targeted PR and
|
||||
manual coverage remain available. This change does not fix the historically
|
||||
red tests or pretend they pass.
|
||||
|
||||
`config/e2e-failure-tracking.json` can separate an evidenced repeated failure
|
||||
from new failures in the summary. Each entry must have exact `file`, full
|
||||
`title`, `project`, a nonempty stable `message` substring, an `@owner`, a linked
|
||||
repository `issue`, and an ISO `expires` review date. Expired/malformed entries
|
||||
are ignored and reported; changed error signatures appear as untracked. Entries
|
||||
never skip a test or change its exit status. The initial list is empty because
|
||||
the analysis established red workflows but did not establish owners and
|
||||
reproductions for individual failures. Do not blanket-baseline an entire red run.
|
||||
|
||||
## Capacity measurements and acceptance
|
||||
|
||||
`CI runner demand` runs daily at 04:23 UTC and can be dispatched manually. It
|
||||
reads the previous 24 complete hours in hourly pages, samples up to six runs per
|
||||
workflow/outcome stratum, and fetches job pages with bounded concurrency. An
|
||||
hour exceeding the API's 1,000-result search cap fails visibly. The report and
|
||||
raw evidence are retained for 30 days. No extra runner pool is provisioned.
|
||||
|
||||
The report measures the full job durations of runs **created** in the window,
|
||||
not occupancy clipped to the window: earlier runs that overlap it are excluded,
|
||||
and completed sampled jobs may finish after it. This matches the baseline
|
||||
cohort method. Workflow IDs keep ref-qualified paths in one sampling stratum.
|
||||
|
||||
The report shows weighted runner-hours and cancelled-run hours per workflow,
|
||||
runner-minutes per completed PR _run_, and weighted queue/provisioning p95 per
|
||||
runner label. It counts latest attempts only, excludes incomplete jobs, and
|
||||
retains zero-job observations. It does not measure other repositories competing
|
||||
for organization capacity. Compare equivalent traffic windows, not raw totals
|
||||
alone. The collector needs only `contents: read` and `actions: read`.
|
||||
|
||||
After a week, compare runner-minutes per PR run, cancellation occupancy and
|
||||
queue p95 in each affected pool. Count newly added planning/reference overhead.
|
||||
A 25–35% overall reduction remains an experiment target, not an achieved result;
|
||||
selection, coalescing and matrix reductions overlap and cannot simply be added.
|
||||
@@ -1,5 +1,8 @@
|
||||
# CI efficiency and runner capacity
|
||||
|
||||
The [September 28 demand rollout](ci-demand-rollout.md) documents staged checks,
|
||||
unit-selection evidence, Bun qualification, review cancellation and daily occupancy reports.
|
||||
|
||||
## September 27 follow-up
|
||||
|
||||
### Shared E2E CLI output
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Qoder CLI integration
|
||||
|
||||
Orca registers `qodercli` as `qoder`: detection, picker/settings, desktop and mobile
|
||||
identity, prompt launch, permission flags, managed hooks, workspace trust, and
|
||||
session resume use the existing TUI-agent and hook-status paths.
|
||||
|
||||
## Verified contract
|
||||
|
||||
Verified on macOS with Qoder CLI 1.1.64, including its versioned executable
|
||||
`qodercli-1.1.64`:
|
||||
|
||||
- `--prompt-interactive` starts an interactive prompt; `--resume <id>` resumes a
|
||||
session; `--dangerously-skip-permissions` is the permission bypass flag.
|
||||
- Hooks use the Claude-shaped nested configuration in `~/.qoder/settings.json`.
|
||||
Orca registers its own `/hook/qoder` source and preserves user hooks.
|
||||
- Real SessionStart, UserPromptSubmit, Notification and SessionEnd events are
|
||||
captured in `src/shared/__fixtures__/qoder-no-account-hooks.jsonl`, including
|
||||
`source: resume` with the original session ID.
|
||||
- Trust is `permissions.trustDirectories` in that settings file, using the
|
||||
canonical workspace path. A workspace `.trusted` marker did not bypass the
|
||||
trust dialog in this version. Remote trust uses the execution host filesystem.
|
||||
- Qoder emits its `◇ … | Ready` title while the trust menu owns input. Readiness
|
||||
therefore requires the live composer text, not merely this title or silence.
|
||||
Raw PTY fixtures under `src/main/runtime/__fixtures__/qoder-*` cover trust,
|
||||
unauthenticated prompt handling and the ready composer.
|
||||
- A hidden Orca dev instance launched Qoder from New Tab in a folder workspace.
|
||||
Its icon, label, terminal rendering and failed-turn indicator were inspected.
|
||||
A harmless prompt reached Qoder, which reported its credit usage limit; the
|
||||
canonical hook store recorded Qoder identity, session metadata and failure.
|
||||
|
||||
## Compatibility and limits
|
||||
|
||||
Windows hooks explicitly select Qoder's documented PowerShell shell, avoiding
|
||||
an assumption that Qoder uses Git Bash just because it is installed. They reuse
|
||||
the existing managed `.cmd` payload without adding an encoded PowerShell hop.
|
||||
The configuration shape and preservation are tested; Windows, Linux and SSH
|
||||
execution have not been exercised live here.
|
||||
|
||||
Resume requests to remote hosts require `agent-session.qoder-resume.v1`, so an
|
||||
older host is not sent an agent enum it cannot accept. Remote hook installation
|
||||
and trust preservation have automated coverage.
|
||||
|
||||
Successful model output, tool execution and live permission dialogs remain
|
||||
unverified because the available account has exhausted its credits. Hook event
|
||||
mapping for these paths follows the official documentation. The China executable
|
||||
`qoderclicn` is not registered; it was not available for verification.
|
||||
|
||||
New Tab follows the existing manual-launch trust behavior: an untrusted folder
|
||||
can show Qoder's trust dialog. Automated workspace/draft launch paths run the
|
||||
trust preflight before prompt delivery.
|
||||
|
||||
## Sources
|
||||
|
||||
- [Official hooks documentation](https://docs.qoder.com/cli/hooks)
|
||||
- [#15291](https://github.com/stablyai/orca/pull/15291): registration and hook proposal
|
||||
- [#13311](https://github.com/stablyai/orca/pull/13311): icon asset (commit
|
||||
`b56197025530adb1b82d97a364a8c3d4a02c0d42`) and agent integration
|
||||
- [#8611](https://github.com/stablyai/orca/pull/8611) and
|
||||
[#12910](https://github.com/stablyai/orca/pull/12910): contributor implementations
|
||||
- [#16540](https://github.com/stablyai/orca/issues/16540): Qoder misidentified as Gemini
|
||||
|
||||
Contributor patches informed the integration; they were not applied wholesale.
|
||||
In particular, the trust marker proposal was replaced using the installed CLI's
|
||||
observed settings write, and no renderer workaround was added because the live
|
||||
WebGL terminal rendered correctly.
|
||||
|
||||
## Cross-review of open proposals
|
||||
|
||||
Reviewed the current patches for all six open Qoder PRs on 2026-09-28:
|
||||
|
||||
| PR | Incorporated or confirmed | Deliberate differences |
|
||||
| --- | --- | --- |
|
||||
| #7502 (vincent-lxc) | Agent registration, mobile parity, identity before Gemini | Use structural titles and the current public permission flag. |
|
||||
| #8611 (Eridanus117, building on #7502) | Qoder hook source, notification/permission mapping, session resume | 1.1.64 supports `--prompt-interactive`; preserve startup session identity and ignore compact restarts. |
|
||||
| #9655 (xingqingzzp-gif) | Cross-checked minimum registration coverage | Placeholder icon and detection-only scope are superseded by the fuller integration; no code copied. |
|
||||
| #12910 (jyang2004) | Reuse the Claude-compatible installer with an event subset, plus remote installation | Use observed `.qoder` path and `/hook/qoder`; Qwen and the China build remain separate work. |
|
||||
| #13311 (sorrycc) | Icon, structural title disambiguation, headless detection, display glyph cleanup | No DOM renderer override: 1.1.64 renders correctly in the live WebGL terminal. No unverified executable alias. |
|
||||
| #15291 (adlternative) | Versioned binary recognition, trust preflight wiring, skills mapping and resume | Trust comes from settings, not `.trusted`; include notifications, permission and failure events with Qoder-specific normalization. |
|
||||
|
||||
The rendered left sidebar was checked in the hidden Electron app using a temporary
|
||||
folder workspace and real Qoder hooks. The workspace fixture needed a parent path
|
||||
on its project group to appear in the sidebar. No agent status was injected into
|
||||
the renderer. A real prompt updated the row text and returned a credit-limit error;
|
||||
the row and workspace card showed failure with the Qoder icon. A DOM observer
|
||||
recorded the prompt-row transition before failure. Permission/waiting and successful
|
||||
completion remain automated event-mapping tests, not live model/tool verification.
|
||||
|
||||
Commit co-authors credit all six proposal authors for the implementation and
|
||||
registration groundwork, including xingqingzzp-gif’s minimal registration proposal.
|
||||
@@ -46,8 +46,10 @@ To restore prompts for one agent only, edit that agent's default arguments or en
|
||||
| Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) |
|
||||
| Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) |
|
||||
| Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) |
|
||||
| Freebuff | Auto-setup | [Freebuff](https://freebuff.com/cli) |
|
||||
| Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) |
|
||||
| Muse | macOS/Linux; trusts the workspace at launch | [Meta](https://dev.meta.ai/docs/muse-code) |
|
||||
| DeepSeek Harness | Launched through its `dsh-tui` profile; hooks, status, questions, resume | [DeepSeek](https://deepseek-harness.github.io/deepseek-harness/) |
|
||||
| ZCode | Deep integration; needs a `zcode` CLI that ships the TUI (see note below) | [Z.ai](https://zcode.z.ai/en/docs) |
|
||||
| Continue | Auto-setup | [Continue](https://docs.continue.dev/guides/cli) |
|
||||
| Cursor CLI | Deep integration | [Cursor](https://cursor.com/cli) |
|
||||
|
||||
@@ -19,6 +19,7 @@ import { displayHostEndpoint } from '../../../src/transport/host-endpoint'
|
||||
import { resolveHostEndpointEdit } from '../../../src/transport/host-endpoint-edit'
|
||||
import { usePrimeHosts, useRefreshHostClient } from '../../../src/transport/client-context'
|
||||
import type { HostProfile } from '../../../src/transport/types'
|
||||
import { hostOs } from '../../../src/platform/host-os'
|
||||
|
||||
export default function EditHostScreen() {
|
||||
const router = useRouter()
|
||||
@@ -179,7 +180,7 @@ export default function EditHostScreen() {
|
||||
) : (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
behavior={hostOs() === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.form, { paddingBottom: insets.bottom + spacing.xl }]}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import ObjectiveC
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
/// Removes WKWebView's form accessory bar (up, down, done). Native screens have none, and the
|
||||
/// keyboard event the shell publishes counts it, so the page lifted by the bar as well. WebKit
|
||||
/// exposes no switch, so the content view is moved onto a subclass whose `inputAccessoryView` is
|
||||
/// nil, the same technique react-native-webview's `hideKeyboardAccessoryView` uses.
|
||||
func hideKeyboardAccessoryBar(of webView: WKWebView) {
|
||||
guard
|
||||
let content = webView.scrollView.subviews.first(where: {
|
||||
String(describing: type(of: $0)).hasPrefix("WKContent")
|
||||
})
|
||||
else {
|
||||
return
|
||||
}
|
||||
let base: AnyClass = type(of: content)
|
||||
let name = "\(NSStringFromClass(base))_OrcaNoAccessory"
|
||||
let subclass: AnyClass
|
||||
if let existing = NSClassFromString(name) {
|
||||
subclass = existing
|
||||
} else {
|
||||
guard let created = objc_allocateClassPair(base, name, 0) else {
|
||||
return
|
||||
}
|
||||
let noAccessory: @convention(block) (AnyObject) -> UIView? = { _ in nil }
|
||||
let selector = #selector(getter: UIResponder.inputAccessoryView)
|
||||
let typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIResponder.self, selector)!)
|
||||
class_addMethod(created, selector, imp_implementationWithBlock(noAccessory), typeEncoding)
|
||||
objc_registerClassPair(created)
|
||||
subclass = created
|
||||
}
|
||||
object_setClass(content, subclass)
|
||||
}
|
||||
|
||||
/// Stops WKWebView reacting to the keyboard itself. It listens for the keyboard's frame and scrolls
|
||||
/// the document to reveal the focused field, on top of the lift the page already applies from the
|
||||
/// height the shell sends, so the page moved twice. The shell's own listeners are unaffected.
|
||||
func ignoreKeyboardNotifications(in webView: WKWebView) {
|
||||
for name in [
|
||||
UIResponder.keyboardWillShowNotification,
|
||||
UIResponder.keyboardDidShowNotification,
|
||||
UIResponder.keyboardWillHideNotification,
|
||||
UIResponder.keyboardDidHideNotification,
|
||||
UIResponder.keyboardWillChangeFrameNotification,
|
||||
UIResponder.keyboardDidChangeFrameNotification
|
||||
] {
|
||||
NotificationCenter.default.removeObserver(webView, name: name, object: nil)
|
||||
}
|
||||
}
|
||||
@@ -221,6 +221,8 @@ final class OrcaMobileWebShellView: ExpoView, WKNavigationDelegate, WKUIDelegate
|
||||
webView.backgroundColor = .clear
|
||||
webView.scrollView.backgroundColor = .clear
|
||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
hideKeyboardAccessoryBar(of: webView)
|
||||
ignoreKeyboardNotifications(in: webView)
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(webView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.history",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.history",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.history",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "ec16931c5431d3dbb05729d4670c381a434bff71648d334b7b5224db188fdccb",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.history-screen",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.history-screen",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "c78ab47eea594b7e1988403321ff9ba135ca60c513bb9d5848bdde26c0ffe3c3",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.resume-launch",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.resume-launch",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"family": "aiVault.resume-launch",
|
||||
"namedDeltas": [],
|
||||
"runnerVersion": 1,
|
||||
"baseline": "44b829c8178af0e08ba953aa45e5c4c907164bf5",
|
||||
"baseline": "2077956254c707d904c066ca890dc25f3671fb70",
|
||||
"lockfileSha256": "9317f3a98ab047f9f96fe26fd8531b8a632ac5954064b322f41a904148cbf6bb",
|
||||
"recorderSha256": "0317fe2aebe4743ce5e7ae194531aa91b4fe0a84df457640851fe6dfdb9362af",
|
||||
"adapterSha256": "2f43211e4084c0493bd02ec98868acf53a4c748f89a70cd9657c9c3ee87b12fb",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user