@omega.js/desktop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +166 -0
- package/bin/omega +2 -0
- package/bin/omega-desktop +2 -0
- package/bin/omg +2 -0
- package/dist/assets/css/core/_fontawesome.scss +146 -0
- package/dist/assets/css/core/_initialize.scss +38 -0
- package/dist/assets/css/omega-desktop.scss +39 -0
- package/dist/assets/css/shell/_index.scss +207 -0
- package/dist/assets/css/tokens/_index.scss +166 -0
- package/dist/assets/js/bootstrap.bundle.js +1 -0
- package/dist/assets/js/core/app-shell.js +136 -0
- package/dist/assets/themes/_template/README.md +115 -0
- package/dist/assets/themes/_template/_config.scss +60 -0
- package/dist/assets/themes/_template/_theme.js +14 -0
- package/dist/assets/themes/_template/_theme.scss +28 -0
- package/dist/assets/themes/_template/css/base/_root.scss +19 -0
- package/dist/assets/themes/_template/css/components/_components.scss +23 -0
- package/dist/assets/themes/base/README.md +5 -0
- package/dist/assets/themes/base/_components/data/org-chart/component.html +33 -0
- package/dist/assets/themes/base/_components/data/org-chart/component.json5 +33 -0
- package/dist/assets/themes/base/_components/data/org-chart/component.scss +183 -0
- package/dist/assets/themes/base/_components/heading/masthead/component.html +22 -0
- package/dist/assets/themes/base/_components/heading/masthead/component.json5 +30 -0
- package/dist/assets/themes/base/_components/heading/section-head/component.html +18 -0
- package/dist/assets/themes/base/_components/heading/section-head/component.json5 +27 -0
- package/dist/assets/themes/base/_components/pricing/features/component.html +29 -0
- package/dist/assets/themes/base/_components/pricing/features/component.json5 +30 -0
- package/dist/assets/themes/base/_hero/orbit/index.html +30 -0
- package/dist/assets/themes/base/_hero/orbit/script.js +33 -0
- package/dist/assets/themes/base/_hero/orbit/style.scss +133 -0
- package/dist/assets/themes/base/_includes/admin/sections/sidebar.html +3 -0
- package/dist/assets/themes/base/_includes/admin/sections/topbar.html +3 -0
- package/dist/assets/themes/base/_includes/backend/sections/sidebar.html +3 -0
- package/dist/assets/themes/base/_includes/backend/sections/topbar.html +3 -0
- package/dist/assets/themes/base/_includes/frontend/components/connection-card.html +49 -0
- package/dist/assets/themes/base/_includes/frontend/components/post-card.html +54 -0
- package/dist/assets/themes/base/_includes/frontend/sections/account-connections.html +42 -0
- package/dist/assets/themes/base/_includes/frontend/sections/account-section-header.html +11 -0
- package/dist/assets/themes/base/_includes/frontend/sections/footer.html +154 -0
- package/dist/assets/themes/base/_includes/frontend/sections/nav.html +276 -0
- package/dist/assets/themes/base/_includes/global/sections/account.html +72 -0
- package/dist/assets/themes/base/_includes/global/sections/app-sidebar.html +248 -0
- package/dist/assets/themes/base/_includes/global/sections/app-topbar.html +228 -0
- package/dist/assets/themes/base/_includes/global/sections/page-header.html +99 -0
- package/dist/assets/themes/base/_layouts/admin/core/minimal-viewport-locked.html +22 -0
- package/dist/assets/themes/base/_layouts/admin/core/minimal.html +23 -0
- package/dist/assets/themes/base/_layouts/backend/core/base.html +57 -0
- package/dist/assets/themes/base/_layouts/backend/core/minimal-viewport-locked.html +32 -0
- package/dist/assets/themes/base/_layouts/backend/core/minimal.html +32 -0
- package/dist/assets/themes/base/_layouts/backend/pages/dashboard/index.html +230 -0
- package/dist/assets/themes/base/_layouts/frontend/core/base.html +31 -0
- package/dist/assets/themes/base/_layouts/frontend/core/cover.html +40 -0
- package/dist/assets/themes/base/_layouts/frontend/core/minimal.html +47 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/404.html +54 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/about.html +144 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/account/index.html +2104 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/alternative.html +319 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/alternatives/index.html +186 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/app.html +142 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/reset.html +53 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signin.html +90 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/signup.html +141 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/auth/token.html +44 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/categories/category.html +88 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/categories/index.html +88 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/index.html +255 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/post.html +261 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/index.html +84 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/blog/tags/tag.html +88 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/collection/category.html +72 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/collection/document.html +54 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/collection/index.html +84 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/connections/callback.html +55 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/contact.html +214 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/download.html +793 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/extension/index.html +253 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/extension/installed.html +198 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/feedback.html +171 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/index.html +66 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/legal/document.html +78 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/payment/checkout.html +459 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/payment/confirmation.html +131 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/portal/email-preferences.html +104 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/pricing.html +596 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/status.html +361 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/team/index.html +210 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/team/member.html +125 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/updates/index.html +119 -0
- package/dist/assets/themes/base/_layouts/frontend/pages/updates/update.html +118 -0
- package/dist/assets/themes/base/_sections/about/hero/section.html +68 -0
- package/dist/assets/themes/base/_sections/about/hero/section.json5 +58 -0
- package/dist/assets/themes/base/_sections/about/letter/section.html +60 -0
- package/dist/assets/themes/base/_sections/about/letter/section.json5 +56 -0
- package/dist/assets/themes/base/_sections/about/photo-band/section.html +24 -0
- package/dist/assets/themes/base/_sections/about/photo-band/section.json5 +23 -0
- package/dist/assets/themes/base/_sections/about/principles/section.html +17 -0
- package/dist/assets/themes/base/_sections/about/principles/section.json5 +34 -0
- package/dist/assets/themes/base/_sections/about/timeline/section.html +20 -0
- package/dist/assets/themes/base/_sections/about/timeline/section.json5 +34 -0
- package/dist/assets/themes/base/_sections/marketing/bento/section.html +91 -0
- package/dist/assets/themes/base/_sections/marketing/bento/section.json5 +152 -0
- package/dist/assets/themes/base/_sections/marketing/cta/section.html +44 -0
- package/dist/assets/themes/base/_sections/marketing/cta/section.json5 +47 -0
- package/dist/assets/themes/base/_sections/marketing/faq/section.html +52 -0
- package/dist/assets/themes/base/_sections/marketing/faq/section.json5 +68 -0
- package/dist/assets/themes/base/_sections/marketing/hero/section.html +349 -0
- package/dist/assets/themes/base/_sections/marketing/hero/section.json5 +335 -0
- package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.html +38 -0
- package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.js +86 -0
- package/dist/assets/themes/base/_sections/marketing/newsletter-cta/section.json5 +29 -0
- package/dist/assets/themes/base/_sections/marketing/pricing-cards/section.html +115 -0
- package/dist/assets/themes/base/_sections/marketing/pricing-cards/section.json5 +72 -0
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.html +62 -0
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.js +44 -0
- package/dist/assets/themes/base/_sections/marketing/product-demo/section.json5 +52 -0
- package/dist/assets/themes/base/_sections/marketing/prose/section.html +22 -0
- package/dist/assets/themes/base/_sections/marketing/prose/section.json5 +41 -0
- package/dist/assets/themes/base/_sections/marketing/showcase/section.html +35 -0
- package/dist/assets/themes/base/_sections/marketing/showcase/section.json5 +52 -0
- package/dist/assets/themes/base/_sections/marketing/stats/section.html +32 -0
- package/dist/assets/themes/base/_sections/marketing/stats/section.json5 +44 -0
- package/dist/assets/themes/base/_sections/marketing/testimonials/section.html +54 -0
- package/dist/assets/themes/base/_sections/marketing/testimonials/section.json5 +70 -0
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.html +30 -0
- package/dist/assets/themes/base/_sections/marketing/trusted-by/section.json5 +38 -0
- package/dist/assets/themes/base/_sections/verts/unit/section.html +7 -0
- package/dist/assets/themes/base/_sections/verts/unit/section.js +22 -0
- package/dist/assets/themes/base/_sections/verts/unit/section.json5 +24 -0
- package/dist/assets/themes/base/_sections/verts/unit/section.scss +24 -0
- package/dist/assets/themes/base/_theme.js +13 -0
- package/dist/assets/themes/base/_theme.scss +4 -0
- package/dist/assets/themes/bootstrap/_theme.js +16 -0
- package/dist/assets/themes/bootstrap/_theme.scss +11 -0
- package/dist/assets/themes/bootstrap/js/index.esm.js +19 -0
- package/dist/assets/themes/bootstrap/js/index.umd.js +34 -0
- package/dist/assets/themes/bootstrap/js/src/alert.js +87 -0
- package/dist/assets/themes/bootstrap/js/src/base-component.js +86 -0
- package/dist/assets/themes/bootstrap/js/src/button.js +72 -0
- package/dist/assets/themes/bootstrap/js/src/carousel.js +474 -0
- package/dist/assets/themes/bootstrap/js/src/collapse.js +297 -0
- package/dist/assets/themes/bootstrap/js/src/dom/data.js +55 -0
- package/dist/assets/themes/bootstrap/js/src/dom/event-handler.js +317 -0
- package/dist/assets/themes/bootstrap/js/src/dom/manipulator.js +71 -0
- package/dist/assets/themes/bootstrap/js/src/dom/selector-engine.js +126 -0
- package/dist/assets/themes/bootstrap/js/src/dropdown.js +458 -0
- package/dist/assets/themes/bootstrap/js/src/modal.js +378 -0
- package/dist/assets/themes/bootstrap/js/src/offcanvas.js +282 -0
- package/dist/assets/themes/bootstrap/js/src/popover.js +97 -0
- package/dist/assets/themes/bootstrap/js/src/scrollspy.js +296 -0
- package/dist/assets/themes/bootstrap/js/src/tab.js +315 -0
- package/dist/assets/themes/bootstrap/js/src/toast.js +224 -0
- package/dist/assets/themes/bootstrap/js/src/tooltip.js +632 -0
- package/dist/assets/themes/bootstrap/js/src/util/backdrop.js +151 -0
- package/dist/assets/themes/bootstrap/js/src/util/component-functions.js +35 -0
- package/dist/assets/themes/bootstrap/js/src/util/config.js +65 -0
- package/dist/assets/themes/bootstrap/js/src/util/focustrap.js +115 -0
- package/dist/assets/themes/bootstrap/js/src/util/index.js +306 -0
- package/dist/assets/themes/bootstrap/js/src/util/sanitizer.js +117 -0
- package/dist/assets/themes/bootstrap/js/src/util/scrollbar.js +114 -0
- package/dist/assets/themes/bootstrap/js/src/util/swipe.js +146 -0
- package/dist/assets/themes/bootstrap/js/src/util/template-factory.js +160 -0
- package/dist/assets/themes/bootstrap/overrides/_accordion.scss +21 -0
- package/dist/assets/themes/bootstrap/overrides/_avatars.scss +51 -0
- package/dist/assets/themes/bootstrap/overrides/_blog-images.scss +45 -0
- package/dist/assets/themes/bootstrap/overrides/_buttons-adaptive.scss +141 -0
- package/dist/assets/themes/bootstrap/overrides/_color-shades.scss +25 -0
- package/dist/assets/themes/bootstrap/overrides/_cursor-utilities.scss +13 -0
- package/dist/assets/themes/bootstrap/overrides/_index.scss +30 -0
- package/dist/assets/themes/bootstrap/overrides/_links.scss +26 -0
- package/dist/assets/themes/bootstrap/overrides/_soft-colors.scss +99 -0
- package/dist/assets/themes/bootstrap/overrides/_spacing-extended.scss +74 -0
- package/dist/assets/themes/bootstrap/overrides/_spinners.scss +22 -0
- package/dist/assets/themes/bootstrap/overrides/_typography-utilities.scss +40 -0
- package/dist/assets/themes/bootstrap/scss/_accordion.scss +153 -0
- package/dist/assets/themes/bootstrap/scss/_alert.scss +68 -0
- package/dist/assets/themes/bootstrap/scss/_badge.scss +38 -0
- package/dist/assets/themes/bootstrap/scss/_breadcrumb.scss +40 -0
- package/dist/assets/themes/bootstrap/scss/_button-group.scss +147 -0
- package/dist/assets/themes/bootstrap/scss/_buttons.scss +216 -0
- package/dist/assets/themes/bootstrap/scss/_card.scss +238 -0
- package/dist/assets/themes/bootstrap/scss/_carousel.scss +226 -0
- package/dist/assets/themes/bootstrap/scss/_close.scss +66 -0
- package/dist/assets/themes/bootstrap/scss/_containers.scss +41 -0
- package/dist/assets/themes/bootstrap/scss/_dropdown.scss +250 -0
- package/dist/assets/themes/bootstrap/scss/_forms.scss +9 -0
- package/dist/assets/themes/bootstrap/scss/_functions.scss +302 -0
- package/dist/assets/themes/bootstrap/scss/_grid.scss +39 -0
- package/dist/assets/themes/bootstrap/scss/_helpers.scss +12 -0
- package/dist/assets/themes/bootstrap/scss/_images.scss +42 -0
- package/dist/assets/themes/bootstrap/scss/_list-group.scss +199 -0
- package/dist/assets/themes/bootstrap/scss/_maps.scss +174 -0
- package/dist/assets/themes/bootstrap/scss/_mixins.scss +42 -0
- package/dist/assets/themes/bootstrap/scss/_modal.scss +240 -0
- package/dist/assets/themes/bootstrap/scss/_nav.scss +197 -0
- package/dist/assets/themes/bootstrap/scss/_navbar.scss +289 -0
- package/dist/assets/themes/bootstrap/scss/_offcanvas.scss +147 -0
- package/dist/assets/themes/bootstrap/scss/_pagination.scss +109 -0
- package/dist/assets/themes/bootstrap/scss/_placeholders.scss +51 -0
- package/dist/assets/themes/bootstrap/scss/_popover.scss +196 -0
- package/dist/assets/themes/bootstrap/scss/_progress.scss +68 -0
- package/dist/assets/themes/bootstrap/scss/_reboot.scss +611 -0
- package/dist/assets/themes/bootstrap/scss/_root.scss +187 -0
- package/dist/assets/themes/bootstrap/scss/_spinners.scss +85 -0
- package/dist/assets/themes/bootstrap/scss/_tables.scss +171 -0
- package/dist/assets/themes/bootstrap/scss/_toasts.scss +73 -0
- package/dist/assets/themes/bootstrap/scss/_tooltip.scss +119 -0
- package/dist/assets/themes/bootstrap/scss/_transitions.scss +27 -0
- package/dist/assets/themes/bootstrap/scss/_type.scss +106 -0
- package/dist/assets/themes/bootstrap/scss/_utilities.scss +806 -0
- package/dist/assets/themes/bootstrap/scss/_variables-dark.scss +102 -0
- package/dist/assets/themes/bootstrap/scss/_variables.scss +1753 -0
- package/dist/assets/themes/bootstrap/scss/bootstrap-grid.scss +62 -0
- package/dist/assets/themes/bootstrap/scss/bootstrap-reboot.scss +10 -0
- package/dist/assets/themes/bootstrap/scss/bootstrap-utilities.scss +19 -0
- package/dist/assets/themes/bootstrap/scss/bootstrap.scss +52 -0
- package/dist/assets/themes/bootstrap/scss/forms/_floating-labels.scss +97 -0
- package/dist/assets/themes/bootstrap/scss/forms/_form-check.scss +189 -0
- package/dist/assets/themes/bootstrap/scss/forms/_form-control.scss +214 -0
- package/dist/assets/themes/bootstrap/scss/forms/_form-range.scss +91 -0
- package/dist/assets/themes/bootstrap/scss/forms/_form-select.scss +80 -0
- package/dist/assets/themes/bootstrap/scss/forms/_form-text.scss +11 -0
- package/dist/assets/themes/bootstrap/scss/forms/_input-group.scss +132 -0
- package/dist/assets/themes/bootstrap/scss/forms/_labels.scss +36 -0
- package/dist/assets/themes/bootstrap/scss/forms/_validation.scss +12 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_clearfix.scss +3 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_color-bg.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_colored-links.scss +30 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_focus-ring.scss +5 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_icon-link.scss +25 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_position.scss +36 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_ratio.scss +26 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_stacks.scss +15 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_stretched-link.scss +15 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_text-truncation.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
- package/dist/assets/themes/bootstrap/scss/helpers/_vr.scss +8 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_alert.scss +18 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_backdrop.scss +14 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_banner.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_border-radius.scss +78 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_box-shadow.scss +18 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_breakpoints.scss +127 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_buttons.scss +70 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_caret.scss +69 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_clearfix.scss +9 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_color-mode.scss +21 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_color-scheme.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_container.scss +11 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_deprecate.scss +10 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_forms.scss +163 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_gradients.scss +47 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_grid.scss +151 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_image.scss +16 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_list-group.scss +26 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_lists.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_pagination.scss +10 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_reset-text.scss +17 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_resize.scss +6 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_table-variants.scss +24 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_text-truncate.scss +8 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_transition.scss +26 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_utilities.scss +97 -0
- package/dist/assets/themes/bootstrap/scss/mixins/_visually-hidden.scss +38 -0
- package/dist/assets/themes/bootstrap/scss/tests/jasmine.js +16 -0
- package/dist/assets/themes/bootstrap/scss/tests/mixins/_auto-import-of-variables-dark.test.scss +7 -0
- package/dist/assets/themes/bootstrap/scss/tests/mixins/_color-modes.test.scss +69 -0
- package/dist/assets/themes/bootstrap/scss/tests/mixins/_media-query-color-mode-full.test.scss +8 -0
- package/dist/assets/themes/bootstrap/scss/tests/mixins/_utilities.test.scss +393 -0
- package/dist/assets/themes/bootstrap/scss/tests/sass-true/register.js +14 -0
- package/dist/assets/themes/bootstrap/scss/tests/sass-true/runner.js +17 -0
- package/dist/assets/themes/bootstrap/scss/tests/utilities/_api.test.scss +75 -0
- package/dist/assets/themes/bootstrap/scss/utilities/_api.scss +47 -0
- package/dist/assets/themes/bootstrap/scss/vendor/_rfs.scss +348 -0
- package/dist/assets/themes/classy/README.md +89 -0
- package/dist/assets/themes/classy/_config.scss +245 -0
- package/dist/assets/themes/classy/_theme.js +31 -0
- package/dist/assets/themes/classy/_theme.scss +57 -0
- package/dist/assets/themes/classy/css/app/_panels.scss +183 -0
- package/dist/assets/themes/classy/css/base/_fonts.scss +79 -0
- package/dist/assets/themes/classy/css/base/_no-raise.scss +49 -0
- package/dist/assets/themes/classy/css/base/_root.scss +113 -0
- package/dist/assets/themes/classy/css/base/_typography.scss +154 -0
- package/dist/assets/themes/classy/css/base/_utilities.scss +284 -0
- package/dist/assets/themes/classy/css/components/_alerts.scss +51 -0
- package/dist/assets/themes/classy/css/components/_badges.scss +46 -0
- package/dist/assets/themes/classy/css/components/_buttons.scss +175 -0
- package/dist/assets/themes/classy/css/components/_cards.scss +41 -0
- package/dist/assets/themes/classy/css/components/_dropdowns.scss +48 -0
- package/dist/assets/themes/classy/css/components/_forms.scss +171 -0
- package/dist/assets/themes/classy/css/components/_receipt.scss +45 -0
- package/dist/assets/themes/classy/css/layout/_footer.scss +298 -0
- package/dist/assets/themes/classy/css/layout/_general.scss +209 -0
- package/dist/assets/themes/classy/css/layout/_nav.scss +154 -0
- package/dist/assets/themes/classy/css/layout/_shell.scss +458 -0
- package/dist/assets/themes/classy/css/marketing/_bento.scss +238 -0
- package/dist/assets/themes/classy/css/marketing/_content.scss +1328 -0
- package/dist/assets/themes/classy/css/marketing/_downloads.scss +246 -0
- package/dist/assets/themes/classy/css/marketing/_hero.scss +311 -0
- package/dist/assets/themes/classy/css/marketing/_sections.scss +649 -0
- package/dist/assets/themes/classy/css/pages/404/index.scss +67 -0
- package/dist/assets/themes/classy/css/pages/_auth.scss +235 -0
- package/dist/assets/themes/classy/css/pages/admin/firebase/index.scss +22 -0
- package/dist/assets/themes/classy/css/pages/blog/[slug].scss +56 -0
- package/dist/assets/themes/classy/css/pages/cookies.scss +2 -0
- package/dist/assets/themes/classy/css/pages/dashboard/account/index.scss +31 -0
- package/dist/assets/themes/classy/css/pages/feedback/index.scss +39 -0
- package/dist/assets/themes/classy/css/pages/legal/_document.scss +192 -0
- package/dist/assets/themes/classy/css/pages/payment/checkout/index.scss +585 -0
- package/dist/assets/themes/classy/css/pages/payment/confirmation/index.scss +9 -0
- package/dist/assets/themes/classy/css/pages/privacy.scss +2 -0
- package/dist/assets/themes/classy/css/pages/status/index.scss +164 -0
- package/dist/assets/themes/classy/css/pages/terms.scss +2 -0
- package/dist/assets/themes/classy/css/pages/updates/index.scss +128 -0
- package/dist/assets/themes/classy/fonts/inter-italic-latin-ext.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/inter-italic-latin.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/inter-normal-latin-ext.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/inter-normal-latin.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/newsreader-italic-latin-ext.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/newsreader-italic-latin.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/newsreader-normal-latin-ext.woff2 +0 -0
- package/dist/assets/themes/classy/fonts/newsreader-normal-latin.woff2 +0 -0
- package/dist/assets/themes/classy/js/hero-demo-form.js +56 -0
- package/dist/assets/themes/neobrutalism/README.md +118 -0
- package/dist/assets/themes/neobrutalism/_config.scss +139 -0
- package/dist/assets/themes/neobrutalism/_layouts/frontend/core/base.html +30 -0
- package/dist/assets/themes/neobrutalism/_layouts/frontend/pages/index.html +275 -0
- package/dist/assets/themes/neobrutalism/_theme.js +24 -0
- package/dist/assets/themes/neobrutalism/_theme.scss +61 -0
- package/dist/assets/themes/neobrutalism/css/base/_mixins.scss +46 -0
- package/dist/assets/themes/neobrutalism/css/base/_root.scss +169 -0
- package/dist/assets/themes/neobrutalism/css/base/_typography.scss +61 -0
- package/dist/assets/themes/neobrutalism/css/base/_utilities.scss +89 -0
- package/dist/assets/themes/neobrutalism/css/components/_billing-toggle.scss +74 -0
- package/dist/assets/themes/neobrutalism/css/components/_buttons.scss +148 -0
- package/dist/assets/themes/neobrutalism/css/components/_cards.scss +47 -0
- package/dist/assets/themes/neobrutalism/css/components/_forms.scss +88 -0
- package/dist/assets/themes/neobrutalism/css/components/_marketing.scss +244 -0
- package/dist/assets/themes/neobrutalism/css/layout/_general.scss +213 -0
- package/dist/assets/themes/neobrutalism/css/layout/_navigation.scss +171 -0
- package/dist/assets/themes/neobrutalism/css/pages/index.scss +203 -0
- package/dist/assets/themes/neobrutalism/css/pages/pricing/index.scss +58 -0
- package/dist/assets/themes/neobrutalism/css/pages/test/libraries/layers/index.scss +7 -0
- package/dist/assets/themes/neobrutalism/js/pages/test/libraries/layers/index.js +9 -0
- package/dist/assets/themes/newsflash/README.md +117 -0
- package/dist/assets/themes/newsflash/_components/heading/lede/component.html +14 -0
- package/dist/assets/themes/newsflash/_components/heading/lede/component.json5 +23 -0
- package/dist/assets/themes/newsflash/_components/heading/rule-head/component.html +17 -0
- package/dist/assets/themes/newsflash/_components/heading/rule-head/component.json5 +23 -0
- package/dist/assets/themes/newsflash/_components/news/byline/component.html +12 -0
- package/dist/assets/themes/newsflash/_components/news/byline/component.json5 +18 -0
- package/dist/assets/themes/newsflash/_components/news/story-card/component.html +25 -0
- package/dist/assets/themes/newsflash/_components/news/story-card/component.json5 +21 -0
- package/dist/assets/themes/newsflash/_config.scss +147 -0
- package/dist/assets/themes/newsflash/_layouts/frontend/core/base.html +51 -0
- package/dist/assets/themes/newsflash/_layouts/frontend/pages/blog/index.html +299 -0
- package/dist/assets/themes/newsflash/_layouts/frontend/pages/blog/post.html +235 -0
- package/dist/assets/themes/newsflash/_layouts/frontend/pages/index.html +328 -0
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.html +39 -0
- package/dist/assets/themes/newsflash/_sections/marketing/desks/section.json5 +49 -0
- package/dist/assets/themes/newsflash/_sections/marketing/newsletter-cta/section.html +103 -0
- package/dist/assets/themes/newsflash/_sections/marketing/newsletter-cta/section.json5 +50 -0
- package/dist/assets/themes/newsflash/_sections/marketing/rundown/section.html +28 -0
- package/dist/assets/themes/newsflash/_sections/marketing/rundown/section.json5 +40 -0
- package/dist/assets/themes/newsflash/_theme.js +24 -0
- package/dist/assets/themes/newsflash/_theme.scss +70 -0
- package/dist/assets/themes/newsflash/css/base/_fonts.scss +80 -0
- package/dist/assets/themes/newsflash/css/base/_mixins.scss +51 -0
- package/dist/assets/themes/newsflash/css/base/_root.scss +257 -0
- package/dist/assets/themes/newsflash/css/base/_typography.scss +49 -0
- package/dist/assets/themes/newsflash/css/base/_utilities.scss +122 -0
- package/dist/assets/themes/newsflash/css/components/_badges.scss +65 -0
- package/dist/assets/themes/newsflash/css/components/_billing-toggle.scss +61 -0
- package/dist/assets/themes/newsflash/css/components/_buttons.scss +139 -0
- package/dist/assets/themes/newsflash/css/components/_cards.scss +47 -0
- package/dist/assets/themes/newsflash/css/components/_editorial.scss +173 -0
- package/dist/assets/themes/newsflash/css/components/_forms.scss +75 -0
- package/dist/assets/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
- package/dist/assets/themes/newsflash/css/components/_panels.scss +156 -0
- package/dist/assets/themes/newsflash/css/components/_ticker.scss +67 -0
- package/dist/assets/themes/newsflash/css/layout/_general.scss +330 -0
- package/dist/assets/themes/newsflash/css/layout/_navigation.scss +168 -0
- package/dist/assets/themes/newsflash/css/pages/404/index.scss +79 -0
- package/dist/assets/themes/newsflash/css/pages/blog/[slug].scss +186 -0
- package/dist/assets/themes/newsflash/css/pages/blog/index.scss +17 -0
- package/dist/assets/themes/newsflash/css/pages/index.scss +157 -0
- package/dist/assets/themes/newsflash/css/pages/pricing/index.scss +55 -0
- package/dist/assets/themes/newsflash/css/pages/test/libraries/layers/index.scss +7 -0
- package/dist/assets/themes/newsflash/fonts/fraunces-italic-latin-ext.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/fraunces-italic-latin.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/fraunces-normal-latin-ext.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/fraunces-normal-latin.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-italic-latin-ext.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-italic-latin.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-normal-latin-ext.woff2 +0 -0
- package/dist/assets/themes/newsflash/fonts/schibsted-grotesk-normal-latin.woff2 +0 -0
- package/dist/assets/themes/newsflash/js/pages/blog/[slug].js +29 -0
- package/dist/assets/themes/newsflash/js/pages/test/libraries/layers/index.js +9 -0
- package/dist/build.js +249 -0
- package/dist/cli-run.js +34 -0
- package/dist/cli.js +49 -0
- package/dist/commands/build.js +32 -0
- package/dist/commands/cdp/capture.js +91 -0
- package/dist/commands/cdp/client.js +140 -0
- package/dist/commands/cdp/eval.js +20 -0
- package/dist/commands/cdp/quit.js +84 -0
- package/dist/commands/cdp/relaunch.js +62 -0
- package/dist/commands/cdp/shot.js +20 -0
- package/dist/commands/cdp/status.js +38 -0
- package/dist/commands/cdp/theme.js +21 -0
- package/dist/commands/cdp.js +53 -0
- package/dist/commands/clean.js +34 -0
- package/dist/commands/deploy.js +75 -0
- package/dist/commands/finalize-release.js +238 -0
- package/dist/commands/install.js +37 -0
- package/dist/commands/launch.js +150 -0
- package/dist/commands/lib/dependencies.js +35 -0
- package/dist/commands/lib/deploy-precheck.js +117 -0
- package/dist/commands/lib/ensure-target.js +300 -0
- package/dist/commands/logs.js +185 -0
- package/dist/commands/package.js +31 -0
- package/dist/commands/publish.js +34 -0
- package/dist/commands/push-secrets.js +141 -0
- package/dist/commands/release.js +286 -0
- package/dist/commands/runner.js +1874 -0
- package/dist/commands/sign-windows.js +563 -0
- package/dist/commands/test.js +121 -0
- package/dist/commands/update.js +21 -0
- package/dist/commands/validate-certs.js +238 -0
- package/dist/commands/version.js +9 -0
- package/dist/config/page-template.html +35 -0
- package/dist/defaults/.github/workflows/build.yml +276 -0
- package/dist/defaults/AGENTS.md +109 -0
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +1 -0
- package/dist/defaults/_.gitignore +24 -0
- package/dist/defaults/_mas/README.md +49 -0
- package/dist/defaults/_mas/entitlements.mas.inherit.plist +12 -0
- package/dist/defaults/_mas/entitlements.mas.loginhelper.plist +8 -0
- package/dist/defaults/_mas/entitlements.mas.plist +33 -0
- package/dist/defaults/config/certs/.gitkeep +0 -0
- package/dist/defaults/config/certs/README.md +60 -0
- package/dist/defaults/config/icons/macos/dmg.png +0 -0
- package/dist/defaults/config/icons/macos/icon.png +0 -0
- package/dist/defaults/config/icons/macos/tray.png +0 -0
- package/dist/defaults/config/icons/windows/icon.png +0 -0
- package/dist/defaults/config/omega.json5 +279 -0
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/gulpfile.js +6 -0
- package/dist/defaults/hooks/build/post.js +9 -0
- package/dist/defaults/hooks/build/pre.js +9 -0
- package/dist/defaults/hooks/notarize/post.js +16 -0
- package/dist/defaults/hooks/release/post.js +9 -0
- package/dist/defaults/hooks/release/pre.js +9 -0
- package/dist/defaults/src/assets/js/components/about/index.js +10 -0
- package/dist/defaults/src/assets/js/components/main/index.js +14 -0
- package/dist/defaults/src/assets/js/components/settings/index.js +10 -0
- package/dist/defaults/src/assets/scss/main.scss +34 -0
- package/dist/defaults/src/assets/scss/pages/about.scss +3 -0
- package/dist/defaults/src/assets/scss/pages/main.scss +4 -0
- package/dist/defaults/src/assets/scss/pages/settings.scss +3 -0
- package/dist/defaults/src/integrations/context-menu/index.js +60 -0
- package/dist/defaults/src/integrations/menu/index.js +58 -0
- package/dist/defaults/src/integrations/tray/index.js +60 -0
- package/dist/defaults/src/main.js +78 -0
- package/dist/defaults/src/preload.js +15 -0
- package/dist/defaults/src/views/about/index.html +3 -0
- package/dist/defaults/src/views/main/index.html +8 -0
- package/dist/defaults/src/views/settings/index.html +3 -0
- package/dist/defaults/test/README.md +41 -0
- package/dist/defaults/test/_init.js +10 -0
- package/dist/gulp/main.js +144 -0
- package/dist/gulp/tasks/audit.js +82 -0
- package/dist/gulp/tasks/build-config.js +445 -0
- package/dist/gulp/tasks/bundle.js +451 -0
- package/dist/gulp/tasks/defaults.js +20 -0
- package/dist/gulp/tasks/distribute.js +8 -0
- package/dist/gulp/tasks/html.js +95 -0
- package/dist/gulp/tasks/package-quick.js +102 -0
- package/dist/gulp/tasks/package.js +53 -0
- package/dist/gulp/tasks/release.js +57 -0
- package/dist/gulp/tasks/sass.js +82 -0
- package/dist/gulp/tasks/serve.js +191 -0
- package/dist/hooks/notarize.js +70 -0
- package/dist/index.js +14 -0
- package/dist/lib/_menu-mixin.js +199 -0
- package/dist/lib/analytics.js +360 -0
- package/dist/lib/app-state.js +220 -0
- package/dist/lib/auth-flow.js +160 -0
- package/dist/lib/auth-persistence.js +207 -0
- package/dist/lib/auto-updater.js +752 -0
- package/dist/lib/client-bridge.js +371 -0
- package/dist/lib/context-menu.js +302 -0
- package/dist/lib/context.js +162 -0
- package/dist/lib/deep-link.js +370 -0
- package/dist/lib/fontawesome.js +203 -0
- package/dist/lib/ipc.js +169 -0
- package/dist/lib/logger-lite.js +259 -0
- package/dist/lib/logger.js +4 -0
- package/dist/lib/menu.js +521 -0
- package/dist/lib/protocol.js +85 -0
- package/dist/lib/remote-config.js +224 -0
- package/dist/lib/remote-scripts.js +158 -0
- package/dist/lib/restart-manager/index.js +550 -0
- package/dist/lib/restart-manager/install.js +353 -0
- package/dist/lib/restart-manager/protocol.js +274 -0
- package/dist/lib/sign-helpers/auto-unlock.js +95 -0
- package/dist/lib/sign-helpers/entitlements.js +98 -0
- package/dist/lib/sign-helpers/resolve-icons.js +171 -0
- package/dist/lib/sign-helpers/sign-events.js +75 -0
- package/dist/lib/sign-helpers/update-info.js +186 -0
- package/dist/lib/startup.js +214 -0
- package/dist/lib/storage.js +193 -0
- package/dist/lib/templating.js +85 -0
- package/dist/lib/theme.js +173 -0
- package/dist/lib/tray.js +414 -0
- package/dist/lib/usage.js +141 -0
- package/dist/lib/window-manager.js +509 -0
- package/dist/main.js +542 -0
- package/dist/omega-bin.js +6 -0
- package/dist/preload.js +241 -0
- package/dist/renderer.js +456 -0
- package/dist/test/assert.js +4 -0
- package/dist/test/fixtures/consumer-app/config/omega.json5 +39 -0
- package/dist/test/fixtures/consumer-app/package.json +13 -0
- package/dist/test/fixtures/consumer-app/src/assets/js/components/main/index.js +11 -0
- package/dist/test/fixtures/consumer-app/src/assets/scss/main.scss +3 -0
- package/dist/test/fixtures/consumer-app/src/main.js +17 -0
- package/dist/test/fixtures/consumer-app/src/preload.js +4 -0
- package/dist/test/fixtures/consumer-app/src/views/main/index.html +4 -0
- package/dist/test/harness/boot-entry.js +237 -0
- package/dist/test/harness/main-entry.js +494 -0
- package/dist/test/harness/package.json +6 -0
- package/dist/test/harness/renderer-entry.js +173 -0
- package/dist/test/harness/renderer-preload.js +310 -0
- package/dist/test/harness/renderer.html +12 -0
- package/dist/test/index.js +65 -0
- package/dist/test/runner.js +99 -0
- package/dist/test/runners/boot.js +414 -0
- package/dist/test/runners/electron.js +92 -0
- package/dist/test/runners/render-event.js +46 -0
- package/dist/test/suites/boot/build-isolation.test.js +54 -0
- package/dist/test/suites/boot/consumer-app-boots.test.js +99 -0
- package/dist/test/suites/boot/restart-manager.test.js +38 -0
- package/dist/test/suites/boot/storage-bundled.test.js +59 -0
- package/dist/test/suites/boot/theme.test.js +68 -0
- package/dist/test/suites/boot/view-suite.test.js +47 -0
- package/dist/test/suites/build/attach-log-file.test.js +99 -0
- package/dist/test/suites/build/audit.test.js +271 -0
- package/dist/test/suites/build/boot-fixture.test.js +48 -0
- package/dist/test/suites/build/build-config.test.js +457 -0
- package/dist/test/suites/build/build-verbs.test.js +156 -0
- package/dist/test/suites/build/build-workflow-jobs.test.js +55 -0
- package/dist/test/suites/build/cdp.test.js +89 -0
- package/dist/test/suites/build/cli.test.js +135 -0
- package/dist/test/suites/build/config-schema.test.js +133 -0
- package/dist/test/suites/build/defaults-scaffold.test.js +192 -0
- package/dist/test/suites/build/deliver-certs.test.js +95 -0
- package/dist/test/suites/build/derive-signing-env.test.js +122 -0
- package/dist/test/suites/build/electron-node-version.test.js +49 -0
- package/dist/test/suites/build/electron-targets.test.js +92 -0
- package/dist/test/suites/build/ensure-target.test.js +131 -0
- package/dist/test/suites/build/entitlements.test.js +107 -0
- package/dist/test/suites/build/env-delivery.test.js +166 -0
- package/dist/test/suites/build/env-watch.test.js +86 -0
- package/dist/test/suites/build/exports.test.js +63 -0
- package/dist/test/suites/build/finalize-release.test.js +152 -0
- package/dist/test/suites/build/format-fetch-error.test.js +85 -0
- package/dist/test/suites/build/framework-deps.test.js +173 -0
- package/dist/test/suites/build/get-config.test.js +137 -0
- package/dist/test/suites/build/github-utils.test.js +54 -0
- package/dist/test/suites/build/icon-sheet.test.js +62 -0
- package/dist/test/suites/build/license-stamp.test.js +63 -0
- package/dist/test/suites/build/logger-lite.test.js +148 -0
- package/dist/test/suites/build/logs-command.test.js +226 -0
- package/dist/test/suites/build/manager.test.js +193 -0
- package/dist/test/suites/build/menu-mixin.test.js +130 -0
- package/dist/test/suites/build/merge-line-files.test.js +230 -0
- package/dist/test/suites/build/omega-shell.test.js +94 -0
- package/dist/test/suites/build/omega-tokens.test.js +64 -0
- package/dist/test/suites/build/project-scripts-deps.test.js +90 -0
- package/dist/test/suites/build/push-secrets.test.js +226 -0
- package/dist/test/suites/build/release-pipeline.test.js +365 -0
- package/dist/test/suites/build/renderer-auth-bridge.test.js +158 -0
- package/dist/test/suites/build/renderer-entry-key.test.js +37 -0
- package/dist/test/suites/build/renderer-node-shims.test.js +125 -0
- package/dist/test/suites/build/resolve-icons.test.js +296 -0
- package/dist/test/suites/build/resolve-signing-cert.test.js +342 -0
- package/dist/test/suites/build/restart-manager-install.test.js +178 -0
- package/dist/test/suites/build/restart-manager-protocol.test.js +158 -0
- package/dist/test/suites/build/run-consumer-hook.test.js +137 -0
- package/dist/test/suites/build/runner.test.js +2599 -0
- package/dist/test/suites/build/sanitize-signing-env.test.js +98 -0
- package/dist/test/suites/build/sanitize-url.test.js +78 -0
- package/dist/test/suites/build/secure-remote-url.test.js +42 -0
- package/dist/test/suites/build/sentry.test.js +89 -0
- package/dist/test/suites/build/serve-resolve-electron.test.js +58 -0
- package/dist/test/suites/build/setup-scripts.test.js +172 -0
- package/dist/test/suites/build/sign-events.test.js +90 -0
- package/dist/test/suites/build/sign-windows-e2e.test.js +302 -0
- package/dist/test/suites/build/sign-windows.test.js +313 -0
- package/dist/test/suites/build/strip-dev-blocks.test.js +116 -0
- package/dist/test/suites/build/templating.test.js +95 -0
- package/dist/test/suites/build/test-discovery.test.js +64 -0
- package/dist/test/suites/build/test-stealth.test.js +83 -0
- package/dist/test/suites/build/test-target-no-match.test.js +119 -0
- package/dist/test/suites/build/update-info.test.js +224 -0
- package/dist/test/suites/build/url-helpers.test.js +289 -0
- package/dist/test/suites/build/validate-certs.test.js +103 -0
- package/dist/test/suites/build/validate-config.test.js +90 -0
- package/dist/test/suites/build/verb-logs.test.js +43 -0
- package/dist/test/suites/build/wave5-pins.test.js +35 -0
- package/dist/test/suites/main/analytics.test.js +364 -0
- package/dist/test/suites/main/app-state.test.js +197 -0
- package/dist/test/suites/main/auth-flow.test.js +196 -0
- package/dist/test/suites/main/auth-persistence.test.js +167 -0
- package/dist/test/suites/main/auto-updater.test.js +1138 -0
- package/dist/test/suites/main/boot-sequence.test.js +77 -0
- package/dist/test/suites/main/client-bridge.integration.test.js +134 -0
- package/dist/test/suites/main/client-bridge.test.js +249 -0
- package/dist/test/suites/main/context-menu.test.js +365 -0
- package/dist/test/suites/main/context.test.js +155 -0
- package/dist/test/suites/main/deep-link.test.js +288 -0
- package/dist/test/suites/main/epipe-handler.test.js +40 -0
- package/dist/test/suites/main/fontawesome.test.js +147 -0
- package/dist/test/suites/main/harness-cdp.test.js +49 -0
- package/dist/test/suites/main/ipc.test.js +165 -0
- package/dist/test/suites/main/menu.test.js +419 -0
- package/dist/test/suites/main/protocol.test.js +77 -0
- package/dist/test/suites/main/remote-config.test.js +189 -0
- package/dist/test/suites/main/remote-scripts.test.js +276 -0
- package/dist/test/suites/main/restart-manager.test.js +328 -0
- package/dist/test/suites/main/startup-paths-and-ua.test.js +99 -0
- package/dist/test/suites/main/startup.test.js +129 -0
- package/dist/test/suites/main/stealth-window.test.js +74 -0
- package/dist/test/suites/main/storage.test.js +89 -0
- package/dist/test/suites/main/theme.test.js +137 -0
- package/dist/test/suites/main/tray.test.js +322 -0
- package/dist/test/suites/main/truncate-on-boot.test.js +22 -0
- package/dist/test/suites/main/url-helpers.test.js +429 -0
- package/dist/test/suites/main/usage.test.js +127 -0
- package/dist/test/suites/main/window-bounds.test.js +169 -0
- package/dist/test/suites/main/window-manager.test.js +233 -0
- package/dist/test/suites/renderer/analytics-bridge.test.js +128 -0
- package/dist/test/suites/renderer/cross-context-helpers.test.js +131 -0
- package/dist/test/suites/renderer/fontawesome.test.js +190 -0
- package/dist/test/suites/renderer/round-trip.test.js +107 -0
- package/dist/test/suites/renderer/theme.test.js +118 -0
- package/dist/test/suites/renderer/tooltips.test.js +166 -0
- package/dist/test/suites/renderer/verts.test.js +100 -0
- package/dist/test/suites/renderer/window-em-surface.test.js +107 -0
- package/dist/test/utils/dist-snapshot.js +36 -0
- package/dist/test/utils/extended-mode-warning.js +13 -0
- package/dist/utils/app-root.js +28 -0
- package/dist/utils/attach-log-file.js +4 -0
- package/dist/utils/build-pipeline.js +58 -0
- package/dist/utils/deliver-certs.js +69 -0
- package/dist/utils/derive-signing-env.js +56 -0
- package/dist/utils/dist-root.js +21 -0
- package/dist/utils/electron-node-version.js +48 -0
- package/dist/utils/electron-targets.js +72 -0
- package/dist/utils/format-fetch-error.js +38 -0
- package/dist/utils/github.js +73 -0
- package/dist/utils/load-consumer-file.js +26 -0
- package/dist/utils/merge-line-files.js +5 -0
- package/dist/utils/mode-helpers.js +147 -0
- package/dist/utils/resolve-signing-cert.js +175 -0
- package/dist/utils/run-consumer-hook.js +38 -0
- package/dist/utils/runner-env.js +687 -0
- package/dist/utils/safe-install.js +4 -0
- package/dist/utils/sanitize-signing-env.js +45 -0
- package/dist/utils/sanitize-url.js +28 -0
- package/dist/utils/secure-remote-url.js +25 -0
- package/dist/utils/stealth-window.js +37 -0
- package/dist/utils/test-stealth.js +29 -0
- package/dist/utils/url-helpers.js +169 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/actions-secrets.js +145 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bundle.js +292 -0
- package/dist/vendor/devkit/certs.js +199 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/clean-dirs.js +28 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-precheck.js +54 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/empty-modules-plugin.js +64 -0
- package/dist/vendor/devkit/env-watch.js +132 -0
- package/dist/vendor/devkit/framework-deps.js +46 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/npm-registry.js +52 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/prompt.js +187 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/strip-dev-blocks-plugin.js +29 -0
- package/dist/vendor/devkit/strip-dev-blocks.js +32 -0
- package/dist/vendor/devkit/target-dependencies.js +137 -0
- package/dist/vendor/devkit/target-secrets.js +165 -0
- package/dist/vendor/devkit/test/assert.js +120 -0
- package/dist/vendor/devkit/test/define-cases.js +104 -0
- package/dist/vendor/devkit/test/extended-mode-warning.js +22 -0
- package/dist/vendor/devkit/test/runner-core.js +554 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/env.js +49 -0
- package/dist/vendor/monitoring/index.js +28 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/dist/vendor/monitoring/main.js +99 -0
- package/dist/vendor/monitoring/preload.js +45 -0
- package/dist/vendor/monitoring/renderer.js +87 -0
- package/docs/analytics.md +140 -0
- package/docs/app-state.md +92 -0
- package/docs/audit.md +69 -0
- package/docs/auto-updater.md +243 -0
- package/docs/boot-sequence.md +39 -0
- package/docs/build-system.md +161 -0
- package/docs/cdp-debugging.md +169 -0
- package/docs/client-bridge.md +267 -0
- package/docs/common-mistakes.md +21 -0
- package/docs/config-schema.md +120 -0
- package/docs/context-menu.md +112 -0
- package/docs/context.md +81 -0
- package/docs/css.md +84 -0
- package/docs/deep-link.md +178 -0
- package/docs/environment-detection.md +104 -0
- package/docs/fontawesome.md +107 -0
- package/docs/hooks.md +87 -0
- package/docs/icons.md +79 -0
- package/docs/index.md +279 -0
- package/docs/installer-options.md +162 -0
- package/docs/ipc.md +61 -0
- package/docs/lib-modules.md +53 -0
- package/docs/logging.md +229 -0
- package/docs/menu.md +160 -0
- package/docs/releasing.md +229 -0
- package/docs/remote-config.md +118 -0
- package/docs/remote-scripts.md +144 -0
- package/docs/restart-manager.md +144 -0
- package/docs/runner.md +253 -0
- package/docs/sentry.md +97 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/signing.md +267 -0
- package/docs/startup.md +142 -0
- package/docs/storage.md +59 -0
- package/docs/templating.md +101 -0
- package/docs/test-boot-layer.md +157 -0
- package/docs/test-framework.md +357 -0
- package/docs/themes.md +135 -0
- package/docs/tooltips.md +99 -0
- package/docs/tray.md +164 -0
- package/docs/usage.md +58 -0
- package/docs/verts.md +62 -0
- package/docs/windows.md +149 -0
- package/package.json +184 -0
- package/scripts/sync-nvmrc.js +100 -0
|
@@ -0,0 +1,2599 @@
|
|
|
1
|
+
// Build-layer tests for the runner command. Most behavior is platform-gated to Windows;
|
|
2
|
+
// what we can verify on Mac is: module shape, error paths for non-Windows, error messages
|
|
3
|
+
// for missing GH_TOKEN, subcommand dispatch.
|
|
4
|
+
//
|
|
5
|
+
// ⚠ THE SUITE MAY NEVER ACT ON A REAL BOX.
|
|
6
|
+
// 2026-09-04, on the signing box: two cases called `runner install` with no home
|
|
7
|
+
// of their own. RUNNER_HOME is resolved when runner.js is REQUIRED, so it was
|
|
8
|
+
// the box's own `%LOCALAPPDATA%\omega-runner`; the config walk read the saved
|
|
9
|
+
// GH_TOKEN back out of that home's `.env` after the case deleted it from the
|
|
10
|
+
// environment, a blank OMEGA_RUNNER_ORGS off a TTY means every admin org, and
|
|
11
|
+
// the lane tore the box's runner down and registered 34 orgs for real.
|
|
12
|
+
//
|
|
13
|
+
// So the redirect happens HERE, at the top of the file, before any require of
|
|
14
|
+
// runner.js can freeze the wrong home — and `OMEGA_TEST_RUNNER` arms the
|
|
15
|
+
// production refusal in utils/runner-env.js, which catches the same mistake in
|
|
16
|
+
// any suite that forgets this. The guard case below pins both.
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const defineCases = require('../../../vendor/devkit/test/define-cases.js');
|
|
21
|
+
|
|
22
|
+
// <packages/desktop>/.temp/runner-home-<pid> — fresh per run, never a real home.
|
|
23
|
+
const SCRATCH_HOME = path.join(__dirname, '..', '..', '..', '..', '.temp', `runner-home-${process.pid}`);
|
|
24
|
+
fs.rmSync(SCRATCH_HOME, { recursive: true, force: true });
|
|
25
|
+
fs.mkdirSync(SCRATCH_HOME, { recursive: true });
|
|
26
|
+
process.env.OMEGA_RUNNER_HOME = SCRATCH_HOME;
|
|
27
|
+
// The Startup folder is the second machine-wide surface, and no home scopes it:
|
|
28
|
+
// `uninstall` sweeps every `omega-runner-*.cmd` in it. Same redirect, same
|
|
29
|
+
// reason, and the guard refuses every mutating verb on the box without it.
|
|
30
|
+
const SCRATCH_STARTUP_DIR = path.join(__dirname, '..', '..', '..', '..', '.temp', `runner-startup-${process.pid}`);
|
|
31
|
+
fs.rmSync(SCRATCH_STARTUP_DIR, { recursive: true, force: true });
|
|
32
|
+
fs.mkdirSync(SCRATCH_STARTUP_DIR, { recursive: true });
|
|
33
|
+
process.env.OMEGA_RUNNER_STARTUP_DIR = SCRATCH_STARTUP_DIR;
|
|
34
|
+
process.env.OMEGA_TEST_RUNNER = '1';
|
|
35
|
+
|
|
36
|
+
module.exports = defineCases({
|
|
37
|
+
type: 'suite',
|
|
38
|
+
layer: 'build',
|
|
39
|
+
description: 'runner command — Windows EV-token signing runner',
|
|
40
|
+
tests: [
|
|
41
|
+
{
|
|
42
|
+
name: 'the suite can never touch a real box: RUNNER_HOME is a scratch under .temp',
|
|
43
|
+
run: (ctx) => {
|
|
44
|
+
// If this fails, STOP: every mutating case below would act on the home
|
|
45
|
+
// it names. Nothing here asserts a value the module computed for itself
|
|
46
|
+
// — the point is that the redirect above won the race with the require.
|
|
47
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
48
|
+
|
|
49
|
+
ctx.expect(runner.RUNNER_HOME).toBe(SCRATCH_HOME);
|
|
50
|
+
ctx.expect(runner.RUNNER_HOME.split(path.sep)).toContain('.temp');
|
|
51
|
+
|
|
52
|
+
// The Startup folder froze at require time the same way, and it is the
|
|
53
|
+
// surface no home scopes — an unpinned one is the box's own.
|
|
54
|
+
ctx.expect(runner.STARTUP_DIR).toBe(SCRATCH_STARTUP_DIR);
|
|
55
|
+
ctx.expect(runner.STARTUP_DIR.split(path.sep)).toContain('.temp');
|
|
56
|
+
|
|
57
|
+
// …and it is not the box's own home, by any of the names that home has.
|
|
58
|
+
ctx.expect(runner.RUNNER_HOME).not.toBe(runner.defaultRunnerHome('win32', { LOCALAPPDATA: 'C:\\Users\\me\\AppData\\Local' }));
|
|
59
|
+
ctx.expect(runner.RUNNER_HOME).not.toBe(runner.defaultRunnerHome('darwin', {}));
|
|
60
|
+
for (const real of [process.env.LOCALAPPDATA, process.env.APPDATA].filter(Boolean)) {
|
|
61
|
+
ctx.expect(runner.RUNNER_HOME.startsWith(real)).toBe(false);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'a test process is REFUSED against a home that is not a scratch — every mutating verb, whichever home is real',
|
|
67
|
+
run: async (ctx) => {
|
|
68
|
+
// The backstop for the suite that forgets the redirect above: with a
|
|
69
|
+
// test marker set, the mutating verbs will not touch a real home at all.
|
|
70
|
+
const os = require('os');
|
|
71
|
+
const jetpack = require('fs-jetpack');
|
|
72
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
73
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
74
|
+
|
|
75
|
+
const realish = path.join(os.homedir(), 'omega-runner-NOT-A-SCRATCH');
|
|
76
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
77
|
+
process.env.OMEGA_RUNNER_FORCE = '1'; // the platform gate must not be what saves us
|
|
78
|
+
jetpack.remove(realish); // a RED run of this case is what would create it
|
|
79
|
+
try {
|
|
80
|
+
// Every verb that CHANGES the machine — stop kills real listeners under
|
|
81
|
+
// the home, self-update runs a global npm install.
|
|
82
|
+
for (const sub of ['install', 'config', 'register-org', 'start', 'restart', 'stop', 'uninstall', 'self-update']) {
|
|
83
|
+
let threw;
|
|
84
|
+
try {
|
|
85
|
+
// `_env: {}` is a fixture for the config walk. It is NOT an answer
|
|
86
|
+
// to "am I a test process" — that comes from process.env, always,
|
|
87
|
+
// or a case could hand the guard an empty object and disarm it.
|
|
88
|
+
await runner({ _: ['runner', sub, 'some-org'], _home: realish, _env: {}, _interactive: false });
|
|
89
|
+
} catch (e) { threw = e; }
|
|
90
|
+
ctx.expect(threw).toBeDefined();
|
|
91
|
+
ctx.expect(threw.message).toContain('OMEGA_TEST_RUNNER'); // names the variable
|
|
92
|
+
ctx.expect(threw.message).toContain(realish);
|
|
93
|
+
}
|
|
94
|
+
ctx.expect(jetpack.exists(realish)).toBe(false); // nothing was created
|
|
95
|
+
|
|
96
|
+
// Read-only verbs are not the danger and stay usable.
|
|
97
|
+
ctx.expect(runner.isScratchRunnerHome(SCRATCH_HOME)).toBe(true);
|
|
98
|
+
ctx.expect(runner.isScratchRunnerHome(realish)).toBe(false);
|
|
99
|
+
ctx.expect(runner.isScratchRunnerHome(fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-')))).toBe(true);
|
|
100
|
+
|
|
101
|
+
// BOTH homes are checked, and the refusal names the one that is real:
|
|
102
|
+
// the module-level RUNNER_HOME is the home whose `.env` was already
|
|
103
|
+
// read into this process at require time.
|
|
104
|
+
let threw;
|
|
105
|
+
try {
|
|
106
|
+
runnerEnv.assertTestSafeRunnerHome('install', SCRATCH_HOME, realish);
|
|
107
|
+
} catch (e) { threw = e; }
|
|
108
|
+
ctx.expect(threw).toBeDefined();
|
|
109
|
+
ctx.expect(threw.message).toContain(realish);
|
|
110
|
+
ctx.expect(threw.message).not.toContain(SCRATCH_HOME);
|
|
111
|
+
runnerEnv.assertTestSafeRunnerHome('install', SCRATCH_HOME, SCRATCH_HOME); // both scratch: allowed
|
|
112
|
+
} finally {
|
|
113
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
114
|
+
jetpack.remove(realish);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'a real module-level RUNNER_HOME refuses even when the call passes a scratch _home',
|
|
120
|
+
run: (ctx) => {
|
|
121
|
+
// RUNNER_HOME is frozen when the module is required, so this one needs
|
|
122
|
+
// its own process: the box's shape exactly — a real home in the
|
|
123
|
+
// environment, a test marker, and a caller that thinks `_home` saves it.
|
|
124
|
+
const os = require('os');
|
|
125
|
+
const jetpack = require('fs-jetpack');
|
|
126
|
+
const { spawnSync } = require('child_process');
|
|
127
|
+
|
|
128
|
+
const fakeProfile = path.join(os.homedir(), `omega-runner-fake-profile-${process.pid}`);
|
|
129
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
130
|
+
const commandPath = path.join(__dirname, '..', '..', '..', 'commands', 'runner.js');
|
|
131
|
+
try {
|
|
132
|
+
const script = [
|
|
133
|
+
`const runner = require(${JSON.stringify(commandPath)});`,
|
|
134
|
+
`runner({ _: ['runner', 'install'], _home: ${JSON.stringify(scratch)}, _interactive: false })`,
|
|
135
|
+
` .then(() => process.stdout.write('RAN'))`,
|
|
136
|
+
` .catch((e) => process.stdout.write(e.message));`,
|
|
137
|
+
].join('\n');
|
|
138
|
+
const out = spawnSync(process.execPath, ['-e', script], {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
env: {
|
|
141
|
+
...process.env,
|
|
142
|
+
OMEGA_RUNNER_HOME: fakeProfile, // what the box has: a real home
|
|
143
|
+
OMEGA_TEST_RUNNER: '1',
|
|
144
|
+
OMEGA_RUNNER_FORCE: '1', // the platform gate must not be what saves us
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
ctx.expect(out.stdout).toContain('OMEGA_TEST_RUNNER');
|
|
149
|
+
ctx.expect(out.stdout).toContain(fakeProfile);
|
|
150
|
+
ctx.expect(out.stdout.includes('RAN')).toBe(false);
|
|
151
|
+
ctx.expect(jetpack.exists(fakeProfile)).toBe(false);
|
|
152
|
+
ctx.expect(jetpack.list(scratch) || []).toEqual([]); // and the scratch was not installed into either
|
|
153
|
+
} finally {
|
|
154
|
+
jetpack.remove(fakeProfile);
|
|
155
|
+
jetpack.remove(scratch);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: '`status` reports the home the call passed, never the module-level RUNNER_HOME',
|
|
161
|
+
run: (ctx) => {
|
|
162
|
+
// `_home` is the whole redirect: a suite that points it at a scratch must
|
|
163
|
+
// get a scratch, whatever home the module froze at require time. Two homes,
|
|
164
|
+
// both real on disk, and only B's contents may show up in the output.
|
|
165
|
+
const os = require('os');
|
|
166
|
+
const jetpack = require('fs-jetpack');
|
|
167
|
+
const { spawnSync } = require('child_process');
|
|
168
|
+
|
|
169
|
+
const homeA = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-a-'));
|
|
170
|
+
const homeB = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-b-'));
|
|
171
|
+
const commandPath = path.join(__dirname, '..', '..', '..', 'commands', 'runner.js');
|
|
172
|
+
try {
|
|
173
|
+
jetpack.write(path.join(homeA, 'config.json'), { registeredOrgs: ['org-in-a'] });
|
|
174
|
+
jetpack.write(path.join(homeB, 'config.json'), { registeredOrgs: ['org-in-b'] });
|
|
175
|
+
|
|
176
|
+
const script = [
|
|
177
|
+
`const runner = require(${JSON.stringify(commandPath)});`,
|
|
178
|
+
`runner({ _: ['runner', 'status'], _home: ${JSON.stringify(homeB)}, _interactive: false })`,
|
|
179
|
+
` .then(() => process.stdout.write('\\nRAN'))`,
|
|
180
|
+
` .catch((e) => process.stdout.write('\\nTHREW ' + e.message));`,
|
|
181
|
+
].join('\n');
|
|
182
|
+
const out = spawnSync(process.execPath, ['-e', script], {
|
|
183
|
+
encoding: 'utf8',
|
|
184
|
+
env: {
|
|
185
|
+
...process.env,
|
|
186
|
+
OMEGA_RUNNER_HOME: homeA, // the home the module freezes
|
|
187
|
+
OMEGA_TEST_RUNNER: '1',
|
|
188
|
+
OMEGA_RUNNER_FORCE: '1', // status is Windows-only otherwise
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
ctx.expect(out.stdout).toContain('RAN');
|
|
193
|
+
ctx.expect(out.stdout).toContain(`omega-runner home: ${homeB}`);
|
|
194
|
+
ctx.expect(out.stdout).toContain(`Log: ${path.join(homeB, 'logs', 'runner.log')}`);
|
|
195
|
+
ctx.expect(out.stdout).toContain('org-in-b'); // B's install record
|
|
196
|
+
ctx.expect(out.stdout.includes(homeA)).toBe(false); // and not one word about A
|
|
197
|
+
ctx.expect(out.stdout.includes('org-in-a')).toBe(false);
|
|
198
|
+
} finally {
|
|
199
|
+
jetpack.remove(homeA);
|
|
200
|
+
jetpack.remove(homeB);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: '`uninstall` tears down the home the call passed, leaving the module-level one whole',
|
|
206
|
+
run: (ctx) => {
|
|
207
|
+
// The destructive half of the same contract. Neither org dir carries a
|
|
208
|
+
// config.cmd, so the deregistration roster is empty and nothing reaches
|
|
209
|
+
// GitHub — what is left to prove is which home lost its tree.
|
|
210
|
+
const os = require('os');
|
|
211
|
+
const jetpack = require('fs-jetpack');
|
|
212
|
+
const { spawnSync } = require('child_process');
|
|
213
|
+
|
|
214
|
+
const homeA = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-a-'));
|
|
215
|
+
const homeB = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-b-'));
|
|
216
|
+
// The Startup folder is machine-wide — NEITHER home scopes it. Round 4
|
|
217
|
+
// ran this case on the box without a seam here and `uninstall` deleted
|
|
218
|
+
// the box's three real shortcuts. It gets a scratch of its own now, and
|
|
219
|
+
// the sweep may only ever reach into that.
|
|
220
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
221
|
+
const host = os.hostname().toLowerCase();
|
|
222
|
+
const commandPath = path.join(__dirname, '..', '..', '..', 'commands', 'runner.js');
|
|
223
|
+
try {
|
|
224
|
+
jetpack.write(path.join(homeA, 'config.json'), { registeredOrgs: ['org-in-a'] });
|
|
225
|
+
jetpack.dir(path.join(homeA, 'actions-runner-org-in-a'));
|
|
226
|
+
jetpack.write(path.join(homeB, 'config.json'), { registeredOrgs: ['org-in-b'] });
|
|
227
|
+
jetpack.dir(path.join(homeB, 'actions-runner-org-in-b'));
|
|
228
|
+
// Both, whichever home they read as: the sweep takes every
|
|
229
|
+
// `omega-runner-*` in the folder it is given, which is the contract.
|
|
230
|
+
jetpack.write(path.join(startupDir, `omega-runner-${host}-org-in-b.cmd`), '@echo off\r\n');
|
|
231
|
+
jetpack.write(path.join(startupDir, `omega-runner-${host}-org-in-a.cmd`), '@echo off\r\n');
|
|
232
|
+
|
|
233
|
+
const script = [
|
|
234
|
+
`const runner = require(${JSON.stringify(commandPath)});`,
|
|
235
|
+
`runner({ _: ['runner', 'uninstall'], _home: ${JSON.stringify(homeB)}, _interactive: false })`,
|
|
236
|
+
` .then(() => process.stdout.write('\\nRAN'))`,
|
|
237
|
+
` .catch((e) => process.stdout.write('\\nTHREW ' + e.message));`,
|
|
238
|
+
].join('\n');
|
|
239
|
+
const out = spawnSync(process.execPath, ['-e', script], {
|
|
240
|
+
encoding: 'utf8',
|
|
241
|
+
env: {
|
|
242
|
+
...process.env,
|
|
243
|
+
OMEGA_RUNNER_HOME: homeA,
|
|
244
|
+
OMEGA_RUNNER_STARTUP_DIR: startupDir,
|
|
245
|
+
OMEGA_TEST_RUNNER: '1',
|
|
246
|
+
OMEGA_RUNNER_FORCE: '1',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
ctx.expect(out.stdout).toContain('RAN');
|
|
251
|
+
// B lost its install; only the logs dir the run's own tee wrote survives.
|
|
252
|
+
ctx.expect(jetpack.list(homeB)).toEqual(['logs']);
|
|
253
|
+
// A was never touched.
|
|
254
|
+
ctx.expect(jetpack.list(homeA).sort()).toEqual(['actions-runner-org-in-a', 'config.json']);
|
|
255
|
+
// The sweep reached the scratch and nothing else. `removeRunnerStartupShortcut`
|
|
256
|
+
// keeps its win32 early return, so off Windows the shortcuts survive —
|
|
257
|
+
// what the seam proves on every platform is WHICH folder was listed.
|
|
258
|
+
const sweptStartup = process.platform === 'win32'
|
|
259
|
+
? []
|
|
260
|
+
: [`omega-runner-${host}-org-in-a.cmd`, `omega-runner-${host}-org-in-b.cmd`];
|
|
261
|
+
ctx.expect((jetpack.list(startupDir) || []).sort()).toEqual(sweptStartup);
|
|
262
|
+
// The surfaces no home and no seam can scope are not touched at all.
|
|
263
|
+
ctx.expect(out.stdout).toContain('Test run: skipping the machine-wide sweeps (services, logon tasks, legacy homes).');
|
|
264
|
+
} finally {
|
|
265
|
+
jetpack.remove(homeA);
|
|
266
|
+
jetpack.remove(homeB);
|
|
267
|
+
jetpack.remove(startupDir);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'a test run refuses every mutating subcommand while the Startup dir is the real one',
|
|
273
|
+
run: (ctx) => {
|
|
274
|
+
// The round-4 hole, pinned: both homes scratch, and the run still swept
|
|
275
|
+
// the box's real Startup folder because no home scopes it. Now the folder
|
|
276
|
+
// is checked like a home — a non-scratch one refuses before anything runs.
|
|
277
|
+
const os = require('os');
|
|
278
|
+
const jetpack = require('fs-jetpack');
|
|
279
|
+
const { spawnSync } = require('child_process');
|
|
280
|
+
|
|
281
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
282
|
+
const realStartup = path.join(os.homedir(), 'omega-runner-startup-never-created');
|
|
283
|
+
const commandPath = path.join(__dirname, '..', '..', '..', 'commands', 'runner.js');
|
|
284
|
+
try {
|
|
285
|
+
jetpack.write(path.join(scratch, 'config.json'), { registeredOrgs: ['org-in-scratch'] });
|
|
286
|
+
jetpack.dir(path.join(scratch, 'actions-runner-org-in-scratch'));
|
|
287
|
+
|
|
288
|
+
const script = [
|
|
289
|
+
`const runner = require(${JSON.stringify(commandPath)});`,
|
|
290
|
+
`runner({ _: ['runner', 'uninstall'], _home: ${JSON.stringify(scratch)}, _interactive: false })`,
|
|
291
|
+
` .then(() => process.stdout.write('\\nRAN'))`,
|
|
292
|
+
` .catch((e) => process.stdout.write('\\nTHREW ' + e.message));`,
|
|
293
|
+
].join('\n');
|
|
294
|
+
const out = spawnSync(process.execPath, ['-e', script], {
|
|
295
|
+
encoding: 'utf8',
|
|
296
|
+
env: {
|
|
297
|
+
...process.env,
|
|
298
|
+
OMEGA_RUNNER_HOME: scratch, // both homes scratch…
|
|
299
|
+
OMEGA_RUNNER_STARTUP_DIR: realStartup, // …and the third surface real
|
|
300
|
+
OMEGA_TEST_RUNNER: '1',
|
|
301
|
+
OMEGA_RUNNER_FORCE: '1',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
ctx.expect(out.stdout).toContain('THREW');
|
|
306
|
+
ctx.expect(out.stdout.includes('RAN')).toBe(false);
|
|
307
|
+
ctx.expect(out.stdout).toContain(realStartup); // names the offender
|
|
308
|
+
ctx.expect(out.stdout).toContain('OMEGA_RUNNER_STARTUP_DIR'); // and the seam that fixes it
|
|
309
|
+
// The refusal wrote nothing: the scratch home is whole and the real
|
|
310
|
+
// Startup path was never so much as created.
|
|
311
|
+
ctx.expect(jetpack.list(scratch).sort()).toEqual(['actions-runner-org-in-scratch', 'config.json']);
|
|
312
|
+
ctx.expect(jetpack.exists(realStartup)).toBe(false);
|
|
313
|
+
} finally {
|
|
314
|
+
jetpack.remove(scratch);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: '`monitor` lists the orgs of the home the call passed, not the module-level one',
|
|
320
|
+
timeout: 30000,
|
|
321
|
+
run: (ctx) => {
|
|
322
|
+
// monitor tails forever by design, so the child is killed once it has
|
|
323
|
+
// printed its roster — the block it prints on start is the whole
|
|
324
|
+
// assertion, and it comes from that home's config.json.
|
|
325
|
+
const os = require('os');
|
|
326
|
+
const jetpack = require('fs-jetpack');
|
|
327
|
+
const { spawnSync } = require('child_process');
|
|
328
|
+
|
|
329
|
+
const homeA = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-a-'));
|
|
330
|
+
const homeB = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-b-'));
|
|
331
|
+
const commandPath = path.join(__dirname, '..', '..', '..', 'commands', 'runner.js');
|
|
332
|
+
try {
|
|
333
|
+
jetpack.write(path.join(homeA, 'config.json'), { registeredOrgs: ['org-in-a'] });
|
|
334
|
+
jetpack.write(path.join(homeB, 'config.json'), { registeredOrgs: ['org-in-b'] });
|
|
335
|
+
|
|
336
|
+
const script = [
|
|
337
|
+
`const runner = require(${JSON.stringify(commandPath)});`,
|
|
338
|
+
// `file` so the roster is the only thing the log path can affect.
|
|
339
|
+
`runner({ _: ['runner', 'monitor'], file: ${JSON.stringify(path.join(homeB, 'omega-signing.log'))}, _home: ${JSON.stringify(homeB)} });`,
|
|
340
|
+
].join('\n');
|
|
341
|
+
const out = spawnSync(process.execPath, ['-e', script], {
|
|
342
|
+
encoding: 'utf8',
|
|
343
|
+
timeout: 8000, // it never exits on its own (the Windows process scan is slow)
|
|
344
|
+
env: {
|
|
345
|
+
...process.env,
|
|
346
|
+
OMEGA_RUNNER_HOME: homeA,
|
|
347
|
+
OMEGA_TEST_RUNNER: '1',
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
ctx.expect(out.stdout).toContain('Monitoring signing requests across 1 org(s)');
|
|
352
|
+
ctx.expect(out.stdout).toContain('org-in-b');
|
|
353
|
+
ctx.expect(out.stdout.includes('org-in-a')).toBe(false);
|
|
354
|
+
ctx.expect(out.stdout.includes(homeA)).toBe(false);
|
|
355
|
+
} finally {
|
|
356
|
+
jetpack.remove(homeA);
|
|
357
|
+
jetpack.remove(homeB);
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'runner command exports a function',
|
|
363
|
+
run: (ctx) => {
|
|
364
|
+
const mod = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
365
|
+
ctx.expect(typeof mod).toBe('function');
|
|
366
|
+
ctx.expect(Array.isArray(mod.RUNNER_LABELS)).toBe(true);
|
|
367
|
+
ctx.expect(mod.RUNNER_LABELS).toContain('self-hosted');
|
|
368
|
+
ctx.expect(mod.RUNNER_LABELS).toContain('windows');
|
|
369
|
+
ctx.expect(mod.RUNNER_LABELS).toContain('ev-token');
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'runner pinned actions/runner version is set',
|
|
374
|
+
run: (ctx) => {
|
|
375
|
+
const mod = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
376
|
+
ctx.expect(typeof mod.ACTIONS_RUNNER_VERSION).toBe('string');
|
|
377
|
+
ctx.expect(mod.ACTIONS_RUNNER_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: 'runner unknown subcommand throws',
|
|
382
|
+
run: async (ctx) => {
|
|
383
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
384
|
+
let threw;
|
|
385
|
+
try {
|
|
386
|
+
await runner({ _: ['runner', 'banana'] });
|
|
387
|
+
} catch (e) { threw = e; }
|
|
388
|
+
ctx.expect(threw).toBeDefined();
|
|
389
|
+
ctx.expect(threw.message).toMatch(/Unknown runner subcommand/);
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'runner install on non-Windows refuses without override — against the scratch home, with no token anywhere',
|
|
394
|
+
run: async (ctx) => {
|
|
395
|
+
// This case used to delete GH_TOKEN and trust "the token check fires".
|
|
396
|
+
// On the box it did not: the walk read the saved token back out of the
|
|
397
|
+
// REAL home's .env and installed for real (2026-09-04). Now the home is
|
|
398
|
+
// the suite's scratch, its .env carries no token, and the environment
|
|
399
|
+
// handed to the walk carries none either — there is no path left where a
|
|
400
|
+
// real token can reach this case.
|
|
401
|
+
const jetpack = require('fs-jetpack');
|
|
402
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
403
|
+
|
|
404
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
405
|
+
const origToken = process.env.GH_TOKEN;
|
|
406
|
+
delete process.env.OMEGA_RUNNER_FORCE;
|
|
407
|
+
delete process.env.GH_TOKEN;
|
|
408
|
+
jetpack.write(path.join(SCRATCH_HOME, '.env'), 'GH_TOKEN=""\n');
|
|
409
|
+
|
|
410
|
+
let threw;
|
|
411
|
+
try {
|
|
412
|
+
await runner({ _: ['runner', 'install'], _home: SCRATCH_HOME, _env: {}, _interactive: false });
|
|
413
|
+
} catch (e) { threw = e; }
|
|
414
|
+
|
|
415
|
+
if (origForce !== undefined) process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
416
|
+
if (origToken !== undefined) process.env.GH_TOKEN = origToken;
|
|
417
|
+
|
|
418
|
+
ctx.expect(threw).toBeDefined();
|
|
419
|
+
// Off Windows the platform check wins; on the box the missing-token
|
|
420
|
+
// refusal does. Either way nothing was installed.
|
|
421
|
+
ctx.expect(threw.message).toMatch(process.platform === 'win32' ? /GH_TOKEN/ : /only runs on Windows/);
|
|
422
|
+
ctx.expect(jetpack.exists(path.join(SCRATCH_HOME, '_template'))).toBe(false);
|
|
423
|
+
ctx.expect(jetpack.exists(path.join(SCRATCH_HOME, 'config.json'))).toBe(false);
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'runner register-org without org throws clear error',
|
|
428
|
+
run: async (ctx) => {
|
|
429
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
430
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
431
|
+
const origToken = process.env.GH_TOKEN;
|
|
432
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
433
|
+
process.env.GH_TOKEN = 'ghp_test_dummy';
|
|
434
|
+
|
|
435
|
+
let threw;
|
|
436
|
+
try {
|
|
437
|
+
await runner({ _: ['runner', 'register-org'] }); // no org
|
|
438
|
+
} catch (e) { threw = e; }
|
|
439
|
+
|
|
440
|
+
if (origForce !== undefined) process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
441
|
+
else delete process.env.OMEGA_RUNNER_FORCE;
|
|
442
|
+
if (origToken !== undefined) process.env.GH_TOKEN = origToken;
|
|
443
|
+
else delete process.env.GH_TOKEN;
|
|
444
|
+
|
|
445
|
+
ctx.expect(threw).toBeDefined();
|
|
446
|
+
ctx.expect(threw.message).toMatch(/Usage:.*register-org/);
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: 'runner install without GH_TOKEN throws — off a TTY, naming the key, nothing registered',
|
|
451
|
+
run: async (ctx) => {
|
|
452
|
+
const jetpack = require('fs-jetpack');
|
|
453
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
454
|
+
|
|
455
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
456
|
+
const origToken = process.env.GH_TOKEN;
|
|
457
|
+
process.env.OMEGA_RUNNER_FORCE = '1'; // the platform gate passes: the TOKEN check is what must fire
|
|
458
|
+
delete process.env.GH_TOKEN;
|
|
459
|
+
jetpack.write(path.join(SCRATCH_HOME, '.env'), 'GH_TOKEN=""\n'); // and the home has none saved either
|
|
460
|
+
|
|
461
|
+
let threw;
|
|
462
|
+
try {
|
|
463
|
+
await runner({ _: ['runner', 'install'], _home: SCRATCH_HOME, _env: {}, _interactive: false });
|
|
464
|
+
} catch (e) { threw = e; }
|
|
465
|
+
|
|
466
|
+
if (origForce !== undefined) process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
467
|
+
else delete process.env.OMEGA_RUNNER_FORCE;
|
|
468
|
+
if (origToken !== undefined) process.env.GH_TOKEN = origToken;
|
|
469
|
+
|
|
470
|
+
ctx.expect(threw).toBeDefined();
|
|
471
|
+
ctx.expect(threw.message).toContain('GH_TOKEN');
|
|
472
|
+
ctx.expect(threw.message).toContain('no terminal to ask in');
|
|
473
|
+
// Off a TTY it refuses BEFORE it acts: no download, no registration.
|
|
474
|
+
ctx.expect(jetpack.exists(path.join(SCRATCH_HOME, '_template'))).toBe(false);
|
|
475
|
+
ctx.expect(jetpack.exists(path.join(SCRATCH_HOME, 'config.json'))).toBe(false);
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: 'GH_TOKEN error message recommends admin:org (not manage_runners:org)',
|
|
480
|
+
run: (ctx) => {
|
|
481
|
+
// Guards against the regression where docs/code suggested manage_runners:org —
|
|
482
|
+
// GitHub's runner-registration endpoint requires admin:org for classic PATs.
|
|
483
|
+
const fs = require('fs');
|
|
484
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
485
|
+
ctx.expect(src).toContain('admin:org');
|
|
486
|
+
ctx.expect(src).not.toMatch(/lacks manage_runners:org/);
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: 'downloadActionsRunner uses Expand-Archive on Windows + tar fallback elsewhere',
|
|
491
|
+
run: (ctx) => {
|
|
492
|
+
// 1.2.36+ uses PowerShell Expand-Archive on Windows because Git-for-Windows'
|
|
493
|
+
// GNU tar misinterprets `C:\...` paths as `host:path`. tar still used on
|
|
494
|
+
// mac/linux for the framework-dev path.
|
|
495
|
+
const fs = require('fs');
|
|
496
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
497
|
+
ctx.expect(src).toContain('Expand-Archive'); // Windows path present
|
|
498
|
+
ctx.expect(src).toContain("spawnSync('tar', ['-xf'"); // unix fallback present
|
|
499
|
+
ctx.expect(src).toContain("process.platform === 'win32'"); // platform fork present
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: 'tar can extract a real Windows actions/runner zip (smoke)',
|
|
504
|
+
run: async (ctx) => {
|
|
505
|
+
// Validates the tar approach works against an actual zip with the layout
|
|
506
|
+
// actions/runner ships. We don't pull from GH on every test (slow + flaky);
|
|
507
|
+
// instead build a tiny fixture zip on disk via Node, then extract via tar.
|
|
508
|
+
//
|
|
509
|
+
// tar is the NON-Windows extraction path (the framework-dev one, with
|
|
510
|
+
// OMEGA_RUNNER_FORCE); macOS ships bsdtar, which reads zip, so it is a
|
|
511
|
+
// valid local check there. GNU tar on Linux does NOT read zip. Windows
|
|
512
|
+
// extracts with Expand-Archive and never calls tar — and the `tar` on a
|
|
513
|
+
// Windows box's PATH is often Git for Windows' GNU tar, which reads a
|
|
514
|
+
// `C:\...` path as `host:path` and exits 128 (the exact reason the
|
|
515
|
+
// production code switched). So the case runs on macOS only.
|
|
516
|
+
if (process.platform === 'linux') {
|
|
517
|
+
ctx.skip('GNU tar on Linux does not support zip — the tar path is the non-Windows framework-dev one');
|
|
518
|
+
}
|
|
519
|
+
if (process.platform === 'win32') {
|
|
520
|
+
ctx.skip('Windows extracts with Expand-Archive, never tar — a Git-for-Windows GNU tar on PATH cannot take a C:\\ path');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const fs = require('fs');
|
|
524
|
+
const os = require('os');
|
|
525
|
+
const { spawnSync } = require('child_process');
|
|
526
|
+
|
|
527
|
+
// Skip if tar isn't on PATH for any reason.
|
|
528
|
+
const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['tar']);
|
|
529
|
+
if (which.status !== 0) {
|
|
530
|
+
ctx.skip('tar not found on PATH');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Build a minimal zip via Node — uses the same standard zip container that
|
|
534
|
+
// actions/runner ships. We're testing that tar's zip support works at all.
|
|
535
|
+
// Smallest valid zip = 22-byte EOCD with no entries.
|
|
536
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-tar-test-'));
|
|
537
|
+
const zipPath = path.join(tmp, 'empty.zip');
|
|
538
|
+
// EOCD signature (PK\x05\x06) + 18 zero bytes = empty valid zip
|
|
539
|
+
const eocd = Buffer.from([0x50, 0x4B, 0x05, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
540
|
+
fs.writeFileSync(zipPath, eocd);
|
|
541
|
+
|
|
542
|
+
const r = spawnSync('tar', ['-xf', zipPath, '-C', tmp]);
|
|
543
|
+
// Cleanup before assertion so we don't leak tmp dirs on failure.
|
|
544
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (e) { /* ignore */ }
|
|
545
|
+
|
|
546
|
+
ctx.expect(r.status).toBe(0);
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'install surfaces zero-success failure with non-zero exit code',
|
|
551
|
+
run: (ctx) => {
|
|
552
|
+
// Source-text guard: confirm install reports + sets exitCode when 0/N orgs registered.
|
|
553
|
+
const fs = require('fs');
|
|
554
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
555
|
+
ctx.expect(src).toContain('Install summary');
|
|
556
|
+
ctx.expect(src).toContain('process.exitCode = 1');
|
|
557
|
+
ctx.expect(src).toContain('failedByReason');
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'install is idempotent — calls uninstall first if the runner home exists',
|
|
562
|
+
run: (ctx) => {
|
|
563
|
+
// Source-text guard: re-running install should never leave you in a worse state.
|
|
564
|
+
const fs = require('fs');
|
|
565
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
566
|
+
ctx.expect(src).toContain('Existing runner installation detected');
|
|
567
|
+
ctx.expect(src).toContain('uninstalling first for a clean re-install');
|
|
568
|
+
// ...and the electron-manager era's install counts as an existing one.
|
|
569
|
+
ctx.expect(src).toMatch(/jetpack\.exists\(home\)\s*\|\|\s*listLegacyRunnerHomes\(\{ home \}\)\.length > 0/);
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: 'downloadActionsRunner uses curl + size validation (not wonderful-fetch buffer)',
|
|
574
|
+
run: (ctx) => {
|
|
575
|
+
// Guards against the regression where wonderful-fetch returned an unexpected
|
|
576
|
+
// ~224MB buffer that tar couldn't extract.
|
|
577
|
+
const fs = require('fs');
|
|
578
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
579
|
+
ctx.expect(src).toContain("spawnSync('curl'");
|
|
580
|
+
ctx.expect(src).toContain('1024 * 1024'); // size sanity check
|
|
581
|
+
ctx.expect(src).not.toContain("require('wonderful-fetch')");
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'per-org state reads the runner dir + Startup shortcut out of a fake runner home',
|
|
586
|
+
run: (ctx) => {
|
|
587
|
+
// What `monitor` and `status` both report: is this org registered on disk,
|
|
588
|
+
// does it auto-start at logon, is a listener alive. No Scheduled Tasks.
|
|
589
|
+
const fs = require('fs');
|
|
590
|
+
const os = require('os');
|
|
591
|
+
const jetpack = require('fs-jetpack');
|
|
592
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
593
|
+
|
|
594
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
595
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const before = runner.orgRunnerState('Omega-JS-Stack', { home, startupDir });
|
|
599
|
+
ctx.expect(before.dir).toBe(path.join(home, 'actions-runner-omega-js-stack'));
|
|
600
|
+
ctx.expect(before.installed).toBe(false);
|
|
601
|
+
ctx.expect(before.autoStart).toBe(false);
|
|
602
|
+
ctx.expect(before.state).toBe('NOT_INSTALLED');
|
|
603
|
+
|
|
604
|
+
// Register: the per-org runner dir lands, then its Startup shortcut.
|
|
605
|
+
jetpack.dir(before.dir);
|
|
606
|
+
ctx.expect(runner.orgRunnerState('Omega-JS-Stack', { home, startupDir }).state).toBe('STOPPED');
|
|
607
|
+
|
|
608
|
+
jetpack.write(path.join(startupDir, `${before.name}.cmd`), '@echo off\r\n');
|
|
609
|
+
const after = runner.orgRunnerState('Omega-JS-Stack', { home, startupDir });
|
|
610
|
+
ctx.expect(after.installed).toBe(true);
|
|
611
|
+
ctx.expect(after.autoStart).toBe(true);
|
|
612
|
+
// No Runner.Listener.exe under a temp dir on any platform.
|
|
613
|
+
ctx.expect(after.listeners).toEqual([]);
|
|
614
|
+
ctx.expect(after.state).toBe('STOPPED');
|
|
615
|
+
} finally {
|
|
616
|
+
jetpack.remove(home);
|
|
617
|
+
jetpack.remove(startupDir);
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'monitor lists every registered org with its state (no Scheduled Tasks)',
|
|
623
|
+
run: (ctx) => {
|
|
624
|
+
// The regression this replaces: monitor called listEmRunnerTasks() /
|
|
625
|
+
// taskState(), neither of which exists, so it threw a ReferenceError on
|
|
626
|
+
// any box with at least one registered org.
|
|
627
|
+
const fs = require('fs');
|
|
628
|
+
const os = require('os');
|
|
629
|
+
const jetpack = require('fs-jetpack');
|
|
630
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
631
|
+
|
|
632
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
633
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['Omega-JS-Stack', 'ITW-Creative-Works'] });
|
|
637
|
+
jetpack.dir(path.join(home, 'actions-runner-omega-js-stack'));
|
|
638
|
+
|
|
639
|
+
const states = runner.monitorOrgStates({ home, startupDir, env: {} });
|
|
640
|
+
ctx.expect(states.length).toBe(2);
|
|
641
|
+
ctx.expect(states.map((s) => s.org)).toEqual(['Omega-JS-Stack', 'ITW-Creative-Works']);
|
|
642
|
+
ctx.expect(states[0].state).toBe('STOPPED');
|
|
643
|
+
ctx.expect(states[1].state).toBe('NOT_INSTALLED');
|
|
644
|
+
|
|
645
|
+
// OMEGA_RUNNER_ORGS narrows the roster the same way install does.
|
|
646
|
+
const filtered = runner.monitorOrgStates({ home, startupDir, env: { OMEGA_RUNNER_ORGS: 'omega-js-stack' } });
|
|
647
|
+
ctx.expect(filtered.map((s) => s.org)).toEqual(['Omega-JS-Stack']);
|
|
648
|
+
} finally {
|
|
649
|
+
jetpack.remove(home);
|
|
650
|
+
jetpack.remove(startupDir);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'uninstall sweeps up leftover actions.runner.* services + retries removal',
|
|
656
|
+
run: (ctx) => {
|
|
657
|
+
const fs = require('fs');
|
|
658
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
659
|
+
ctx.expect(src).toContain('uninstallActionsRunnerServices');
|
|
660
|
+
ctx.expect(src).toContain('removeRunnerHomeWithRetry');
|
|
661
|
+
ctx.expect(src).toContain('SERVICE_NAME:\\s*(actions\\.runner'); // pattern for service discovery
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
name: 'uninstall builds its deregistration roster from the runner dirs on disk',
|
|
666
|
+
run: (ctx) => {
|
|
667
|
+
// Off config.json deliberately: a half-finished install leaves per-org
|
|
668
|
+
// directories config.json never recorded, and those are exactly the ones
|
|
669
|
+
// that strand an orphaned runner in the org.
|
|
670
|
+
const fs = require('fs');
|
|
671
|
+
const os = require('os');
|
|
672
|
+
const jetpack = require('fs-jetpack');
|
|
673
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
674
|
+
|
|
675
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
676
|
+
try {
|
|
677
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
|
|
678
|
+
jetpack.dir(path.join(home, 'actions-runner-bar')); // no config.cmd — nothing to remove with
|
|
679
|
+
jetpack.write(path.join(home, '_template', 'config.cmd'), 'rem template'); // never registered against an org
|
|
680
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['gone'] });
|
|
681
|
+
|
|
682
|
+
const roster = runner.listOrgRunnerDirs(home);
|
|
683
|
+
ctx.expect(roster.map((r) => r.org)).toEqual(['foo']);
|
|
684
|
+
ctx.expect(roster[0].dir).toBe(path.join(home, 'actions-runner-foo'));
|
|
685
|
+
ctx.expect(roster[0].configCmd).toBe(path.join(home, 'actions-runner-foo', 'config.cmd'));
|
|
686
|
+
} finally {
|
|
687
|
+
jetpack.remove(home);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
ctx.expect(runner.listOrgRunnerDirs(path.join(home, 'does-not-exist'))).toEqual([]);
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: 'uninstall runs config.cmd remove per org dir, with that dir as cwd',
|
|
695
|
+
run: async (ctx) => {
|
|
696
|
+
const fs = require('fs');
|
|
697
|
+
const os = require('os');
|
|
698
|
+
const jetpack = require('fs-jetpack');
|
|
699
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
700
|
+
|
|
701
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
702
|
+
const origToken = process.env.GH_TOKEN;
|
|
703
|
+
process.env.GH_TOKEN = 'ghp_test_dummy';
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
|
|
707
|
+
jetpack.write(path.join(home, 'actions-runner-baz', 'config.cmd'), 'rem baz');
|
|
708
|
+
|
|
709
|
+
const calls = [];
|
|
710
|
+
const result = await runner.deregisterOrgRunners({
|
|
711
|
+
home,
|
|
712
|
+
getRemoveToken: async (org) => `tok-${org}`,
|
|
713
|
+
exec: (cmd, args, cwd) => { calls.push({ cmd, args, cwd }); return { status: 0 }; },
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
ctx.expect(result.deregistered).toEqual(['baz', 'foo']);
|
|
717
|
+
ctx.expect(result.failed).toEqual([]);
|
|
718
|
+
ctx.expect(calls.length).toBe(2);
|
|
719
|
+
// cmd.exe /c — a .cmd file cannot be spawned directly by CreateProcess.
|
|
720
|
+
ctx.expect(calls[1].cmd).toBe('cmd.exe');
|
|
721
|
+
ctx.expect(calls[1].args).toEqual([
|
|
722
|
+
'/c', path.join(home, 'actions-runner-foo', 'config.cmd'), 'remove', '--token', 'tok-foo',
|
|
723
|
+
]);
|
|
724
|
+
ctx.expect(calls[1].cwd).toBe(path.join(home, 'actions-runner-foo'));
|
|
725
|
+
} finally {
|
|
726
|
+
if (origToken === undefined) delete process.env.GH_TOKEN;
|
|
727
|
+
else process.env.GH_TOKEN = origToken;
|
|
728
|
+
jetpack.remove(home);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: 'a failed config.cmd remove keeps that org for a later uninstall to retry',
|
|
734
|
+
run: async (ctx) => {
|
|
735
|
+
const fs = require('fs');
|
|
736
|
+
const os = require('os');
|
|
737
|
+
const jetpack = require('fs-jetpack');
|
|
738
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
739
|
+
|
|
740
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
741
|
+
const origToken = process.env.GH_TOKEN;
|
|
742
|
+
process.env.GH_TOKEN = 'ghp_test_dummy';
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
|
|
746
|
+
jetpack.write(path.join(home, 'actions-runner-baz', 'config.cmd'), 'rem baz');
|
|
747
|
+
|
|
748
|
+
const result = await runner.deregisterOrgRunners({
|
|
749
|
+
home,
|
|
750
|
+
getRemoveToken: async (org) => `tok-${org}`,
|
|
751
|
+
// baz removes cleanly; foo exits 1 — it is STILL registered on GitHub.
|
|
752
|
+
exec: (cmd, args, cwd) => ({ status: cwd.endsWith('actions-runner-foo') ? 1 : 0 }),
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
ctx.expect(result.deregistered).toEqual(['baz']);
|
|
756
|
+
ctx.expect(result.failed.map((f) => f.org)).toEqual(['foo']);
|
|
757
|
+
// The directory holding the registration has to survive, or the retry
|
|
758
|
+
// has no config.cmd to remove with.
|
|
759
|
+
ctx.expect(result.failed[0].dir).toBe(path.join(home, 'actions-runner-foo'));
|
|
760
|
+
} finally {
|
|
761
|
+
if (origToken === undefined) delete process.env.GH_TOKEN;
|
|
762
|
+
else process.env.GH_TOKEN = origToken;
|
|
763
|
+
jetpack.remove(home);
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: 'without GH_TOKEN, deregistration is skipped loudly rather than half-run',
|
|
769
|
+
run: async (ctx) => {
|
|
770
|
+
const fs = require('fs');
|
|
771
|
+
const os = require('os');
|
|
772
|
+
const jetpack = require('fs-jetpack');
|
|
773
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
774
|
+
|
|
775
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
776
|
+
const origToken = process.env.GH_TOKEN;
|
|
777
|
+
delete process.env.GH_TOKEN;
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
|
|
781
|
+
|
|
782
|
+
let called = false;
|
|
783
|
+
const result = await runner.deregisterOrgRunners({
|
|
784
|
+
home,
|
|
785
|
+
exec: () => { called = true; return { status: 0 }; },
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
ctx.expect(result.deregistered).toEqual([]);
|
|
789
|
+
ctx.expect(called).toBe(false);
|
|
790
|
+
// Not deregistered means the directory stays, so a later run can retry.
|
|
791
|
+
ctx.expect(result.failed.map((f) => f.org)).toEqual(['foo']);
|
|
792
|
+
} finally {
|
|
793
|
+
if (origToken !== undefined) process.env.GH_TOKEN = origToken;
|
|
794
|
+
jetpack.remove(home);
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: 'a test run never kills a process whose path it cannot read',
|
|
800
|
+
run: (ctx) => {
|
|
801
|
+
// A Runner.Listener/Worker whose ExecutablePath the query cannot read
|
|
802
|
+
// belongs to ANOTHER account, and `$ep -eq ''` matches it against
|
|
803
|
+
// WHATEVER home is passed — so `uninstall` from a test would `taskkill /F`
|
|
804
|
+
// the box's real runners with two scratch homes and a scratch Startup dir.
|
|
805
|
+
// PowerShell cannot run here, so the filter text is the assertion.
|
|
806
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
807
|
+
|
|
808
|
+
const real = runner.listenerFilterScript('c:\\users\\me\\appdata\\local\\omega-runner', { includeUnreadable: true });
|
|
809
|
+
const test = runner.listenerFilterScript('c:\\users\\me\\appdata\\local\\omega-runner', { includeUnreadable: false });
|
|
810
|
+
|
|
811
|
+
ctx.expect(real).toContain("$ep -eq ''"); // a real box clears every handle in the home
|
|
812
|
+
ctx.expect(test.includes("$ep -eq ''")).toBe(false); // a test owns no stranger's process
|
|
813
|
+
// Both still scope to the home, and both still catch the cmd.exe wrappers.
|
|
814
|
+
ctx.expect(test).toContain('$ep.StartsWith($home_lc)');
|
|
815
|
+
ctx.expect(test).toContain('run.cmd');
|
|
816
|
+
ctx.expect(real).toContain('$ep.StartsWith($home_lc)');
|
|
817
|
+
|
|
818
|
+
// …and the two paths that kill are wired to it. Source-level, because
|
|
819
|
+
// neither can be observed off Windows: both return before spawning.
|
|
820
|
+
const source = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
821
|
+
ctx.expect(source).toContain('listenerFilterScript(homeLower, { includeUnreadable: !isTestRun() })');
|
|
822
|
+
ctx.expect(source).toContain('if (isTestRun()) continue;'); // the same rule, enumerating for `stop`
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
name: 'a test run refuses to deregister without an injected seam',
|
|
827
|
+
run: async (ctx) => {
|
|
828
|
+
// The default path mints a REAL removal token with GH_TOKEN and runs each
|
|
829
|
+
// org dir's own config.cmd — the box's registrations, gone. Neither is
|
|
830
|
+
// scoped by a home, so the seam is the only thing between a test and
|
|
831
|
+
// GitHub, and its absence has to be a refusal rather than a request.
|
|
832
|
+
const os = require('os');
|
|
833
|
+
const jetpack = require('fs-jetpack');
|
|
834
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
835
|
+
|
|
836
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
837
|
+
const marker = path.join(home, 'config-cmd-ran.txt');
|
|
838
|
+
const origToken = process.env.GH_TOKEN;
|
|
839
|
+
process.env.GH_TOKEN = 'ghp_test_dummy'; // the box's shape: a token IS set
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
// A config.cmd that leaves proof if anything ever executes it.
|
|
843
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), `@echo off\r\necho ran > "${marker}"\r\n`);
|
|
844
|
+
|
|
845
|
+
let threw;
|
|
846
|
+
try {
|
|
847
|
+
await runner.deregisterOrgRunners({ home }); // no exec, no getRemoveToken
|
|
848
|
+
} catch (e) { threw = e; }
|
|
849
|
+
|
|
850
|
+
ctx.expect(threw).toBeDefined();
|
|
851
|
+
ctx.expect(threw.message).toContain('exec');
|
|
852
|
+
ctx.expect(threw.message).toContain('getRemoveToken');
|
|
853
|
+
ctx.expect(threw.message).toContain('test run');
|
|
854
|
+
ctx.expect(jetpack.exists(marker)).toBe(false); // config.cmd never ran
|
|
855
|
+
|
|
856
|
+
// `exec` alone does not disarm it while a token is set: the default
|
|
857
|
+
// `getRemoveToken` would still mint a real removal token.
|
|
858
|
+
let threwExecOnly;
|
|
859
|
+
try {
|
|
860
|
+
await runner.deregisterOrgRunners({ home, exec: () => ({ status: 0 }) });
|
|
861
|
+
} catch (e) { threwExecOnly = e; }
|
|
862
|
+
ctx.expect(threwExecOnly).toBeDefined();
|
|
863
|
+
ctx.expect(threwExecOnly.message).toContain('getRemoveToken');
|
|
864
|
+
ctx.expect(jetpack.exists(marker)).toBe(false);
|
|
865
|
+
|
|
866
|
+
// An empty roster is not a refusal — there is nothing to deregister.
|
|
867
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
868
|
+
ctx.expect(await runner.deregisterOrgRunners({ home: empty })).toEqual({ deregistered: [], failed: [] });
|
|
869
|
+
jetpack.remove(empty);
|
|
870
|
+
} finally {
|
|
871
|
+
if (origToken === undefined) delete process.env.GH_TOKEN;
|
|
872
|
+
else process.env.GH_TOKEN = origToken;
|
|
873
|
+
jetpack.remove(home);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
name: 'a listener in session 0 is a warning state, not a healthy RUNNING',
|
|
879
|
+
run: (ctx) => {
|
|
880
|
+
// Session 0 has no desktop and its own (empty) certificate store, so a
|
|
881
|
+
// runner there picks up jobs and then fails every one of them.
|
|
882
|
+
const fs = require('fs');
|
|
883
|
+
const os = require('os');
|
|
884
|
+
const jetpack = require('fs-jetpack');
|
|
885
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
886
|
+
|
|
887
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
888
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
jetpack.dir(path.join(home, 'actions-runner-foo'));
|
|
892
|
+
|
|
893
|
+
const healthy = runner.orgRunnerState('foo', {
|
|
894
|
+
home, startupDir,
|
|
895
|
+
listListeners: () => [{ pid: 11, sessionId: 1, execPath: 'C:\\x\\Runner.Listener.exe' }],
|
|
896
|
+
});
|
|
897
|
+
ctx.expect(healthy.state).toBe('RUNNING');
|
|
898
|
+
ctx.expect(healthy.sessionZero).toBe(false);
|
|
899
|
+
|
|
900
|
+
const blind = runner.orgRunnerState('foo', {
|
|
901
|
+
home, startupDir,
|
|
902
|
+
listListeners: () => [{ pid: 12, sessionId: 0, execPath: null }],
|
|
903
|
+
});
|
|
904
|
+
ctx.expect(blind.state).toBe('SESSION_0');
|
|
905
|
+
ctx.expect(blind.sessionZero).toBe(true);
|
|
906
|
+
|
|
907
|
+
const lines = runner.orgRunnerStateLines(blind);
|
|
908
|
+
ctx.expect(lines[0]).toContain('SESSION_0');
|
|
909
|
+
ctx.expect(lines[0]).toContain('⚠');
|
|
910
|
+
ctx.expect(lines[1]).toContain('PID=12');
|
|
911
|
+
ctx.expect(lines[1]).toContain('session=0');
|
|
912
|
+
ctx.expect(lines[1]).toContain('cannot see CurrentUser\\My cert store');
|
|
913
|
+
} finally {
|
|
914
|
+
jetpack.remove(home);
|
|
915
|
+
jetpack.remove(startupDir);
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: 'status and monitor render the per-org block from ONE derivation',
|
|
921
|
+
run: (ctx) => {
|
|
922
|
+
// The drift this closes: status scanned processes itself while monitor
|
|
923
|
+
// asked a pair of Scheduled-Task helpers that did not exist.
|
|
924
|
+
const fs = require('fs');
|
|
925
|
+
const os = require('os');
|
|
926
|
+
const jetpack = require('fs-jetpack');
|
|
927
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
928
|
+
|
|
929
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
930
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
931
|
+
|
|
932
|
+
try {
|
|
933
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['foo', 'bar'] });
|
|
934
|
+
jetpack.dir(path.join(home, 'actions-runner-foo'));
|
|
935
|
+
jetpack.write(path.join(startupDir, `${runner.orgRunnerState('foo', { home, startupDir }).name}.cmd`), '@echo off\r\n');
|
|
936
|
+
|
|
937
|
+
const report = runner.orgRunnerReport({
|
|
938
|
+
home, startupDir, env: {},
|
|
939
|
+
listListeners: (dir) => (dir.endsWith('actions-runner-foo')
|
|
940
|
+
? [{ pid: 99, sessionId: 1, execPath: 'C:\\x\\Runner.Listener.exe' }]
|
|
941
|
+
: []),
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
ctx.expect(report.map((r) => r.state.org)).toEqual(['foo', 'bar']);
|
|
945
|
+
ctx.expect(report[0].state.state).toBe('RUNNING');
|
|
946
|
+
ctx.expect(report[0].lines[0]).toContain('auto-starts at logon');
|
|
947
|
+
ctx.expect(report[0].lines[1]).toContain('PID=99');
|
|
948
|
+
// bar is registered in config.json but nothing was ever laid down for it.
|
|
949
|
+
ctx.expect(report[1].state.state).toBe('NOT_INSTALLED');
|
|
950
|
+
ctx.expect(report[1].lines.length).toBe(1);
|
|
951
|
+
ctx.expect(report[1].lines[0]).toContain('no Startup shortcut');
|
|
952
|
+
} finally {
|
|
953
|
+
jetpack.remove(home);
|
|
954
|
+
jetpack.remove(startupDir);
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: 'RUNNER_HOME resolves per platform (%LOCALAPPDATA% on Windows, <cwd>/.gh-runners elsewhere)',
|
|
960
|
+
run: (ctx) => {
|
|
961
|
+
const os = require('os');
|
|
962
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
963
|
+
|
|
964
|
+
// Windows: per-user, under %LOCALAPPDATA% — the whole reason install
|
|
965
|
+
// needs no admin. Falls back to the profile path when the var is unset.
|
|
966
|
+
ctx.expect(runner.defaultRunnerHome('win32', { LOCALAPPDATA: path.join('C:', 'Users', 'ian', 'AppData', 'Local') }))
|
|
967
|
+
.toBe(path.join('C:', 'Users', 'ian', 'AppData', 'Local', 'omega-runner'));
|
|
968
|
+
ctx.expect(runner.defaultRunnerHome('win32', {}))
|
|
969
|
+
.toBe(path.join(os.homedir(), 'AppData', 'Local', 'omega-runner'));
|
|
970
|
+
|
|
971
|
+
// Everywhere else (framework dev only — the runner itself is Windows-only).
|
|
972
|
+
ctx.expect(runner.defaultRunnerHome('darwin', {})).toBe(path.join(process.cwd(), '.gh-runners'));
|
|
973
|
+
ctx.expect(runner.defaultRunnerHome('linux', {})).toBe(path.join(process.cwd(), '.gh-runners'));
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: 'install honors OMEGA_RUNNER_ORGS filter from env',
|
|
978
|
+
run: (ctx) => {
|
|
979
|
+
const fs = require('fs');
|
|
980
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
981
|
+
ctx.expect(src).toContain('OMEGA_RUNNER_ORGS');
|
|
982
|
+
ctx.expect(src).toContain('filter');
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: 'each org gets its own actions-runner-<org>/ directory (multi-org architecture)',
|
|
987
|
+
run: (ctx) => {
|
|
988
|
+
const fs = require('fs');
|
|
989
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
990
|
+
ctx.expect(src).toContain('actions-runner-${org.toLowerCase()}');
|
|
991
|
+
ctx.expect(src).toContain('templateDir');
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
name: 'config.cmd spawn uses inherited stdio (so --runasservice actually creates the service)',
|
|
996
|
+
run: (ctx) => {
|
|
997
|
+
const fs = require('fs');
|
|
998
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
999
|
+
// Inherit, NOT pipe — actions/runner's --runasservice silently skips service
|
|
1000
|
+
// install when stdout/stderr are piped (it can't see a console).
|
|
1001
|
+
ctx.expect(src).toContain("stdio: 'inherit'");
|
|
1002
|
+
ctx.expect(src).toContain('null (killed)'); // surfaces kill status meaningfully
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'config.cmd invoked via cmd.exe /c (not shell:true) to avoid Node DEP0190',
|
|
1007
|
+
run: (ctx) => {
|
|
1008
|
+
const fs = require('fs');
|
|
1009
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
1010
|
+
ctx.expect(src).toContain("spawnSync('cmd.exe', ['/c', configCmd");
|
|
1011
|
+
// shell:true is gone from registerOrg's spawnSync (other places may still use it)
|
|
1012
|
+
ctx.expect(src).not.toMatch(/spawnSync\(configCmd[^)]*shell:\s*true/s);
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: 'the electron-manager era homes are found from the environment, never the current home',
|
|
1017
|
+
run: (ctx) => {
|
|
1018
|
+
// An upgraded box still runs `em runner`'s install out of
|
|
1019
|
+
// %LOCALAPPDATA%\em-runner (C:\actions-runners before that). Those are
|
|
1020
|
+
// what uninstall tears down and status names — unless one of them IS
|
|
1021
|
+
// the current home, which is the omega install and not a leftover.
|
|
1022
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1023
|
+
const sep = path.win32.sep;
|
|
1024
|
+
const env = { LOCALAPPDATA: ['C:', 'Users', 'ian', 'AppData', 'Local'].join(sep), SystemDrive: 'C:' };
|
|
1025
|
+
const emHome = [env.LOCALAPPDATA, 'em-runner'].join(sep);
|
|
1026
|
+
const cHome = ['C:', 'actions-runners'].join(sep);
|
|
1027
|
+
const home = [env.LOCALAPPDATA, 'omega-runner'].join(sep);
|
|
1028
|
+
|
|
1029
|
+
ctx.expect(runner.LEGACY_RUNNER_PREFIX).toBe('em-runner-');
|
|
1030
|
+
|
|
1031
|
+
// Both on disk: both are named, in that order.
|
|
1032
|
+
ctx.expect(runner.listLegacyRunnerHomes({ platform: 'win32', env, home, exists: () => true })).toEqual([emHome, cHome]);
|
|
1033
|
+
|
|
1034
|
+
// Only what is actually on disk.
|
|
1035
|
+
const onDisk = new Set([cHome.toLowerCase()]);
|
|
1036
|
+
ctx.expect(runner.listLegacyRunnerHomes({ platform: 'win32', env, home, exists: (p) => onDisk.has(p.toLowerCase()) })).toEqual([cHome]);
|
|
1037
|
+
ctx.expect(runner.listLegacyRunnerHomes({ platform: 'win32', env, home, exists: () => false })).toEqual([]);
|
|
1038
|
+
|
|
1039
|
+
// OMEGA_RUNNER_HOME pointed AT the legacy path: that is the install, not a leftover.
|
|
1040
|
+
ctx.expect(runner.listLegacyRunnerHomes({ platform: 'win32', env, home: emHome.toUpperCase(), exists: () => true })).toEqual([cHome]);
|
|
1041
|
+
|
|
1042
|
+
// Nothing to find off Windows.
|
|
1043
|
+
ctx.expect(runner.listLegacyRunnerHomes({ platform: 'darwin', env, home, exists: () => true })).toEqual([]);
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
name: 'em-runner-* Startup shortcuts are listed apart from omega-runner-* ones',
|
|
1048
|
+
run: (ctx) => {
|
|
1049
|
+
// `start` launches omega shortcuts only; `status` names the legacy ones
|
|
1050
|
+
// and `uninstall` removes them. The two rosters never mix.
|
|
1051
|
+
const fs = require('fs');
|
|
1052
|
+
const os = require('os');
|
|
1053
|
+
const jetpack = require('fs-jetpack');
|
|
1054
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1055
|
+
|
|
1056
|
+
const startupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-startup-'));
|
|
1057
|
+
try {
|
|
1058
|
+
jetpack.write(path.join(startupDir, 'omega-runner-host-acme.cmd'), '@echo off');
|
|
1059
|
+
jetpack.write(path.join(startupDir, 'omega-runner-host-zed.cmd'), '@echo off');
|
|
1060
|
+
jetpack.write(path.join(startupDir, 'em-runner-host-acme.cmd'), '@echo off');
|
|
1061
|
+
jetpack.write(path.join(startupDir, 'EM-RUNNER-host-old.CMD'), '@echo off');
|
|
1062
|
+
jetpack.write(path.join(startupDir, 'em-runner-.cmd'), '@echo off'); // prefix alone names no runner
|
|
1063
|
+
jetpack.write(path.join(startupDir, 'em-runner-notes.txt'), 'not a shortcut');
|
|
1064
|
+
jetpack.write(path.join(startupDir, 'Something Else.lnk'), '');
|
|
1065
|
+
|
|
1066
|
+
ctx.expect(runner.listRunnerStartupShortcuts(startupDir)).toEqual(['omega-runner-host-acme', 'omega-runner-host-zed']);
|
|
1067
|
+
ctx.expect(runner.listLegacyRunnerStartupShortcuts(startupDir)).toEqual(['EM-RUNNER-host-old', 'em-runner-host-acme']);
|
|
1068
|
+
|
|
1069
|
+
ctx.expect(runner.listRunnerStartupShortcuts(path.join(startupDir, 'missing'))).toEqual([]);
|
|
1070
|
+
ctx.expect(runner.listLegacyRunnerStartupShortcuts(null)).toEqual([]);
|
|
1071
|
+
} finally {
|
|
1072
|
+
jetpack.remove(startupDir);
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
name: 'the box owns its signing config: <runner home>/.env fills what the shell and CI did not set',
|
|
1078
|
+
run: (ctx) => {
|
|
1079
|
+
// The PIN, the cert, signtool, the token and the orgs belong to the
|
|
1080
|
+
// MACHINE, never to a brand's .env — and a value the shell or a CI job
|
|
1081
|
+
// already carries wins over the file. An empty delivered value counts
|
|
1082
|
+
// as absent (sanitize-signing-env's rule), and an empty line in the
|
|
1083
|
+
// file sets nothing.
|
|
1084
|
+
const fs = require('fs');
|
|
1085
|
+
const os = require('os');
|
|
1086
|
+
const jetpack = require('fs-jetpack');
|
|
1087
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1088
|
+
|
|
1089
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1090
|
+
try {
|
|
1091
|
+
// No file yet: nothing happens, and the caller is told.
|
|
1092
|
+
const missing = runnerEnv.loadRunnerEnv({ home, env: {} });
|
|
1093
|
+
ctx.expect(missing.present).toBe(false);
|
|
1094
|
+
ctx.expect(missing.file).toBe(path.join(home, '.env'));
|
|
1095
|
+
|
|
1096
|
+
// install lays the template down ONCE.
|
|
1097
|
+
ctx.expect(runnerEnv.ensureRunnerEnvFile(home)).toBe(true);
|
|
1098
|
+
ctx.expect(runnerEnv.ensureRunnerEnvFile(home)).toBe(false);
|
|
1099
|
+
const template = jetpack.read(path.join(home, '.env'));
|
|
1100
|
+
for (const key of runnerEnv.RUNNER_ENV_KEYS) ctx.expect(template).toContain(`${key}=""`);
|
|
1101
|
+
|
|
1102
|
+
// The template's empty values set nothing.
|
|
1103
|
+
const blank = {};
|
|
1104
|
+
ctx.expect(runnerEnv.loadRunnerEnv({ home, env: blank }).applied).toEqual([]);
|
|
1105
|
+
ctx.expect(Object.keys(blank)).toEqual([]);
|
|
1106
|
+
|
|
1107
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1108
|
+
'GH_TOKEN=ghp_from_the_box',
|
|
1109
|
+
`WIN_EV_TOKEN_PATH=${'a'.repeat(40)}`,
|
|
1110
|
+
'WIN_CSC_KEY_PASSWORD="pin with spaces"',
|
|
1111
|
+
'SIGNTOOL_PATH=',
|
|
1112
|
+
'# WIN_TIMESTAMP_URL=http://elsewhere',
|
|
1113
|
+
].join('\n'));
|
|
1114
|
+
|
|
1115
|
+
const env = { WIN_EV_TOKEN_PATH: 'b'.repeat(40), GH_TOKEN: ' ' };
|
|
1116
|
+
const result = runnerEnv.loadRunnerEnv({ home, env });
|
|
1117
|
+
ctx.expect(result.present).toBe(true);
|
|
1118
|
+
ctx.expect(result.applied).toEqual(['GH_TOKEN', 'WIN_CSC_KEY_PASSWORD']); // blank shell value = absent
|
|
1119
|
+
ctx.expect(result.skipped).toEqual(['WIN_EV_TOKEN_PATH']); // shell wins
|
|
1120
|
+
ctx.expect(env.GH_TOKEN).toBe('ghp_from_the_box');
|
|
1121
|
+
ctx.expect(env.WIN_EV_TOKEN_PATH).toBe('b'.repeat(40));
|
|
1122
|
+
ctx.expect(env.WIN_CSC_KEY_PASSWORD).toBe('pin with spaces');
|
|
1123
|
+
ctx.expect('SIGNTOOL_PATH' in env).toBe(false);
|
|
1124
|
+
ctx.expect('WIN_TIMESTAMP_URL' in env).toBe(false);
|
|
1125
|
+
|
|
1126
|
+
// The report names keys, never values.
|
|
1127
|
+
const report = runnerEnv.runnerEnvReport(env);
|
|
1128
|
+
ctx.expect(report.find((r) => r.key === 'GH_TOKEN').set).toBe(true);
|
|
1129
|
+
ctx.expect(report.find((r) => r.key === 'SIGNTOOL_PATH').set).toBe(false);
|
|
1130
|
+
ctx.expect(JSON.stringify(report)).not.toContain('ghp_from_the_box');
|
|
1131
|
+
} finally {
|
|
1132
|
+
jetpack.remove(home);
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
name: 'uninstall removes the runner home but keeps the box .env and its logs',
|
|
1138
|
+
run: async (ctx) => {
|
|
1139
|
+
// Configuration is not install state: a re-install never asks for the
|
|
1140
|
+
// PIN or the token again. Neither is the box's own trail — runner.log
|
|
1141
|
+
// is what the uninstall itself is writing while it runs, so removing it
|
|
1142
|
+
// would delete the record of the run that is happening. With neither on
|
|
1143
|
+
// disk the home goes whole.
|
|
1144
|
+
const fs = require('fs');
|
|
1145
|
+
const os = require('os');
|
|
1146
|
+
const jetpack = require('fs-jetpack');
|
|
1147
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1148
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1149
|
+
|
|
1150
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1151
|
+
try {
|
|
1152
|
+
jetpack.write(path.join(home, '.env'), 'WIN_CSC_KEY_PASSWORD=keep-me');
|
|
1153
|
+
jetpack.write(runnerEnv.runnerLogFile(home), '# omega log\n');
|
|
1154
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['foo'] });
|
|
1155
|
+
jetpack.write(path.join(home, 'actions-runner-foo', 'config.cmd'), 'rem foo');
|
|
1156
|
+
jetpack.write(path.join(home, '_template', 'config.cmd'), 'rem template');
|
|
1157
|
+
|
|
1158
|
+
await runner.removeRunnerHomeWithRetry(new Set(), home);
|
|
1159
|
+
|
|
1160
|
+
ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'logs']);
|
|
1161
|
+
ctx.expect(jetpack.read(path.join(home, '.env'))).toBe('WIN_CSC_KEY_PASSWORD=keep-me');
|
|
1162
|
+
ctx.expect(jetpack.read(runnerEnv.runnerLogFile(home))).toBe('# omega log\n');
|
|
1163
|
+
|
|
1164
|
+
// A kept org dir (its deregistration failed) survives beside them.
|
|
1165
|
+
jetpack.write(path.join(home, 'actions-runner-bar', 'config.cmd'), 'rem bar');
|
|
1166
|
+
jetpack.write(path.join(home, 'actions-runner-baz', 'config.cmd'), 'rem baz');
|
|
1167
|
+
await runner.removeRunnerHomeWithRetry(new Set([path.join(home, 'actions-runner-bar')]), home);
|
|
1168
|
+
ctx.expect(jetpack.list(home).sort()).toEqual(['.env', 'actions-runner-bar', 'logs']);
|
|
1169
|
+
|
|
1170
|
+
// Neither the .env nor the logs: the whole home goes.
|
|
1171
|
+
jetpack.remove(path.join(home, '.env'));
|
|
1172
|
+
jetpack.remove(path.join(home, 'logs'));
|
|
1173
|
+
await runner.removeRunnerHomeWithRetry(new Set(), home);
|
|
1174
|
+
ctx.expect(jetpack.exists(home)).toBe(false);
|
|
1175
|
+
|
|
1176
|
+
// Logs alone are enough to keep the home standing.
|
|
1177
|
+
jetpack.write(runnerEnv.runnerLogFile(home), '# omega log\n');
|
|
1178
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['foo'] });
|
|
1179
|
+
await runner.removeRunnerHomeWithRetry(new Set(), home);
|
|
1180
|
+
ctx.expect(jetpack.list(home)).toEqual(['logs']);
|
|
1181
|
+
jetpack.remove(home);
|
|
1182
|
+
|
|
1183
|
+
// And a home that is not there is not an error.
|
|
1184
|
+
await runner.removeRunnerHomeWithRetry(new Set(), home);
|
|
1185
|
+
} finally {
|
|
1186
|
+
jetpack.remove(home);
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
name: 'a missing required key is asked for in a terminal, saved to the box .env, and refused without one',
|
|
1192
|
+
run: async (ctx) => {
|
|
1193
|
+
// The runner never boots half configured: interactive, the missing
|
|
1194
|
+
// required keys are prompted (secrets masked) and written back; off a
|
|
1195
|
+
// TTY, or when an answer is empty, the command refuses naming the file
|
|
1196
|
+
// and the keys. Optional keys are never asked for.
|
|
1197
|
+
const fs = require('fs');
|
|
1198
|
+
const os = require('os');
|
|
1199
|
+
const jetpack = require('fs-jetpack');
|
|
1200
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1201
|
+
|
|
1202
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1203
|
+
try {
|
|
1204
|
+
// Off a TTY: refuse, and say which keys and which file.
|
|
1205
|
+
let threw;
|
|
1206
|
+
try {
|
|
1207
|
+
await runnerEnv.ensureRunnerConfig({ home, env: {}, interactive: false, prompt: {} });
|
|
1208
|
+
} catch (e) { threw = e; }
|
|
1209
|
+
ctx.expect(threw).toBeDefined();
|
|
1210
|
+
ctx.expect(threw.message).toContain('GH_TOKEN, WIN_EV_TOKEN_PATH, WIN_CSC_KEY_PASSWORD, SIGNTOOL_PATH');
|
|
1211
|
+
ctx.expect(threw.message).toContain(path.join(home, '.env'));
|
|
1212
|
+
ctx.expect(threw.message).toContain('no terminal');
|
|
1213
|
+
ctx.expect(jetpack.exists(path.join(home, '.env'))).toBe('file'); // the template was laid down anyway
|
|
1214
|
+
|
|
1215
|
+
// Only the signing scope: GH_TOKEN is not that caller's business.
|
|
1216
|
+
threw = undefined;
|
|
1217
|
+
try {
|
|
1218
|
+
await runnerEnv.ensureRunnerConfig({ home, env: { WIN_EV_TOKEN_PATH: 'f'.repeat(40) }, scopes: ['signing'], interactive: false, prompt: {} });
|
|
1219
|
+
} catch (e) { threw = e; }
|
|
1220
|
+
ctx.expect(threw.message).toContain('WIN_CSC_KEY_PASSWORD, SIGNTOOL_PATH are not set');
|
|
1221
|
+
ctx.expect(threw.message).not.toContain('GH_TOKEN');
|
|
1222
|
+
|
|
1223
|
+
// Interactive: each missing required key is asked once, secrets through
|
|
1224
|
+
// the masked prompt, and the answers land in the file AND the env.
|
|
1225
|
+
const asked = [];
|
|
1226
|
+
const fakePrompt = {
|
|
1227
|
+
input: async ({ message, default: suggestion }) => { asked.push(['input', message, suggestion]); return message.startsWith('SIGNTOOL_PATH') ? (suggestion || 'C:/sdk/signtool.exe') : 'a'.repeat(40); },
|
|
1228
|
+
password: async ({ message }) => { asked.push(['password', message]); return message.startsWith('GH_TOKEN') ? 'ghp_answered' : '1234'; },
|
|
1229
|
+
};
|
|
1230
|
+
const env = { OMEGA_RUNNER_ORGS: 'acme' };
|
|
1231
|
+
const result = await runnerEnv.ensureRunnerConfig({ home, env, interactive: true, prompt: fakePrompt });
|
|
1232
|
+
ctx.expect(result.asked).toEqual(['GH_TOKEN', 'WIN_EV_TOKEN_PATH', 'WIN_CSC_KEY_PASSWORD', 'SIGNTOOL_PATH']);
|
|
1233
|
+
ctx.expect(asked.map((a) => a[0])).toEqual(['password', 'input', 'password', 'input']);
|
|
1234
|
+
ctx.expect(asked.some((a) => a[1].startsWith('OMEGA_RUNNER_ORGS') || a[1].startsWith('WIN_TIMESTAMP_URL'))).toBe(false);
|
|
1235
|
+
ctx.expect(env.GH_TOKEN).toBe('ghp_answered');
|
|
1236
|
+
ctx.expect(env.WIN_CSC_KEY_PASSWORD).toBe('1234');
|
|
1237
|
+
|
|
1238
|
+
const written = jetpack.read(path.join(home, '.env'));
|
|
1239
|
+
ctx.expect(written).toContain('GH_TOKEN="ghp_answered"'); // every value double-quoted
|
|
1240
|
+
ctx.expect(written).toContain('WIN_CSC_KEY_PASSWORD="1234"');
|
|
1241
|
+
ctx.expect(written).toContain(`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`);
|
|
1242
|
+
ctx.expect(written).toContain('# The token PIN'); // comments survive
|
|
1243
|
+
ctx.expect(written).toContain('# WIN_TIMESTAMP_URL='); // optional stays commented
|
|
1244
|
+
ctx.expect((written.match(/^GH_TOKEN=/gm) || []).length).toBe(1); // replaced in place, not appended
|
|
1245
|
+
|
|
1246
|
+
// Satisfied now: nothing is asked again, from a fresh env read off the file.
|
|
1247
|
+
const again = await runnerEnv.ensureRunnerConfig({ home, env: {}, interactive: false, prompt: {} });
|
|
1248
|
+
ctx.expect(again.asked).toEqual([]);
|
|
1249
|
+
|
|
1250
|
+
// An empty answer to a required key is still a refusal.
|
|
1251
|
+
jetpack.write(path.join(home, '.env'), 'GH_TOKEN=x\nWIN_EV_TOKEN_PATH=\nWIN_CSC_KEY_PASSWORD=\nSIGNTOOL_PATH=s\n');
|
|
1252
|
+
threw = undefined;
|
|
1253
|
+
try {
|
|
1254
|
+
await runnerEnv.ensureRunnerConfig({ home, env: {}, interactive: true, prompt: { input: async () => ' ', password: async () => '' } });
|
|
1255
|
+
} catch (e) { threw = e; }
|
|
1256
|
+
ctx.expect(threw.message).toContain('WIN_EV_TOKEN_PATH, WIN_CSC_KEY_PASSWORD are not set');
|
|
1257
|
+
ctx.expect(threw.message).not.toContain('no terminal');
|
|
1258
|
+
|
|
1259
|
+
// Every value is written double-quoted, so a PIN with spaces or a `#`
|
|
1260
|
+
// reads back whole and the file obeys the one .env quoting rule.
|
|
1261
|
+
runnerEnv.writeRunnerEnvValues(home, { WIN_CSC_KEY_PASSWORD: 'p in spaces #1', NEW_KEY: 'appended' });
|
|
1262
|
+
const quoted = jetpack.read(path.join(home, '.env'));
|
|
1263
|
+
ctx.expect(quoted).toContain('WIN_CSC_KEY_PASSWORD="p in spaces #1"');
|
|
1264
|
+
ctx.expect(quoted).toContain('NEW_KEY="appended"');
|
|
1265
|
+
const back = {};
|
|
1266
|
+
runnerEnv.loadRunnerEnv({ home, env: back });
|
|
1267
|
+
ctx.expect(back.WIN_CSC_KEY_PASSWORD).toBe('p in spaces #1');
|
|
1268
|
+
ctx.expect(back.NEW_KEY).toBe('appended');
|
|
1269
|
+
|
|
1270
|
+
// A value carrying a double quote cannot be written at all — dotenv
|
|
1271
|
+
// has no escape for it — so it is refused naming the key and the char.
|
|
1272
|
+
let refused;
|
|
1273
|
+
try {
|
|
1274
|
+
runnerEnv.writeRunnerEnvValues(home, { WIN_CSC_KEY_PASSWORD: 'p in "quotes"' });
|
|
1275
|
+
} catch (e) { refused = e; }
|
|
1276
|
+
ctx.expect(refused).toBeDefined();
|
|
1277
|
+
ctx.expect(refused.message).toContain('WIN_CSC_KEY_PASSWORD');
|
|
1278
|
+
ctx.expect(refused.message).toContain('"');
|
|
1279
|
+
ctx.expect(jetpack.read(path.join(home, '.env'))).toContain('WIN_CSC_KEY_PASSWORD="p in spaces #1"');
|
|
1280
|
+
} finally {
|
|
1281
|
+
jetpack.remove(home);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
name: 'install and config are ONE walk: every key is asked with the current value as its default',
|
|
1287
|
+
run: async (ctx) => {
|
|
1288
|
+
// Ian's box, 2026-09-04: a shell that carried an exported
|
|
1289
|
+
// OMEGA_RUNNER_ORGS made install skip the orgs question silently, and a
|
|
1290
|
+
// brand's GH_TOKEN could configure the box. So EVERY key is asked, every
|
|
1291
|
+
// time, and the default is the file's value — not the shell's.
|
|
1292
|
+
const fs = require('fs');
|
|
1293
|
+
const os = require('os');
|
|
1294
|
+
const jetpack = require('fs-jetpack');
|
|
1295
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1296
|
+
|
|
1297
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1298
|
+
try {
|
|
1299
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1300
|
+
'GH_TOKEN="ghp_saved"',
|
|
1301
|
+
'OMEGA_RUNNER_ORGS="acme"',
|
|
1302
|
+
`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`,
|
|
1303
|
+
'WIN_CSC_KEY_PASSWORD="old-pin"',
|
|
1304
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
1305
|
+
'WIN_TIMESTAMP_URL=""',
|
|
1306
|
+
].join('\n'));
|
|
1307
|
+
|
|
1308
|
+
const asked = [];
|
|
1309
|
+
const prompt = {
|
|
1310
|
+
input: async ({ message, default: current }) => { asked.push([message.split(' ')[0], current, message]); return ''; },
|
|
1311
|
+
password: async ({ message }) => { asked.push([message.split(' ')[0], undefined, message]); return ''; },
|
|
1312
|
+
checkbox: async (opts) => { asked.push(['OMEGA_RUNNER_ORGS', null, opts.message]); return ['acme']; },
|
|
1313
|
+
};
|
|
1314
|
+
// The shell of Ian's window: an exported org list and a brand's token.
|
|
1315
|
+
const env = { OMEGA_RUNNER_ORGS: 'org1,org2,org3', GH_TOKEN: 'ghp_from_the_brand' };
|
|
1316
|
+
const result = await runnerEnv.reconfigureRunnerEnv({
|
|
1317
|
+
home, env, interactive: true, prompt,
|
|
1318
|
+
discoverOrgs: async () => ['acme', 'zed'],
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
// Every schema key of both scopes, in schema order, required or not.
|
|
1322
|
+
ctx.expect(asked.map((a) => a[0])).toEqual([
|
|
1323
|
+
'GH_TOKEN', 'WIN_EV_TOKEN_PATH', 'WIN_CSC_KEY_PASSWORD', 'SIGNTOOL_PATH', 'WIN_TIMESTAMP_URL', 'OMEGA_RUNNER_ORGS',
|
|
1324
|
+
]);
|
|
1325
|
+
// The file's value is the default; a secret never echoes, it says so instead.
|
|
1326
|
+
ctx.expect(asked.find((a) => a[0] === 'WIN_EV_TOKEN_PATH')[1]).toBe('a'.repeat(40));
|
|
1327
|
+
ctx.expect(asked.find((a) => a[0] === 'SIGNTOOL_PATH')[1]).toBe('C:/sdk/signtool.exe');
|
|
1328
|
+
ctx.expect(asked.find((a) => a[0] === 'GH_TOKEN')[2]).toContain('Enter keeps the current value');
|
|
1329
|
+
ctx.expect(asked.find((a) => a[0] === 'GH_TOKEN')[2]).not.toContain('ghp_saved');
|
|
1330
|
+
|
|
1331
|
+
// Enter everywhere: every key is still ANSWERED (and so rewritten
|
|
1332
|
+
// quoted), and the walk's answers — the box's own values — are what
|
|
1333
|
+
// the rest of the command sees.
|
|
1334
|
+
ctx.expect(result.answers.sort()).toEqual(['GH_TOKEN', 'SIGNTOOL_PATH', 'WIN_CSC_KEY_PASSWORD', 'WIN_EV_TOKEN_PATH']);
|
|
1335
|
+
ctx.expect(env.GH_TOKEN).toBe('ghp_saved');
|
|
1336
|
+
ctx.expect(env.OMEGA_RUNNER_ORGS).toBe('acme');
|
|
1337
|
+
ctx.expect(jetpack.read(path.join(home, '.env'))).toContain('GH_TOKEN="ghp_saved"');
|
|
1338
|
+
|
|
1339
|
+
// install runs that same walk, so it is asked the same way (source-text
|
|
1340
|
+
// guard lives in the install-wiring case below).
|
|
1341
|
+
ctx.expect(typeof runnerEnv.reconfigureRunnerEnv).toBe('function');
|
|
1342
|
+
} finally {
|
|
1343
|
+
jetpack.remove(home);
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
name: 'a value only the shell carries is the current value, and Enter saves it — secret or not',
|
|
1349
|
+
run: async (ctx) => {
|
|
1350
|
+
// "Current" means current: a key the file does not have yet but the
|
|
1351
|
+
// shell delivered is offered as the default, and accepting it writes it
|
|
1352
|
+
// to the box's file. The non-secret keys already did this (the prompt
|
|
1353
|
+
// hands the default back); a secret must not be the exception, and the
|
|
1354
|
+
// message says "current", not "saved", because it may be neither.
|
|
1355
|
+
const fs = require('fs');
|
|
1356
|
+
const os = require('os');
|
|
1357
|
+
const jetpack = require('fs-jetpack');
|
|
1358
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1359
|
+
|
|
1360
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1361
|
+
try {
|
|
1362
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1363
|
+
'GH_TOKEN=""',
|
|
1364
|
+
`WIN_EV_TOKEN_PATH=""`,
|
|
1365
|
+
'WIN_CSC_KEY_PASSWORD="1234"',
|
|
1366
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
1367
|
+
].join('\n'));
|
|
1368
|
+
|
|
1369
|
+
const asked = [];
|
|
1370
|
+
const env = { GH_TOKEN: 'ghp_from_the_shell', WIN_EV_TOKEN_PATH: 'e'.repeat(40) };
|
|
1371
|
+
const result = await runnerEnv.reconfigureRunnerEnv({
|
|
1372
|
+
home, env, interactive: true,
|
|
1373
|
+
prompt: {
|
|
1374
|
+
input: async ({ message, default: current }) => { asked.push([message, current]); return ''; }, // Enter
|
|
1375
|
+
password: async ({ message }) => { asked.push([message, undefined]); return ''; }, // Enter
|
|
1376
|
+
checkbox: async () => ['acme'],
|
|
1377
|
+
},
|
|
1378
|
+
discoverOrgs: async () => ['acme'],
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
// Offered as the default (secret: said, never echoed), and saved.
|
|
1382
|
+
ctx.expect(asked.find((a) => a[0].startsWith('WIN_EV_TOKEN_PATH'))[1]).toBe('e'.repeat(40));
|
|
1383
|
+
ctx.expect(asked.find((a) => a[0].startsWith('GH_TOKEN'))[0]).toContain('Enter keeps the current value');
|
|
1384
|
+
ctx.expect(asked.find((a) => a[0].startsWith('GH_TOKEN'))[0]).not.toContain('ghp_from_the_shell');
|
|
1385
|
+
|
|
1386
|
+
ctx.expect(result.answers.sort()).toEqual(['GH_TOKEN', 'OMEGA_RUNNER_ORGS', 'SIGNTOOL_PATH', 'WIN_CSC_KEY_PASSWORD', 'WIN_EV_TOKEN_PATH']);
|
|
1387
|
+
const written = jetpack.read(path.join(home, '.env'));
|
|
1388
|
+
ctx.expect(written).toContain('GH_TOKEN="ghp_from_the_shell"');
|
|
1389
|
+
ctx.expect(written).toContain(`WIN_EV_TOKEN_PATH="${'e'.repeat(40)}"`);
|
|
1390
|
+
ctx.expect(written).toContain('WIN_CSC_KEY_PASSWORD="1234"'); // kept: rewritten with the same value, quoted
|
|
1391
|
+
ctx.expect((written.match(/^GH_TOKEN=/gm) || []).length).toBe(1);
|
|
1392
|
+
} finally {
|
|
1393
|
+
jetpack.remove(home);
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
name: 'the walk rewrites every answered key, so a pre-quoting file comes back fully quoted',
|
|
1399
|
+
run: async (ctx) => {
|
|
1400
|
+
// A box configured before the quoting rule kept its bare `KEY=value`
|
|
1401
|
+
// lines forever: only a CHANGED value was written, so Enter (keep) never
|
|
1402
|
+
// rewrote the line. Every value the walk ends with is written now — the
|
|
1403
|
+
// kept ones too — and the writer serializes `KEY="value"`, so one walk
|
|
1404
|
+
// brings an old file up to the rule without changing a single value.
|
|
1405
|
+
const fs = require('fs');
|
|
1406
|
+
const jetpack = require('fs-jetpack');
|
|
1407
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1408
|
+
|
|
1409
|
+
const home = fs.mkdtempSync(path.join(SCRATCH_HOME, 'quoted-'));
|
|
1410
|
+
try {
|
|
1411
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1412
|
+
'GH_TOKEN=abc', // written by the pre-quoting code
|
|
1413
|
+
'OMEGA_RUNNER_ORGS="acme"',
|
|
1414
|
+
`WIN_EV_TOKEN_PATH=${'a'.repeat(40)}`,
|
|
1415
|
+
'WIN_CSC_KEY_PASSWORD="old pin"', // already quoted, spaces and all
|
|
1416
|
+
'SIGNTOOL_PATH=C:/sdk/signtool.exe',
|
|
1417
|
+
'WIN_TIMESTAMP_URL=http://timestamp.sectigo.com',
|
|
1418
|
+
].join('\n'));
|
|
1419
|
+
|
|
1420
|
+
// Enter on everything, the same fake prompts the walk cases above use.
|
|
1421
|
+
await runnerEnv.reconfigureRunnerEnv({
|
|
1422
|
+
home, env: {}, interactive: true,
|
|
1423
|
+
prompt: {
|
|
1424
|
+
input: async ({ default: current }) => current || '',
|
|
1425
|
+
password: async () => '',
|
|
1426
|
+
checkbox: async () => ['acme'],
|
|
1427
|
+
},
|
|
1428
|
+
discoverOrgs: async () => ['acme'],
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// Every KEY= line in the file is now KEY="…", commented ones included.
|
|
1432
|
+
const written = jetpack.read(path.join(home, '.env'));
|
|
1433
|
+
const bare = written.split('\n').filter((line) => /^\s*[A-Z][A-Z0-9_]*\s*=/.test(line) && !/^[A-Z][A-Z0-9_]*="[^"]*"$/.test(line));
|
|
1434
|
+
ctx.expect(bare).toEqual([]);
|
|
1435
|
+
|
|
1436
|
+
// …and not one value moved.
|
|
1437
|
+
const back = {};
|
|
1438
|
+
runnerEnv.loadRunnerEnv({ home, env: back });
|
|
1439
|
+
ctx.expect(back.GH_TOKEN).toBe('abc');
|
|
1440
|
+
ctx.expect(back.OMEGA_RUNNER_ORGS).toBe('acme');
|
|
1441
|
+
ctx.expect(back.WIN_EV_TOKEN_PATH).toBe('a'.repeat(40));
|
|
1442
|
+
ctx.expect(back.WIN_CSC_KEY_PASSWORD).toBe('old pin');
|
|
1443
|
+
ctx.expect(back.SIGNTOOL_PATH).toBe('C:/sdk/signtool.exe');
|
|
1444
|
+
ctx.expect(back.WIN_TIMESTAMP_URL).toBe('http://timestamp.sectigo.com');
|
|
1445
|
+
ctx.expect((written.match(/^GH_TOKEN=/gm) || []).length).toBe(1);
|
|
1446
|
+
} finally {
|
|
1447
|
+
jetpack.remove(home);
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: 'the orgs checkbox lists orgs alphabetically and ticks the saved list, else this install\u2019s registered orgs, else nothing',
|
|
1453
|
+
run: async (ctx) => {
|
|
1454
|
+
// A token that administers 35 orgs must never register 35 runners on one
|
|
1455
|
+
// Enter, so nothing is ticked by default. What IS ticked: what the box
|
|
1456
|
+
// already answered, else what it already registered. The list is
|
|
1457
|
+
// alphabetical, case-insensitively, never GitHub's membership order.
|
|
1458
|
+
const fs = require('fs');
|
|
1459
|
+
const os = require('os');
|
|
1460
|
+
const jetpack = require('fs-jetpack');
|
|
1461
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1462
|
+
|
|
1463
|
+
const filled = (orgs) => [
|
|
1464
|
+
'GH_TOKEN="ghp_from_the_box"',
|
|
1465
|
+
`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`,
|
|
1466
|
+
'WIN_CSC_KEY_PASSWORD="1234"',
|
|
1467
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
1468
|
+
`OMEGA_RUNNER_ORGS="${orgs}"`,
|
|
1469
|
+
].join('\n');
|
|
1470
|
+
|
|
1471
|
+
const walk = async (home, registeredOrgs) => {
|
|
1472
|
+
let choices;
|
|
1473
|
+
await runnerEnv.reconfigureRunnerEnv({
|
|
1474
|
+
home, env: {}, interactive: true, registeredOrgs,
|
|
1475
|
+
prompt: {
|
|
1476
|
+
input: async ({ default: current }) => current || '',
|
|
1477
|
+
password: async () => '',
|
|
1478
|
+
checkbox: async (opts) => { choices = opts.choices; return ['Acme']; },
|
|
1479
|
+
},
|
|
1480
|
+
discoverOrgs: async () => ['Acme', 'zed', 'solo'],
|
|
1481
|
+
});
|
|
1482
|
+
return choices.map((c) => `${c.value}:${c.checked ? 'on' : 'off'}`);
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
const saved = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1486
|
+
const installed = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1487
|
+
const fresh = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1488
|
+
try {
|
|
1489
|
+
// 1. The saved list wins, case-insensitively.
|
|
1490
|
+
jetpack.write(path.join(saved, '.env'), filled('acme, zed'));
|
|
1491
|
+
ctx.expect(await walk(saved, ['solo'])).toEqual(['Acme:on', 'solo:off', 'zed:on']);
|
|
1492
|
+
|
|
1493
|
+
// 2. No saved list: what this install registered.
|
|
1494
|
+
jetpack.write(path.join(installed, '.env'), filled(''));
|
|
1495
|
+
ctx.expect(await walk(installed, ['zed'])).toEqual(['Acme:off', 'solo:off', 'zed:on']);
|
|
1496
|
+
|
|
1497
|
+
// 3. Neither: nothing ticked. One Enter registers nothing, by design.
|
|
1498
|
+
jetpack.write(path.join(fresh, '.env'), filled(''));
|
|
1499
|
+
ctx.expect(await walk(fresh, [])).toEqual(['Acme:off', 'solo:off', 'zed:off']);
|
|
1500
|
+
ctx.expect(jetpack.read(path.join(fresh, '.env'))).toContain('OMEGA_RUNNER_ORGS="Acme"');
|
|
1501
|
+
} finally {
|
|
1502
|
+
for (const home of [saved, installed, fresh]) jetpack.remove(home);
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: 'a runner with no org is not a runner: zero ticked refuses, and a token with no admin org skips the question',
|
|
1508
|
+
run: async (ctx) => {
|
|
1509
|
+
const fs = require('fs');
|
|
1510
|
+
const os = require('os');
|
|
1511
|
+
const jetpack = require('fs-jetpack');
|
|
1512
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1513
|
+
|
|
1514
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1515
|
+
try {
|
|
1516
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1517
|
+
'GH_TOKEN="ghp_from_the_box"',
|
|
1518
|
+
`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`,
|
|
1519
|
+
'WIN_CSC_KEY_PASSWORD="1234"',
|
|
1520
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
1521
|
+
].join('\n'));
|
|
1522
|
+
|
|
1523
|
+
const keep = { input: async ({ default: current }) => current || '', password: async () => '' };
|
|
1524
|
+
|
|
1525
|
+
// Everything unticked: refuse, naming the key. Nothing is written.
|
|
1526
|
+
let threw;
|
|
1527
|
+
try {
|
|
1528
|
+
await runnerEnv.reconfigureRunnerEnv({
|
|
1529
|
+
home, env: {}, interactive: true,
|
|
1530
|
+
prompt: { ...keep, checkbox: async () => [] },
|
|
1531
|
+
discoverOrgs: async () => ['Acme', 'zed'],
|
|
1532
|
+
});
|
|
1533
|
+
} catch (e) { threw = e; }
|
|
1534
|
+
ctx.expect(threw).toBeDefined();
|
|
1535
|
+
ctx.expect(threw.message).toContain('OMEGA_RUNNER_ORGS');
|
|
1536
|
+
const back = {};
|
|
1537
|
+
runnerEnv.loadRunnerEnv({ home, env: back });
|
|
1538
|
+
ctx.expect(back.OMEGA_RUNNER_ORGS).toBeUndefined();
|
|
1539
|
+
|
|
1540
|
+
// A token that administers nothing: one warn line, no question, no refusal.
|
|
1541
|
+
const logged = [];
|
|
1542
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]) };
|
|
1543
|
+
const env = {};
|
|
1544
|
+
const result = await runnerEnv.reconfigureRunnerEnv({
|
|
1545
|
+
home, env, interactive: true, logger,
|
|
1546
|
+
prompt: { ...keep, checkbox: async () => { throw new Error('nothing to tick'); } },
|
|
1547
|
+
discoverOrgs: async () => [],
|
|
1548
|
+
});
|
|
1549
|
+
ctx.expect(result.orgs).toBeNull();
|
|
1550
|
+
ctx.expect(result.adminOrgs).toEqual([]); // asked GitHub, got nothing — install need not ask again
|
|
1551
|
+
ctx.expect(env.OMEGA_RUNNER_ORGS).toBeUndefined();
|
|
1552
|
+
ctx.expect(logged.filter((l) => l[0] === 'warn').map((l) => l[1]).join('\n')).toContain('admin');
|
|
1553
|
+
} finally {
|
|
1554
|
+
jetpack.remove(home);
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
name: 'off a TTY the walk asks nothing — install refuses only for a missing required key, config refuses outright',
|
|
1560
|
+
run: async (ctx) => {
|
|
1561
|
+
const fs = require('fs');
|
|
1562
|
+
const os = require('os');
|
|
1563
|
+
const jetpack = require('fs-jetpack');
|
|
1564
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
1565
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1566
|
+
|
|
1567
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1568
|
+
try {
|
|
1569
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1570
|
+
'GH_TOKEN="ghp_from_the_box"',
|
|
1571
|
+
`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`,
|
|
1572
|
+
'WIN_CSC_KEY_PASSWORD="1234"',
|
|
1573
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
1574
|
+
].join('\n'));
|
|
1575
|
+
|
|
1576
|
+
// CI has no keyboard: nothing is asked, nothing is discovered, and a
|
|
1577
|
+
// blank OMEGA_RUNNER_ORGS still means every org the token administers.
|
|
1578
|
+
let discovered = false;
|
|
1579
|
+
const env = {};
|
|
1580
|
+
const result = await runnerEnv.reconfigureRunnerEnv({
|
|
1581
|
+
home, env, interactive: false, prompt: {},
|
|
1582
|
+
discoverOrgs: async () => { discovered = true; return ['Acme']; },
|
|
1583
|
+
});
|
|
1584
|
+
ctx.expect(result.answers).toEqual([]);
|
|
1585
|
+
ctx.expect(discovered).toBe(false); // no terminal, no question, no API call
|
|
1586
|
+
ctx.expect(result.adminOrgs).toBeNull(); // nothing discovered, so install discovers for itself
|
|
1587
|
+
ctx.expect(env.OMEGA_RUNNER_ORGS).toBeUndefined();
|
|
1588
|
+
|
|
1589
|
+
const all = runner.selectRunnerOrgs(['Acme', 'zed'], env.OMEGA_RUNNER_ORGS);
|
|
1590
|
+
ctx.expect(all.orgs).toEqual(['Acme', 'zed']);
|
|
1591
|
+
ctx.expect(all.filtered).toBe(false);
|
|
1592
|
+
|
|
1593
|
+
// An org in the list the token does not administer is named, not registered.
|
|
1594
|
+
const partial = runner.selectRunnerOrgs(['Acme'], 'acme, ghost');
|
|
1595
|
+
ctx.expect(partial.orgs).toEqual(['Acme']);
|
|
1596
|
+
ctx.expect(partial.unmatched).toEqual(['ghost']);
|
|
1597
|
+
|
|
1598
|
+
// A required key missing off a TTY still refuses, naming the file and the keys.
|
|
1599
|
+
jetpack.write(path.join(home, '.env'), 'GH_TOKEN="ghp_from_the_box"\n');
|
|
1600
|
+
let threw;
|
|
1601
|
+
try {
|
|
1602
|
+
await runnerEnv.reconfigureRunnerEnv({ home, env: {}, interactive: false, prompt: {} });
|
|
1603
|
+
} catch (e) { threw = e; }
|
|
1604
|
+
ctx.expect(threw).toBeDefined();
|
|
1605
|
+
ctx.expect(threw.message).toContain('WIN_EV_TOKEN_PATH, WIN_CSC_KEY_PASSWORD, SIGNTOOL_PATH');
|
|
1606
|
+
ctx.expect(threw.message).toContain(path.join(home, '.env'));
|
|
1607
|
+
ctx.expect(threw.message).toContain('no terminal');
|
|
1608
|
+
|
|
1609
|
+
// `config` has nothing to ask WITH, so it refuses even fully configured.
|
|
1610
|
+
threw = undefined;
|
|
1611
|
+
try {
|
|
1612
|
+
await runnerEnv.reconfigureRunnerEnv({ home, env: {}, interactive: false, prompt: {}, requireTerminal: true });
|
|
1613
|
+
} catch (e) { threw = e; }
|
|
1614
|
+
ctx.expect(threw.message).toContain(path.join(home, '.env'));
|
|
1615
|
+
ctx.expect(threw.message).toContain('interactive terminal');
|
|
1616
|
+
} finally {
|
|
1617
|
+
jetpack.remove(home);
|
|
1618
|
+
}
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
name: '`runner config` re-asks every key with the saved value as its default, and Enter keeps a saved secret',
|
|
1623
|
+
run: async (ctx) => {
|
|
1624
|
+
// install and start ask only for what is MISSING; `config` is how a
|
|
1625
|
+
// value that is already there gets CHANGED. Both scopes, every key,
|
|
1626
|
+
// then the orgs checkbox with the current list ticked.
|
|
1627
|
+
const fs = require('fs');
|
|
1628
|
+
const os = require('os');
|
|
1629
|
+
const jetpack = require('fs-jetpack');
|
|
1630
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1631
|
+
|
|
1632
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1633
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1634
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1635
|
+
try {
|
|
1636
|
+
jetpack.write(path.join(home, '.env'), [
|
|
1637
|
+
'GH_TOKEN=ghp_old',
|
|
1638
|
+
'OMEGA_RUNNER_ORGS=acme',
|
|
1639
|
+
`WIN_EV_TOKEN_PATH=${'a'.repeat(40)}`,
|
|
1640
|
+
'WIN_CSC_KEY_PASSWORD=old-pin',
|
|
1641
|
+
'SIGNTOOL_PATH=C:/sdk/signtool.exe',
|
|
1642
|
+
'WIN_TIMESTAMP_URL=',
|
|
1643
|
+
].join('\n'));
|
|
1644
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['acme'] });
|
|
1645
|
+
|
|
1646
|
+
const asked = [];
|
|
1647
|
+
const logged = [];
|
|
1648
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1649
|
+
let choices;
|
|
1650
|
+
const prompt = {
|
|
1651
|
+
input: async ({ message, default: current }) => { asked.push(['input', message, current]); return message.startsWith('WIN_TIMESTAMP_URL') ? 'http://ts.example' : (current || ''); },
|
|
1652
|
+
password: async ({ message }) => { asked.push(['password', message]); return message.startsWith('GH_TOKEN') ? 'ghp_new' : ''; },
|
|
1653
|
+
checkbox: async (opts) => { asked.push(['checkbox', opts.message]); choices = opts.choices; return ['acme', 'zed']; },
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
await runner({
|
|
1657
|
+
_: ['runner', 'config'],
|
|
1658
|
+
_home: home, _env: {}, _logger: logger, _prompt: prompt, _interactive: true,
|
|
1659
|
+
_discoverOrgs: async () => ['acme', 'zed'],
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Every key of both scopes, in file order — the orgs checkbox IS the orgs question.
|
|
1663
|
+
ctx.expect(asked.map((a) => a[1].split(' ')[0])).toEqual([
|
|
1664
|
+
'GH_TOKEN', 'WIN_EV_TOKEN_PATH', 'WIN_CSC_KEY_PASSWORD', 'SIGNTOOL_PATH', 'WIN_TIMESTAMP_URL', 'OMEGA_RUNNER_ORGS',
|
|
1665
|
+
]);
|
|
1666
|
+
ctx.expect(asked.find((a) => a[1].startsWith('WIN_EV_TOKEN_PATH'))[2]).toBe('a'.repeat(40)); // current value is the default
|
|
1667
|
+
ctx.expect(asked.find((a) => a[1].startsWith('WIN_CSC_KEY_PASSWORD'))[1]).toContain('Enter keeps the current value');
|
|
1668
|
+
|
|
1669
|
+
const written = jetpack.read(path.join(home, '.env'));
|
|
1670
|
+
ctx.expect(written).toContain('GH_TOKEN="ghp_new"'); // changed
|
|
1671
|
+
ctx.expect(written).toContain('WIN_CSC_KEY_PASSWORD="old-pin"'); // empty answer keeps the secret, rewritten in place quoted
|
|
1672
|
+
ctx.expect(written).toContain('WIN_TIMESTAMP_URL="http://ts.example"');
|
|
1673
|
+
ctx.expect(written).toContain('OMEGA_RUNNER_ORGS="acme,zed"');
|
|
1674
|
+
ctx.expect((written.match(/^GH_TOKEN=/gm) || []).length).toBe(1);
|
|
1675
|
+
|
|
1676
|
+
// The current list is ticked; the rest are not.
|
|
1677
|
+
ctx.expect(choices.map((c) => `${c.value}:${c.checked ? 'on' : 'off'}`)).toEqual(['acme:on', 'zed:off']);
|
|
1678
|
+
|
|
1679
|
+
// The new list is not the registered one, so say what applies it.
|
|
1680
|
+
ctx.expect(logged.map((l) => l[1]).join('\n')).toContain('npx omega runner install');
|
|
1681
|
+
|
|
1682
|
+
// Off a TTY there is nothing to ask with: refuse, naming the file.
|
|
1683
|
+
let threw;
|
|
1684
|
+
try {
|
|
1685
|
+
await runner({ _: ['runner', 'config'], _home: home, _env: {}, _logger: logger, _prompt: {}, _interactive: false });
|
|
1686
|
+
} catch (e) { threw = e; }
|
|
1687
|
+
ctx.expect(threw).toBeDefined();
|
|
1688
|
+
ctx.expect(threw.message).toContain(path.join(home, '.env'));
|
|
1689
|
+
} finally {
|
|
1690
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1691
|
+
jetpack.remove(home);
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
name: '`start` settles the box config first: a missing required key is asked in a terminal, refused without one',
|
|
1697
|
+
run: async (ctx) => {
|
|
1698
|
+
// A box missing a required key is refused by start exactly as it is by
|
|
1699
|
+
// install — a listener that cannot sign is worse than no listener.
|
|
1700
|
+
const fs = require('fs');
|
|
1701
|
+
const os = require('os');
|
|
1702
|
+
const jetpack = require('fs-jetpack');
|
|
1703
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1704
|
+
|
|
1705
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1706
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1707
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1708
|
+
const logged = [];
|
|
1709
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1710
|
+
try {
|
|
1711
|
+
let threw;
|
|
1712
|
+
try {
|
|
1713
|
+
await runner({ _: ['runner', 'start'], _home: home, _env: {}, _logger: logger, _prompt: {}, _interactive: false });
|
|
1714
|
+
} catch (e) { threw = e; }
|
|
1715
|
+
ctx.expect(threw).toBeDefined();
|
|
1716
|
+
ctx.expect(threw.message).toContain('GH_TOKEN');
|
|
1717
|
+
ctx.expect(threw.message).toContain(path.join(home, '.env'));
|
|
1718
|
+
ctx.expect(threw.message).toContain('no terminal');
|
|
1719
|
+
|
|
1720
|
+
// Interactive, the questions land and the answers are saved. Driven
|
|
1721
|
+
// through the preflight, since `start` itself would spawn a listener.
|
|
1722
|
+
let checkboxes = 0;
|
|
1723
|
+
const env = {};
|
|
1724
|
+
await runner.startPreflight({
|
|
1725
|
+
home, env, logger, interactive: true,
|
|
1726
|
+
prompt: {
|
|
1727
|
+
input: async ({ message, default: suggestion }) => (message.startsWith('SIGNTOOL_PATH') ? (suggestion || 'C:/sdk/signtool.exe') : 'a'.repeat(40)),
|
|
1728
|
+
password: async ({ message }) => (message.startsWith('GH_TOKEN') ? 'ghp_answered' : '1234'),
|
|
1729
|
+
checkbox: async () => { checkboxes++; return []; },
|
|
1730
|
+
},
|
|
1731
|
+
});
|
|
1732
|
+
ctx.expect(env.GH_TOKEN).toBe('ghp_answered');
|
|
1733
|
+
ctx.expect(jetpack.read(path.join(home, '.env'))).toContain('GH_TOKEN="ghp_answered"');
|
|
1734
|
+
ctx.expect(checkboxes).toBe(0); // the orgs question belongs to install and config
|
|
1735
|
+
} finally {
|
|
1736
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1737
|
+
jetpack.remove(home);
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
name: '`start` warns when OMEGA_RUNNER_ORGS names orgs this install never registered',
|
|
1743
|
+
run: async (ctx) => {
|
|
1744
|
+
// start registers nothing — it says which command would.
|
|
1745
|
+
const fs = require('fs');
|
|
1746
|
+
const os = require('os');
|
|
1747
|
+
const jetpack = require('fs-jetpack');
|
|
1748
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1749
|
+
|
|
1750
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1751
|
+
const logged = [];
|
|
1752
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1753
|
+
const filled = {
|
|
1754
|
+
GH_TOKEN: 'ghp_from_the_box',
|
|
1755
|
+
WIN_EV_TOKEN_PATH: 'a'.repeat(40),
|
|
1756
|
+
WIN_CSC_KEY_PASSWORD: '1234',
|
|
1757
|
+
SIGNTOOL_PATH: 'C:/sdk/signtool.exe',
|
|
1758
|
+
};
|
|
1759
|
+
try {
|
|
1760
|
+
jetpack.write(path.join(home, 'config.json'), { registeredOrgs: ['acme'] });
|
|
1761
|
+
|
|
1762
|
+
await runner.startPreflight({ home, env: { ...filled, OMEGA_RUNNER_ORGS: 'Acme, zed' }, logger, interactive: false, prompt: {} });
|
|
1763
|
+
const warns = logged.filter((l) => l[0] === 'warn').map((l) => l[1]).join('\n');
|
|
1764
|
+
ctx.expect(warns).toContain('OMEGA_RUNNER_ORGS names Acme, zed');
|
|
1765
|
+
ctx.expect(warns).toContain('registered acme');
|
|
1766
|
+
ctx.expect(warns).toContain('npx omega runner install');
|
|
1767
|
+
|
|
1768
|
+
// The same set spelled differently is the same set: nothing to say.
|
|
1769
|
+
logged.length = 0;
|
|
1770
|
+
await runner.startPreflight({ home, env: { ...filled, OMEGA_RUNNER_ORGS: 'ACME' }, logger, interactive: false, prompt: {} });
|
|
1771
|
+
ctx.expect(logged.filter((l) => l[0] === 'warn')).toEqual([]);
|
|
1772
|
+
|
|
1773
|
+
// Blank means every admin org, which no registered list contradicts.
|
|
1774
|
+
await runner.startPreflight({ home, env: { ...filled }, logger, interactive: false, prompt: {} });
|
|
1775
|
+
ctx.expect(logged.filter((l) => l[0] === 'warn')).toEqual([]);
|
|
1776
|
+
} finally {
|
|
1777
|
+
jetpack.remove(home);
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
name: '`start` brings EVERY registered org online, detached, in Startup-shortcut order',
|
|
1783
|
+
run: async (ctx) => {
|
|
1784
|
+
// The hole this closes: `start` brought up the FIRST shortcut only, in
|
|
1785
|
+
// the calling terminal, and every other registered org stayed offline
|
|
1786
|
+
// until the next logon. It now walks them all, through the injected
|
|
1787
|
+
// spawn so a case never runs cmd.exe.
|
|
1788
|
+
const fs = require('fs');
|
|
1789
|
+
const os = require('os');
|
|
1790
|
+
const jetpack = require('fs-jetpack');
|
|
1791
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1792
|
+
|
|
1793
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1794
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1795
|
+
const origExit = process.exitCode;
|
|
1796
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1797
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
1798
|
+
const orgs = ['acme', 'zeta'];
|
|
1799
|
+
const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
|
|
1800
|
+
const logged = [];
|
|
1801
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1802
|
+
const spawned = [];
|
|
1803
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
1804
|
+
try {
|
|
1805
|
+
for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
|
|
1806
|
+
for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
|
|
1807
|
+
|
|
1808
|
+
await runner({
|
|
1809
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
1810
|
+
_listeners: () => [],
|
|
1811
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 1000 + spawned.length }; },
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
// One spawn per shortcut, in the order the shortcuts are listed.
|
|
1815
|
+
ctx.expect(spawned).toEqual(orgs.map((org) => path.join(home, `actions-runner-${org}`)));
|
|
1816
|
+
const lines = logged.map((l) => l[1]).join('\n');
|
|
1817
|
+
for (const org of orgs) ctx.expect(lines).toContain(`${hostPrefix}${org}`);
|
|
1818
|
+
ctx.expect(lines).toContain('PID=1001');
|
|
1819
|
+
ctx.expect(lines).toContain('PID=1002');
|
|
1820
|
+
// Every org online: the run is a success.
|
|
1821
|
+
ctx.expect(process.exitCode).toBe(origExit);
|
|
1822
|
+
} finally {
|
|
1823
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1824
|
+
process.exitCode = origExit;
|
|
1825
|
+
for (const file of shortcuts) jetpack.remove(file);
|
|
1826
|
+
jetpack.remove(home);
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
name: '`start` skips the org whose listener is already alive, naming its PID, and starts the rest',
|
|
1832
|
+
run: async (ctx) => {
|
|
1833
|
+
// Idempotent: running it twice is running it once. A second listener on
|
|
1834
|
+
// one registration is the session-takeover storm, so an alive org is
|
|
1835
|
+
// named and left alone rather than refused or duplicated.
|
|
1836
|
+
const fs = require('fs');
|
|
1837
|
+
const os = require('os');
|
|
1838
|
+
const jetpack = require('fs-jetpack');
|
|
1839
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1840
|
+
|
|
1841
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1842
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1843
|
+
const origExit = process.exitCode;
|
|
1844
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1845
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
1846
|
+
const orgs = ['acme', 'zeta'];
|
|
1847
|
+
const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
|
|
1848
|
+
const logged = [];
|
|
1849
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1850
|
+
const spawned = [];
|
|
1851
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
1852
|
+
const aliveDir = path.join(home, 'actions-runner-acme');
|
|
1853
|
+
try {
|
|
1854
|
+
for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
|
|
1855
|
+
for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
|
|
1856
|
+
|
|
1857
|
+
await runner({
|
|
1858
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
1859
|
+
_listeners: (dir) => (dir === aliveDir ? [{ pid: 4242, sessionId: 1, execPath: path.join(aliveDir, 'bin', 'Runner.Listener.exe') }] : []),
|
|
1860
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 7777 }; },
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
ctx.expect(spawned).toEqual([path.join(home, 'actions-runner-zeta')]);
|
|
1864
|
+
const lines = logged.map((l) => l[1]).join('\n');
|
|
1865
|
+
ctx.expect(lines).toContain('acme');
|
|
1866
|
+
ctx.expect(lines).toContain('PID=4242'); // the skip names the live listener
|
|
1867
|
+
ctx.expect(lines).toContain('PID=7777'); // and the one it did start
|
|
1868
|
+
ctx.expect(process.exitCode).toBe(origExit); // a skip is not a failure
|
|
1869
|
+
} finally {
|
|
1870
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1871
|
+
process.exitCode = origExit;
|
|
1872
|
+
for (const file of shortcuts) jetpack.remove(file);
|
|
1873
|
+
jetpack.remove(home);
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
name: 'a spawn that failed leaves that org offline and the run exits non-zero',
|
|
1879
|
+
run: async (ctx) => {
|
|
1880
|
+
// A partial start is a failure: the exit code is what a script or a
|
|
1881
|
+
// logon task reads.
|
|
1882
|
+
const fs = require('fs');
|
|
1883
|
+
const os = require('os');
|
|
1884
|
+
const jetpack = require('fs-jetpack');
|
|
1885
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1886
|
+
|
|
1887
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1888
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1889
|
+
const origExit = process.exitCode;
|
|
1890
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1891
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
1892
|
+
const orgs = ['acme', 'zeta'];
|
|
1893
|
+
const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
|
|
1894
|
+
const logged = [];
|
|
1895
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1896
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
1897
|
+
try {
|
|
1898
|
+
for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
|
|
1899
|
+
for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
|
|
1900
|
+
|
|
1901
|
+
await runner({
|
|
1902
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
1903
|
+
_listeners: () => [],
|
|
1904
|
+
_spawn: (dir) => (dir.endsWith('actions-runner-zeta') ? { ok: false, message: 'run.cmd not found', pid: null } : { ok: true, pid: 1001 }),
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
ctx.expect(process.exitCode).toBe(1);
|
|
1908
|
+
const lines = logged.map((l) => l[1]).join('\n');
|
|
1909
|
+
ctx.expect(lines).toContain('run.cmd not found');
|
|
1910
|
+
ctx.expect(lines).toContain('PID=1001'); // the org that did come up is still reported
|
|
1911
|
+
} finally {
|
|
1912
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1913
|
+
process.exitCode = origExit;
|
|
1914
|
+
for (const file of shortcuts) jetpack.remove(file);
|
|
1915
|
+
jetpack.remove(home);
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
name: '`restart` stops first, then starts every org',
|
|
1921
|
+
run: async (ctx) => {
|
|
1922
|
+
// One command for the loop an operator ran by hand. The order is the
|
|
1923
|
+
// whole point: a start before the stop is the takeover storm.
|
|
1924
|
+
const fs = require('fs');
|
|
1925
|
+
const os = require('os');
|
|
1926
|
+
const jetpack = require('fs-jetpack');
|
|
1927
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1928
|
+
|
|
1929
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1930
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1931
|
+
const origExit = process.exitCode;
|
|
1932
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1933
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
1934
|
+
const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
|
|
1935
|
+
const logged = [];
|
|
1936
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1937
|
+
const spawned = [];
|
|
1938
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
1939
|
+
try {
|
|
1940
|
+
jetpack.dir(path.join(home, 'actions-runner-acme'));
|
|
1941
|
+
jetpack.write(shortcut, '@echo off\r\n');
|
|
1942
|
+
|
|
1943
|
+
await runner({
|
|
1944
|
+
_: ['runner', 'restart'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
1945
|
+
_listeners: () => [],
|
|
1946
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 2222 }; },
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
const lines = logged.map((l) => l[1]);
|
|
1950
|
+
const stopped = lines.findIndex((l) => l.includes('already stopped'));
|
|
1951
|
+
const started = lines.findIndex((l) => l.includes('PID=2222'));
|
|
1952
|
+
ctx.expect(stopped >= 0).toBe(true);
|
|
1953
|
+
ctx.expect(started >= 0).toBe(true);
|
|
1954
|
+
ctx.expect(stopped < started).toBe(true);
|
|
1955
|
+
ctx.expect(spawned).toEqual([path.join(home, 'actions-runner-acme')]);
|
|
1956
|
+
} finally {
|
|
1957
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
1958
|
+
process.exitCode = origExit;
|
|
1959
|
+
jetpack.remove(shortcut);
|
|
1960
|
+
jetpack.remove(home);
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
name: '`restart` settles the box config BEFORE it kills anything',
|
|
1966
|
+
run: async (ctx) => {
|
|
1967
|
+
// Preflight lived inside `start`, so a box missing a required key was
|
|
1968
|
+
// stopped, then refused: every listener dead and nothing to bring them
|
|
1969
|
+
// back. It now runs first, and the refusal costs nothing.
|
|
1970
|
+
const fs = require('fs');
|
|
1971
|
+
const os = require('os');
|
|
1972
|
+
const jetpack = require('fs-jetpack');
|
|
1973
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
1974
|
+
|
|
1975
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
1976
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
1977
|
+
const origExit = process.exitCode;
|
|
1978
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
1979
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
1980
|
+
const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
|
|
1981
|
+
const logged = [];
|
|
1982
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
1983
|
+
const spawned = [];
|
|
1984
|
+
const killed = [];
|
|
1985
|
+
try {
|
|
1986
|
+
jetpack.dir(path.join(home, 'actions-runner-acme'));
|
|
1987
|
+
jetpack.write(shortcut, '@echo off\r\n');
|
|
1988
|
+
|
|
1989
|
+
let threw;
|
|
1990
|
+
try {
|
|
1991
|
+
await runner({
|
|
1992
|
+
_: ['runner', 'restart'], _home: home, _env: {}, _logger: logger, _interactive: false, _prompt: {},
|
|
1993
|
+
_listeners: (dir) => { killed.push(dir); return []; },
|
|
1994
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 3333 }; },
|
|
1995
|
+
});
|
|
1996
|
+
} catch (e) { threw = e; }
|
|
1997
|
+
|
|
1998
|
+
ctx.expect(threw).toBeDefined();
|
|
1999
|
+
ctx.expect(threw.message).toContain('GH_TOKEN');
|
|
2000
|
+
// Nothing was stopped and nothing was started: the box is as it was.
|
|
2001
|
+
ctx.expect(logged.map((l) => l[1]).join('\n').includes('already stopped')).toBe(false);
|
|
2002
|
+
ctx.expect(spawned).toEqual([]);
|
|
2003
|
+
} finally {
|
|
2004
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2005
|
+
process.exitCode = origExit;
|
|
2006
|
+
jetpack.remove(shortcut);
|
|
2007
|
+
jetpack.remove(home);
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
name: '`restart` waits for the killed listener to actually go, then starts',
|
|
2013
|
+
run: async (ctx) => {
|
|
2014
|
+
// taskkill returns before the process leaves the table. A start that
|
|
2015
|
+
// raced it read the dying listener as "already running" and called the
|
|
2016
|
+
// box online while it was on its way down.
|
|
2017
|
+
const fs = require('fs');
|
|
2018
|
+
const os = require('os');
|
|
2019
|
+
const jetpack = require('fs-jetpack');
|
|
2020
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2021
|
+
|
|
2022
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2023
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2024
|
+
const origExit = process.exitCode;
|
|
2025
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2026
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
2027
|
+
const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
|
|
2028
|
+
const logged = [];
|
|
2029
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
2030
|
+
const spawned = [];
|
|
2031
|
+
const waits = [];
|
|
2032
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
2033
|
+
try {
|
|
2034
|
+
jetpack.dir(path.join(home, 'actions-runner-acme'));
|
|
2035
|
+
jetpack.write(shortcut, '@echo off\r\n');
|
|
2036
|
+
|
|
2037
|
+
// Alive for the first two polls, gone on the third.
|
|
2038
|
+
let looks = 0;
|
|
2039
|
+
await runner({
|
|
2040
|
+
_: ['runner', 'restart'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
2041
|
+
_delay: async (ms) => { waits.push(ms); },
|
|
2042
|
+
_listeners: () => (++looks <= 2 ? [{ pid: 909, sessionId: 1, execPath: 'C:/x/Runner.Listener.exe' }] : []),
|
|
2043
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 4444 }; },
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
ctx.expect(waits.length).toBe(2); // it polled rather than trusting taskkill
|
|
2047
|
+
ctx.expect(waits[0]).toBe(250);
|
|
2048
|
+
ctx.expect(spawned).toEqual([path.join(home, 'actions-runner-acme')]);
|
|
2049
|
+
const lines = logged.map((l) => l[1]).join('\n');
|
|
2050
|
+
ctx.expect(lines).toContain('PID=4444'); // the fresh listener, not the dying one
|
|
2051
|
+
ctx.expect(lines.includes('already running')).toBe(false);
|
|
2052
|
+
ctx.expect(process.exitCode).toBe(origExit);
|
|
2053
|
+
} finally {
|
|
2054
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2055
|
+
process.exitCode = origExit;
|
|
2056
|
+
jetpack.remove(shortcut);
|
|
2057
|
+
jetpack.remove(home);
|
|
2058
|
+
}
|
|
2059
|
+
},
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
name: 'a listener that outlives the stop fails the `restart` loudly',
|
|
2063
|
+
run: async (ctx) => {
|
|
2064
|
+
// The poll is bounded: a listener still standing after it is reported,
|
|
2065
|
+
// never started over the top of, and the run exits non-zero.
|
|
2066
|
+
const fs = require('fs');
|
|
2067
|
+
const os = require('os');
|
|
2068
|
+
const jetpack = require('fs-jetpack');
|
|
2069
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2070
|
+
|
|
2071
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2072
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2073
|
+
const origExit = process.exitCode;
|
|
2074
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2075
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
2076
|
+
const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
|
|
2077
|
+
const logged = [];
|
|
2078
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
2079
|
+
const spawned = [];
|
|
2080
|
+
const waits = [];
|
|
2081
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
2082
|
+
try {
|
|
2083
|
+
jetpack.dir(path.join(home, 'actions-runner-acme'));
|
|
2084
|
+
jetpack.write(shortcut, '@echo off\r\n');
|
|
2085
|
+
|
|
2086
|
+
await runner({
|
|
2087
|
+
_: ['runner', 'restart'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
2088
|
+
_delay: async (ms) => { waits.push(ms); },
|
|
2089
|
+
_listeners: () => [{ pid: 909, sessionId: 1, execPath: 'C:/x/Runner.Listener.exe' }],
|
|
2090
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 4444 }; },
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
ctx.expect(waits.length).toBe(20); // bounded, not forever
|
|
2094
|
+
ctx.expect(process.exitCode).toBe(1);
|
|
2095
|
+
const warns = logged.filter((l) => l[0] === 'warn').map((l) => l[1]).join('\n');
|
|
2096
|
+
ctx.expect(warns).toContain('still has a listener after stop');
|
|
2097
|
+
ctx.expect(warns).toContain('PID=909');
|
|
2098
|
+
ctx.expect(spawned).toEqual([]); // never a second one on that registration
|
|
2099
|
+
} finally {
|
|
2100
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2101
|
+
process.exitCode = origExit;
|
|
2102
|
+
jetpack.remove(shortcut);
|
|
2103
|
+
jetpack.remove(home);
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
name: '`start` never takes the terminal over: no inherited stdio in it',
|
|
2109
|
+
run: (ctx) => {
|
|
2110
|
+
// Source-text guard on the branch that was deleted. A reintroduced
|
|
2111
|
+
// foreground start would hang the box's install and every script.
|
|
2112
|
+
const fs = require('fs');
|
|
2113
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
2114
|
+
const from = src.indexOf('async function startServices');
|
|
2115
|
+
const to = src.indexOf('async function restartServices');
|
|
2116
|
+
ctx.expect(from > 0 && to > from).toBe(true);
|
|
2117
|
+
const startSrc = src.slice(from, to);
|
|
2118
|
+
ctx.expect(startSrc.includes("stdio: 'inherit'")).toBe(false);
|
|
2119
|
+
ctx.expect(startSrc.includes('spawnSync')).toBe(false);
|
|
2120
|
+
// ...and install hands off to it rather than foregrounding one org.
|
|
2121
|
+
ctx.expect(src).toContain('await startServices({ ...options, _home: home })');
|
|
2122
|
+
|
|
2123
|
+
// The spawn's own options, pinned the same way: the listener is handed
|
|
2124
|
+
// the runner's private HOME ([#807](https://github.com/Omega-JS-Stack/omega/issues/807))
|
|
2125
|
+
// so its jobs never read the box's symlinked `~/.gitconfig`, and the
|
|
2126
|
+
// detached child does not depend on the `.env` read alone. Only a real
|
|
2127
|
+
// spawn on Windows proves it, which no case may do, so the wiring is
|
|
2128
|
+
// read off the source.
|
|
2129
|
+
const spawnSrc = src.slice(src.indexOf('function spawnRunnerDetached'), src.indexOf('function listRunnerListenerProcessesUnder'));
|
|
2130
|
+
ctx.expect(spawnSrc.length).toBeGreaterThan(0);
|
|
2131
|
+
ctx.expect(spawnSrc).toContain('HOME: runnerPrivateHome(');
|
|
2132
|
+
},
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
name: 'a live listener belongs to the org dir it is INSIDE, never the one its name starts with',
|
|
2136
|
+
run: async (ctx) => {
|
|
2137
|
+
// `acme` and `acme-2` on one box: a bare prefix match read acme-2's
|
|
2138
|
+
// listener as acme's, so acme was skipped as "already running" and
|
|
2139
|
+
// never came up.
|
|
2140
|
+
const fs = require('fs');
|
|
2141
|
+
const os = require('os');
|
|
2142
|
+
const jetpack = require('fs-jetpack');
|
|
2143
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2144
|
+
|
|
2145
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2146
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2147
|
+
const origExit = process.exitCode;
|
|
2148
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2149
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
2150
|
+
const orgs = ['acme', 'acme-2'];
|
|
2151
|
+
const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
|
|
2152
|
+
const logged = [];
|
|
2153
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
2154
|
+
const spawned = [];
|
|
2155
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
2156
|
+
try {
|
|
2157
|
+
for (const org of orgs) jetpack.dir(path.join(home, `actions-runner-${org}`));
|
|
2158
|
+
for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
|
|
2159
|
+
|
|
2160
|
+
// The rule itself, on the two names.
|
|
2161
|
+
const longDir = path.join(home, 'actions-runner-acme-2');
|
|
2162
|
+
const shortDir = path.join(home, 'actions-runner-acme');
|
|
2163
|
+
const live = { pid: 55, sessionId: 1, execPath: path.join(longDir, 'bin', 'Runner.Listener.exe') };
|
|
2164
|
+
ctx.expect(runner.isListenerUnder(live.execPath, longDir)).toBe(true);
|
|
2165
|
+
ctx.expect(runner.isListenerUnder(live.execPath, shortDir)).toBe(false);
|
|
2166
|
+
// Windows spells its paths the other way, whichever OS is asking.
|
|
2167
|
+
ctx.expect(runner.isListenerUnder('C:\\r\\actions-runner-acme-2\\bin\\Runner.Listener.exe', 'C:\\r\\actions-runner-acme')).toBe(false);
|
|
2168
|
+
ctx.expect(runner.isListenerUnder('C:\\r\\actions-runner-acme\\bin\\Runner.Listener.exe', 'C:\\r\\actions-runner-acme')).toBe(true);
|
|
2169
|
+
|
|
2170
|
+
// …and `start` reading through that rule brings the short org up.
|
|
2171
|
+
await runner({
|
|
2172
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
2173
|
+
_listeners: (dir) => [live].filter((p) => runner.isListenerUnder(p.execPath, dir)),
|
|
2174
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 6060 }; },
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
ctx.expect(spawned).toEqual([shortDir]);
|
|
2178
|
+
ctx.expect(process.exitCode).toBe(origExit);
|
|
2179
|
+
} finally {
|
|
2180
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2181
|
+
process.exitCode = origExit;
|
|
2182
|
+
for (const file of shortcuts) jetpack.remove(file);
|
|
2183
|
+
jetpack.remove(home);
|
|
2184
|
+
}
|
|
2185
|
+
},
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
name: '`start` warns about a session-0 listener, skips it, and still exits 0',
|
|
2189
|
+
run: async (ctx) => {
|
|
2190
|
+
// A session-0 listener is alive but cannot see the cert store, so it
|
|
2191
|
+
// fails every job. `start` says so and names `restart`; killing is
|
|
2192
|
+
// `stop`'s job, never a start's.
|
|
2193
|
+
const fs = require('fs');
|
|
2194
|
+
const os = require('os');
|
|
2195
|
+
const jetpack = require('fs-jetpack');
|
|
2196
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2197
|
+
|
|
2198
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2199
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2200
|
+
const origExit = process.exitCode;
|
|
2201
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2202
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
2203
|
+
const shortcut = path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}acme.cmd`);
|
|
2204
|
+
const logged = [];
|
|
2205
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
2206
|
+
const spawned = [];
|
|
2207
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
2208
|
+
try {
|
|
2209
|
+
jetpack.dir(path.join(home, 'actions-runner-acme'));
|
|
2210
|
+
jetpack.write(shortcut, '@echo off\r\n');
|
|
2211
|
+
|
|
2212
|
+
await runner({
|
|
2213
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
2214
|
+
_listeners: () => [{ pid: 12, sessionId: 0, execPath: 'C:/r/actions-runner-acme/bin/Runner.Listener.exe' }],
|
|
2215
|
+
_spawn: (dir) => { spawned.push(dir); return { ok: true, pid: 7070 }; },
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
const warns = logged.filter((l) => l[0] === 'warn').map((l) => l[1]).join('\n');
|
|
2219
|
+
ctx.expect(warns).toContain('SESSION 0');
|
|
2220
|
+
ctx.expect(warns).toContain('PID=12');
|
|
2221
|
+
ctx.expect(warns).toContain('runner restart');
|
|
2222
|
+
ctx.expect(spawned).toEqual([]); // skipped, never killed and never doubled
|
|
2223
|
+
ctx.expect(process.exitCode).toBe(origExit); // still a clean run
|
|
2224
|
+
} finally {
|
|
2225
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2226
|
+
process.exitCode = origExit;
|
|
2227
|
+
jetpack.remove(shortcut);
|
|
2228
|
+
jetpack.remove(home);
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2233
|
+
name: '`runner config` saves the retyped keys BEFORE the orgs question, and names an org the token no longer administers',
|
|
2234
|
+
run: async (ctx) => {
|
|
2235
|
+
// The orgs step can refuse (nothing ticked, a token that cannot list,
|
|
2236
|
+
// a Ctrl-C). None of that may cost the operator the keys they just
|
|
2237
|
+
// retyped — those are written the moment they are answered.
|
|
2238
|
+
const fs = require('fs');
|
|
2239
|
+
const os = require('os');
|
|
2240
|
+
const jetpack = require('fs-jetpack');
|
|
2241
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2242
|
+
|
|
2243
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2244
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2245
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2246
|
+
try {
|
|
2247
|
+
jetpack.write(path.join(home, '.env'), [
|
|
2248
|
+
'GH_TOKEN=ghp_old',
|
|
2249
|
+
'OMEGA_RUNNER_ORGS=acme,ghost',
|
|
2250
|
+
`WIN_EV_TOKEN_PATH=${'a'.repeat(40)}`,
|
|
2251
|
+
'WIN_CSC_KEY_PASSWORD=old-pin',
|
|
2252
|
+
'SIGNTOOL_PATH=C:/sdk/signtool.exe',
|
|
2253
|
+
'WIN_TIMESTAMP_URL=',
|
|
2254
|
+
].join('\n'));
|
|
2255
|
+
|
|
2256
|
+
const logged = [];
|
|
2257
|
+
const logger = { log: (m) => logged.push(['log', String(m)]), warn: (m) => logged.push(['warn', String(m)]), error: (m) => logged.push(['error', String(m)]) };
|
|
2258
|
+
const prompt = {
|
|
2259
|
+
input: async ({ message, default: current }) => (message.startsWith('WIN_EV_TOKEN_PATH') ? 'b'.repeat(40) : (current || '')),
|
|
2260
|
+
password: async ({ message }) => (message.startsWith('GH_TOKEN') ? 'ghp_new' : ''),
|
|
2261
|
+
checkbox: async () => { throw new Error('the operator hit Ctrl-C'); },
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
let threw;
|
|
2265
|
+
try {
|
|
2266
|
+
await runner({
|
|
2267
|
+
_: ['runner', 'config'],
|
|
2268
|
+
_home: home, _env: {}, _logger: logger, _prompt: prompt, _interactive: true,
|
|
2269
|
+
_discoverOrgs: async () => ['acme', 'zed'],
|
|
2270
|
+
});
|
|
2271
|
+
} catch (e) { threw = e; }
|
|
2272
|
+
ctx.expect(threw).toBeDefined();
|
|
2273
|
+
|
|
2274
|
+
// Everything answered before the refusal survived it.
|
|
2275
|
+
const written = jetpack.read(path.join(home, '.env'));
|
|
2276
|
+
ctx.expect(written).toContain('GH_TOKEN="ghp_new"');
|
|
2277
|
+
ctx.expect(written).toContain(`WIN_EV_TOKEN_PATH="${'b'.repeat(40)}"`);
|
|
2278
|
+
ctx.expect(written).toContain('WIN_CSC_KEY_PASSWORD="old-pin"'); // Enter kept it, rewritten in place quoted
|
|
2279
|
+
ctx.expect(written).toContain('OMEGA_RUNNER_ORGS=acme,ghost'); // the question never finished
|
|
2280
|
+
|
|
2281
|
+
// The saved list named an org the token cannot admin any more: said once, before the checkbox.
|
|
2282
|
+
const warns = logged.filter((l) => l[0] === 'warn').map((l) => l[1]).join('\n');
|
|
2283
|
+
ctx.expect(warns).toContain('ghost');
|
|
2284
|
+
ctx.expect(warns).toContain('no longer administers');
|
|
2285
|
+
} finally {
|
|
2286
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2287
|
+
jetpack.remove(home);
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
name: 'every runner subcommand tees its output to <runner home>/logs/runner.log, appending',
|
|
2293
|
+
run: async (ctx) => {
|
|
2294
|
+
// The box keeps its own record of what the runner commands did, beside
|
|
2295
|
+
// the install. It APPENDS: a `runner start` parent holds the file while
|
|
2296
|
+
// every `sign-windows` its listener spawns writes to the same one, and a
|
|
2297
|
+
// truncating open would throw the parent's trail away.
|
|
2298
|
+
const fs = require('fs');
|
|
2299
|
+
const os = require('os');
|
|
2300
|
+
const jetpack = require('fs-jetpack');
|
|
2301
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2302
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2303
|
+
|
|
2304
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2305
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2306
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2307
|
+
try {
|
|
2308
|
+
const logFile = runnerEnv.runnerLogFile(home);
|
|
2309
|
+
ctx.expect(logFile).toBe(path.join(home, 'logs', 'runner.log'));
|
|
2310
|
+
|
|
2311
|
+
jetpack.write(path.join(home, '.env'), [
|
|
2312
|
+
'GH_TOKEN="ghp_from_the_box"',
|
|
2313
|
+
'OMEGA_RUNNER_ORGS="acme"',
|
|
2314
|
+
`WIN_EV_TOKEN_PATH="${'a'.repeat(40)}"`,
|
|
2315
|
+
'WIN_CSC_KEY_PASSWORD="1234"',
|
|
2316
|
+
'SIGNTOOL_PATH="C:/sdk/signtool.exe"',
|
|
2317
|
+
].join('\n'));
|
|
2318
|
+
|
|
2319
|
+
// Two runs of a real subcommand, on a runner (CI is exactly when the
|
|
2320
|
+
// box's record is wanted — the log lives in the runner home, never in
|
|
2321
|
+
// a workspace).
|
|
2322
|
+
const walk = () => runner({
|
|
2323
|
+
_: ['runner', 'config'],
|
|
2324
|
+
_home: home, _env: { GITHUB_ACTIONS: 'true' }, _interactive: true,
|
|
2325
|
+
_prompt: {
|
|
2326
|
+
input: async ({ default: current }) => current || '',
|
|
2327
|
+
password: async () => '',
|
|
2328
|
+
checkbox: async () => ['acme'],
|
|
2329
|
+
},
|
|
2330
|
+
_discoverOrgs: async () => ['acme'],
|
|
2331
|
+
});
|
|
2332
|
+
await walk();
|
|
2333
|
+
await walk();
|
|
2334
|
+
|
|
2335
|
+
const written = jetpack.read(logFile);
|
|
2336
|
+
ctx.expect(written).toContain("This box's signing configuration");
|
|
2337
|
+
ctx.expect(written).toContain('Saved to');
|
|
2338
|
+
ctx.expect((written.match(/# omega log/g) || []).length).toBe(2); // both runs kept
|
|
2339
|
+
} finally {
|
|
2340
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2341
|
+
jetpack.remove(home);
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
name: 'off Windows a subcommand refuses BEFORE it writes: no runner home left in the cwd',
|
|
2347
|
+
run: async (ctx) => {
|
|
2348
|
+
// `npx omega runner status` on a Mac used to drop a `.gh-runners/` next
|
|
2349
|
+
// to whatever you were standing in, because the log attached before the
|
|
2350
|
+
// platform gate ran. The refusal now writes nothing at all.
|
|
2351
|
+
if (process.platform === 'win32') return ctx.skip('the platform gate passes on the box — there is no refusal to prove');
|
|
2352
|
+
|
|
2353
|
+
const fs = require('fs');
|
|
2354
|
+
const os = require('os');
|
|
2355
|
+
const jetpack = require('fs-jetpack');
|
|
2356
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2357
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2358
|
+
|
|
2359
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2360
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2361
|
+
delete process.env.OMEGA_RUNNER_FORCE;
|
|
2362
|
+
try {
|
|
2363
|
+
for (const sub of ['status', 'install', 'config', 'start']) {
|
|
2364
|
+
let threw;
|
|
2365
|
+
try {
|
|
2366
|
+
await runner({ _: ['runner', sub], _home: home, _interactive: false });
|
|
2367
|
+
} catch (e) { threw = e; }
|
|
2368
|
+
ctx.expect(threw).toBeDefined();
|
|
2369
|
+
ctx.expect(threw.message).toMatch(/only runs on Windows/);
|
|
2370
|
+
}
|
|
2371
|
+
ctx.expect(jetpack.exists(runnerEnv.runnerLogFile(home))).toBe(false);
|
|
2372
|
+
ctx.expect(jetpack.exists(path.join(home, 'logs'))).toBe(false);
|
|
2373
|
+
|
|
2374
|
+
// An unknown subcommand still says so — the platform gate never speaks
|
|
2375
|
+
// for a name no subcommand answers to.
|
|
2376
|
+
let threw;
|
|
2377
|
+
try {
|
|
2378
|
+
await runner({ _: ['runner', 'banana'], _home: home });
|
|
2379
|
+
} catch (e) { threw = e; }
|
|
2380
|
+
ctx.expect(threw.message).toMatch(/Unknown runner subcommand/);
|
|
2381
|
+
ctx.expect(jetpack.exists(path.join(home, 'logs'))).toBe(false);
|
|
2382
|
+
} finally {
|
|
2383
|
+
if (origForce !== undefined) process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2384
|
+
jetpack.remove(home);
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
name: 'install runs the same full walk config does, and pays for the org walk once',
|
|
2390
|
+
run: (ctx) => {
|
|
2391
|
+
const fs = require('fs');
|
|
2392
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'), 'utf8');
|
|
2393
|
+
const install = src.slice(src.indexOf('async function install('), src.indexOf('// ─── config ─'));
|
|
2394
|
+
ctx.expect(install).toContain('reconfigureRunnerEnv(');
|
|
2395
|
+
ctx.expect(install).toMatch(/discoverOrgs:\s+discoverAdminOrgs/);
|
|
2396
|
+
// The question already listed the admin orgs; install reuses that list
|
|
2397
|
+
// and only walks GitHub itself when the question never ran.
|
|
2398
|
+
ctx.expect(install).toContain('adminOrgs || await discoverAdminOrgs()');
|
|
2399
|
+
// One walk, one path: the missing-keys-only step is not install's any more.
|
|
2400
|
+
ctx.expect(install).not.toContain('ensureRunnerConfig');
|
|
2401
|
+
ctx.expect(src).not.toContain('askOrgs');
|
|
2402
|
+
},
|
|
2403
|
+
},
|
|
2404
|
+
{
|
|
2405
|
+
name: 'the runner gets a private HOME: `<runner home>/home` with a real .gitconfig, delivered by the runner dir\'s .env',
|
|
2406
|
+
run: (ctx) => {
|
|
2407
|
+
// Why it exists ([#807](https://github.com/Omega-JS-Stack/omega/issues/807)):
|
|
2408
|
+
// actions/checkout copies `$HOME/.gitconfig` into a temporary HOME with
|
|
2409
|
+
// @actions/io, which recreates a SYMLINK source as a Windows junction,
|
|
2410
|
+
// and a junction to a FILE is invalid. The box's `~/.gitconfig` is a
|
|
2411
|
+
// dotfiles symlink and must stay one, so the runner is pointed at a
|
|
2412
|
+
// HOME of its own instead, holding a real file.
|
|
2413
|
+
const fs = require('fs');
|
|
2414
|
+
const os = require('os');
|
|
2415
|
+
const jetpack = require('fs-jetpack');
|
|
2416
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2417
|
+
|
|
2418
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2419
|
+
const runnerDir = path.join(home, 'actions-runner-acme');
|
|
2420
|
+
try {
|
|
2421
|
+
jetpack.dir(runnerDir);
|
|
2422
|
+
ctx.expect(runnerEnv.runnerPrivateHome(home)).toBe(path.join(home, 'home'));
|
|
2423
|
+
|
|
2424
|
+
const result = runnerEnv.ensureRunnerPrivateHome(home);
|
|
2425
|
+
ctx.expect(result.home).toBe(path.join(home, 'home'));
|
|
2426
|
+
ctx.expect(result.written).toBe(true);
|
|
2427
|
+
// The exact bytes, hard-coded: the ONE setting the workflows rely on.
|
|
2428
|
+
ctx.expect(fs.readFileSync(path.join(home, 'home', '.gitconfig'), 'utf8')).toBe('[core]\n\tautocrlf = false\n');
|
|
2429
|
+
|
|
2430
|
+
// The listener reads `<runner dir>/.env` at startup and hands it to
|
|
2431
|
+
// every job, so one line delivers it to the detached spawn and the
|
|
2432
|
+
// Startup shortcut alike. Bare, never quoted: the runner takes the
|
|
2433
|
+
// rest of the line as the value, quotes included.
|
|
2434
|
+
const file = runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: result.home });
|
|
2435
|
+
ctx.expect(file).toBe(path.join(runnerDir, '.env'));
|
|
2436
|
+
ctx.expect(fs.readFileSync(file, 'utf8')).toBe(`HOME=${path.join(home, 'home')}\n`);
|
|
2437
|
+
} finally {
|
|
2438
|
+
jetpack.remove(home);
|
|
2439
|
+
}
|
|
2440
|
+
},
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
name: 'a second ensure changes nothing: the home, the .gitconfig and the .env come back byte-equal',
|
|
2444
|
+
run: (ctx) => {
|
|
2445
|
+
const fs = require('fs');
|
|
2446
|
+
const os = require('os');
|
|
2447
|
+
const jetpack = require('fs-jetpack');
|
|
2448
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2449
|
+
|
|
2450
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2451
|
+
const runnerDir = path.join(home, 'actions-runner-acme');
|
|
2452
|
+
try {
|
|
2453
|
+
jetpack.dir(runnerDir);
|
|
2454
|
+
const first = runnerEnv.ensureRunnerPrivateHome(home);
|
|
2455
|
+
runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: first.home });
|
|
2456
|
+
const gitconfig = fs.readFileSync(path.join(first.home, '.gitconfig'), 'utf8');
|
|
2457
|
+
const env = fs.readFileSync(path.join(runnerDir, '.env'), 'utf8');
|
|
2458
|
+
|
|
2459
|
+
const second = runnerEnv.ensureRunnerPrivateHome(home);
|
|
2460
|
+
runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: second.home });
|
|
2461
|
+
|
|
2462
|
+
ctx.expect(second.written).toBe(false);
|
|
2463
|
+
ctx.expect(fs.readFileSync(path.join(first.home, '.gitconfig'), 'utf8')).toBe(gitconfig);
|
|
2464
|
+
ctx.expect(fs.readFileSync(path.join(runnerDir, '.env'), 'utf8')).toBe(env);
|
|
2465
|
+
} finally {
|
|
2466
|
+
jetpack.remove(home);
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
},
|
|
2470
|
+
{
|
|
2471
|
+
name: 'an existing runner .env keeps its other lines and gains exactly ONE HOME line, in the endings it already uses',
|
|
2472
|
+
run: (ctx) => {
|
|
2473
|
+
// The file GitHub documents for proxy settings. A box that carries one
|
|
2474
|
+
// must keep it: this write adds a line, it does not own the file.
|
|
2475
|
+
const fs = require('fs');
|
|
2476
|
+
const os = require('os');
|
|
2477
|
+
const jetpack = require('fs-jetpack');
|
|
2478
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2479
|
+
|
|
2480
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2481
|
+
const runnerDir = path.join(home, 'actions-runner-acme');
|
|
2482
|
+
try {
|
|
2483
|
+
jetpack.dir(runnerDir);
|
|
2484
|
+
fs.writeFileSync(path.join(runnerDir, '.env'), 'https_proxy=http://proxy:8080\r\nHOME=C:\\stale\r\nno_proxy=localhost\r\n');
|
|
2485
|
+
|
|
2486
|
+
const privateHome = runnerEnv.runnerPrivateHome(home);
|
|
2487
|
+
runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: privateHome });
|
|
2488
|
+
const contents = fs.readFileSync(path.join(runnerDir, '.env'), 'utf8');
|
|
2489
|
+
|
|
2490
|
+
ctx.expect(contents).toBe(`https_proxy=http://proxy:8080\r\nHOME=${privateHome}\r\nno_proxy=localhost\r\n`);
|
|
2491
|
+
ctx.expect(contents.split(/\r?\n/).filter((line) => line.startsWith('HOME=')).length).toBe(1);
|
|
2492
|
+
} finally {
|
|
2493
|
+
jetpack.remove(home);
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
name: 'a runner .env with no HOME line gains one on the line after the last, never after a blank one',
|
|
2499
|
+
run: (ctx) => {
|
|
2500
|
+
// The file ends with a newline, as a written file does. Splitting that
|
|
2501
|
+
// leaves a trailing empty element, and appending after it puts the key
|
|
2502
|
+
// below a blank line: legal to read back, but not a file anyone wants
|
|
2503
|
+
// to look at, and it grows a second blank line per rewrite.
|
|
2504
|
+
const fs = require('fs');
|
|
2505
|
+
const os = require('os');
|
|
2506
|
+
const jetpack = require('fs-jetpack');
|
|
2507
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2508
|
+
|
|
2509
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2510
|
+
const runnerDir = path.join(home, 'actions-runner-acme');
|
|
2511
|
+
try {
|
|
2512
|
+
jetpack.dir(runnerDir);
|
|
2513
|
+
fs.writeFileSync(path.join(runnerDir, '.env'), 'https_proxy=http://proxy:8080\r\nno_proxy=localhost\r\n');
|
|
2514
|
+
|
|
2515
|
+
const privateHome = runnerEnv.runnerPrivateHome(home);
|
|
2516
|
+
runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: privateHome });
|
|
2517
|
+
const first = fs.readFileSync(path.join(runnerDir, '.env'), 'utf8');
|
|
2518
|
+
ctx.expect(first).toBe(`https_proxy=http://proxy:8080\r\nno_proxy=localhost\r\nHOME=${privateHome}\r\n`);
|
|
2519
|
+
|
|
2520
|
+
// And a second call is the same bytes: nothing appended, no line grown.
|
|
2521
|
+
runnerEnv.ensureRunnerDirEnv(runnerDir, { HOME: privateHome });
|
|
2522
|
+
ctx.expect(fs.readFileSync(path.join(runnerDir, '.env'), 'utf8')).toBe(first);
|
|
2523
|
+
} finally {
|
|
2524
|
+
jetpack.remove(home);
|
|
2525
|
+
}
|
|
2526
|
+
},
|
|
2527
|
+
},
|
|
2528
|
+
{
|
|
2529
|
+
name: 'a .gitconfig that is not the one this command writes is LEFT alone, and said so',
|
|
2530
|
+
run: (ctx) => {
|
|
2531
|
+
// A human may have edited it on the box. Overwriting someone's file to
|
|
2532
|
+
// deliver one setting is never the trade.
|
|
2533
|
+
const fs = require('fs');
|
|
2534
|
+
const os = require('os');
|
|
2535
|
+
const jetpack = require('fs-jetpack');
|
|
2536
|
+
const runnerEnv = require(path.join(__dirname, '..', '..', '..', 'utils', 'runner-env.js'));
|
|
2537
|
+
|
|
2538
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2539
|
+
const mine = '[core]\n\tautocrlf = false\n[user]\n\tname = The Box\n';
|
|
2540
|
+
const logged = [];
|
|
2541
|
+
try {
|
|
2542
|
+
jetpack.dir(runnerEnv.runnerPrivateHome(home));
|
|
2543
|
+
fs.writeFileSync(path.join(runnerEnv.runnerPrivateHome(home), '.gitconfig'), mine);
|
|
2544
|
+
|
|
2545
|
+
const result = runnerEnv.ensureRunnerPrivateHome(home, { logger: { log: (m) => logged.push(String(m)), warn: (m) => logged.push(String(m)) } });
|
|
2546
|
+
|
|
2547
|
+
ctx.expect(result.written).toBe(false);
|
|
2548
|
+
ctx.expect(fs.readFileSync(path.join(result.home, '.gitconfig'), 'utf8')).toBe(mine);
|
|
2549
|
+
ctx.expect(logged.join('\n')).toContain(path.join(result.home, '.gitconfig'));
|
|
2550
|
+
} finally {
|
|
2551
|
+
jetpack.remove(home);
|
|
2552
|
+
}
|
|
2553
|
+
},
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
name: '`start` heals a private HOME the install never had, for every registered org, before a listener comes up',
|
|
2557
|
+
run: async (ctx) => {
|
|
2558
|
+
// A box installed before #807 has no private home and no HOME line, and
|
|
2559
|
+
// the first job it picks up dies in `actions/checkout`. The preflight
|
|
2560
|
+
// writes both, so bringing the box online is the whole fix.
|
|
2561
|
+
const fs = require('fs');
|
|
2562
|
+
const os = require('os');
|
|
2563
|
+
const jetpack = require('fs-jetpack');
|
|
2564
|
+
const runner = require(path.join(__dirname, '..', '..', '..', 'commands', 'runner.js'));
|
|
2565
|
+
|
|
2566
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'omega-runner-home-'));
|
|
2567
|
+
const origForce = process.env.OMEGA_RUNNER_FORCE;
|
|
2568
|
+
process.env.OMEGA_RUNNER_FORCE = '1';
|
|
2569
|
+
const hostPrefix = `omega-runner-${os.hostname().toLowerCase()}-`;
|
|
2570
|
+
const orgs = ['acme', 'zeta'];
|
|
2571
|
+
const shortcuts = orgs.map((org) => path.join(SCRATCH_STARTUP_DIR, `${hostPrefix}${org}.cmd`));
|
|
2572
|
+
const logged = [];
|
|
2573
|
+
const logger = { log: (m) => logged.push(String(m)), warn: (m) => logged.push(String(m)), error: (m) => logged.push(String(m)) };
|
|
2574
|
+
const filled = { GH_TOKEN: 'ghp_from_the_box', WIN_EV_TOKEN_PATH: 'a'.repeat(40), WIN_CSC_KEY_PASSWORD: '1234', SIGNTOOL_PATH: 'C:/sdk/signtool.exe' };
|
|
2575
|
+
try {
|
|
2576
|
+
for (const org of orgs) jetpack.write(path.join(home, `actions-runner-${org}`, 'config.cmd'), '@echo off\r\n');
|
|
2577
|
+
for (const file of shortcuts) jetpack.write(file, '@echo off\r\n');
|
|
2578
|
+
ctx.expect(jetpack.exists(path.join(home, 'home'))).toBe(false);
|
|
2579
|
+
|
|
2580
|
+
await runner({
|
|
2581
|
+
_: ['runner', 'start'], _home: home, _env: filled, _logger: logger, _interactive: false, _prompt: {},
|
|
2582
|
+
_listeners: () => [],
|
|
2583
|
+
_spawn: () => ({ ok: true, pid: 1000 }),
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
ctx.expect(fs.readFileSync(path.join(home, 'home', '.gitconfig'), 'utf8')).toBe('[core]\n\tautocrlf = false\n');
|
|
2587
|
+
for (const org of orgs) {
|
|
2588
|
+
ctx.expect(fs.readFileSync(path.join(home, `actions-runner-${org}`, '.env'), 'utf8')).toBe(`HOME=${path.join(home, 'home')}\n`);
|
|
2589
|
+
ctx.expect(logged.join('\n')).toContain(`HOME for ${org}: ${path.join(home, 'home')}`);
|
|
2590
|
+
}
|
|
2591
|
+
} finally {
|
|
2592
|
+
if (origForce === undefined) delete process.env.OMEGA_RUNNER_FORCE; else process.env.OMEGA_RUNNER_FORCE = origForce;
|
|
2593
|
+
for (const file of shortcuts) jetpack.remove(file);
|
|
2594
|
+
jetpack.remove(home);
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
},
|
|
2598
|
+
],
|
|
2599
|
+
});
|