├── .gitattributes ├── .github ├── dependabot.yml ├── linters │ ├── .markdown-lint.yml │ ├── .yaml-lint.yml │ └── tsconfig.json └── workflows │ ├── check-dist.yml │ └── test.yml ├── .gitignore ├── .mergify.yml ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __tests__ ├── .env.template ├── __snapshots__ │ ├── config.test.ts.snap │ └── main.test.ts.snap ├── config.test.ts ├── main.test.ts └── memory.test.ts ├── action.yml ├── dist ├── 356.index.js ├── 356.index.js.map ├── index.js ├── index.js.map ├── licenses.txt ├── proto │ └── channelz.proto └── sourcemap-register.js ├── eslint.config.mjs ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── config.ts ├── gh.ts ├── main.ts ├── memory.ts └── service-account-json.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | # Unordered list style 2 | MD004: 3 | style: dash 4 | 5 | # Ordered list item prefix 6 | MD029: 7 | style: one 8 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | document-end: disable 3 | document-start: 4 | level: warning 5 | present: false 6 | line-length: 7 | level: warning 8 | max: 80 9 | allow-non-breakable-words: true 10 | allow-non-breakable-inline-mappings: true 11 | -------------------------------------------------------------------------------- /.github/linters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "noEmit": true 6 | }, 7 | "include": ["../../__tests__/**/*", "../../src/**/*"], 8 | "exclude": ["../../dist", "../../node_modules", "../../coverage", "*.json"] 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/check-dist.yml: -------------------------------------------------------------------------------- 1 | # `dist/index.js` is a special file in Actions. 2 | # When you reference an action with `uses:` in a workflow, 3 | # `index.js` is the code that will run. 4 | # For our project, we generate this file through a build process from other source files. 5 | # We need to make sure the checked-in `index.js` actually matches what we expect it to be. 6 | name: Check dist/ 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - '**.md' 14 | pull_request: 15 | paths-ignore: 16 | - '**.md' 17 | workflow_dispatch: 18 | 19 | jobs: 20 | check-dist: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Set Node.js 20.x 27 | uses: actions/setup-node@v4.4.0 28 | with: 29 | node-version: 20.x 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Rebuild the dist/ directory 35 | run: npm run build 36 | 37 | - name: Compare the expected and actual dist/ directories 38 | run: | 39 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 40 | echo "Detected uncommitted changes after build. See status below:" 41 | git diff 42 | exit 1 43 | fi 44 | id: diff 45 | 46 | # If index.js was different from expected, upload the expected version as an artifact 47 | - uses: actions/upload-artifact@v4 48 | if: ${{ failure() && steps.diff.conclusion == 'failure' }} 49 | with: 50 | name: dist 51 | path: dist/ 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'build-test' 2 | on: # rebuild any PRs and main branch changes 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/*' 8 | 9 | jobs: 10 | build: # make sure build/ci work properly 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: | 15 | npm install 16 | - run: | 17 | npm run all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | .idea -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | 3 | - name: Automatically approve and merge Dependabot PRs 4 | conditions: 5 | - base=main 6 | - author=dependabot[bot] 7 | - check-success=check-dist 8 | - check-success=build 9 | - -title~=(WIP|wip) 10 | - -label~=(blocked|do-not-merge) 11 | - -merged 12 | - -closed 13 | actions: 14 | review: 15 | type: APPROVE 16 | merge: 17 | method: squash 18 | 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "none", 10 | "bracketSpacing": true, 11 | "bracketSameLine": true, 12 | "arrowParens": "avoid", 13 | "proseWrap": "always", 14 | "htmlWhitespaceSensitivity": "css", 15 | "endOfLine": "lf" 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nikolay Matrosov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitHub Action to On-demand self-hosted YC runner for GitHub Actions. 2 | 3 | Start your Yandex Cloud self-hosted runner right before you need it. Run the job on it. Finally, stop it when you 4 | finish. And all this automatically as a part of your GitHub Actions workflow. 5 | 6 | **Table of Contents** 7 | 8 | 9 | 10 | - [Usage](#usage) 11 | - [Permissions](#permissions) 12 | - [License Summary](#license-summary) 13 | 14 | 15 | 16 | ## Usage 17 | 18 | ```yaml 19 | name: do-the-job 20 | on: pull_request 21 | jobs: 22 | start-runner: 23 | name: Start self-hosted YC runner 24 | runs-on: ubuntu-latest 25 | outputs: 26 | label: ${{ steps.start-yc-runner.outputs.label }} 27 | instance-id: ${{ steps.start-yc-runner.outputs.instance-id }} 28 | steps: 29 | - name: Start YC runner 30 | id: start-yc-runner 31 | uses: yc-actions/yc-github-runner@v2 32 | with: 33 | mode: start 34 | yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }} 35 | github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 36 | folder-id: b1g********* 37 | image-id: fd80********* 38 | cores: 2 39 | memory: 2GB 40 | core-fraction: 100 41 | subnet-id: e9b********* 42 | ttl: PT24H 43 | do-the-job: 44 | name: Do the job on the runner 45 | needs: start-runner # required to start the main job when the runner is ready 46 | runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner 47 | steps: 48 | - name: Hello World 49 | run: echo 'Hello World!' 50 | stop-runner: 51 | name: Stop self-hosted YC runner 52 | needs: 53 | - start-runner # required to get output from the start-runner job 54 | - do-the-job # required to wait when the main job is done 55 | runs-on: ubuntu-latest 56 | if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs 57 | steps: 58 | - name: Stop YC runner 59 | uses: yc-actions/yc-github-runner@v2 60 | with: 61 | mode: stop 62 | yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }} 63 | github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 64 | label: ${{ needs.start-runner.outputs.label }} 65 | instance-id: ${{ needs.start-runner.outputs.instance-id }} 66 | ``` 67 | 68 | One of `yc-sa-json-credentials`, `yc-iam-token` or `yc-sa-id` should be provided depending on the authentication method you 69 | want to use. The action will use the first one it finds. 70 | * `yc-sa-json-credentials` should contain JSON with authorized key for Service Account. More info 71 | in [Yandex Cloud IAM documentation](https://yandex.cloud/en/docs/iam/operations/authentication/manage-authorized-keys#cli_1). 72 | * `yc-iam-token` should contain IAM token. It can be obtained using `yc iam create-token` command or using 73 | [yc-actions/yc-iam-token-fed](https://github.com/yc-actions/yc-iam-token-fed) 74 | ```yaml 75 | - name: Get Yandex Cloud IAM token 76 | id: get-iam-token 77 | uses: docker://ghcr.io/yc-actions/yc-iam-token-fed:1.0.0 78 | with: 79 | yc-sa-id: aje*** 80 | ``` 81 | * `yc-sa-id` should contain Service Account ID. It can be obtained using `yc iam service-accounts list` command. It is 82 | used to exchange GitHub token for IAM token using Workload Identity Federation. More info in [Yandex Cloud IAM documentation](https://yandex.cloud/ru/docs/iam/concepts/workload-identity). 83 | 84 | 85 | See [action.yml](action.yml) for the full documentation for this action's inputs and outputs. 86 | 87 | ### TTL input 88 | If it is set, `expires` label will be added to the instance with the value of the current time plus TTL in seconds. 89 | Instance won't automatically be destroyed by Yandex.Cloud, you should handle it yourself. 90 | For example, by using Cron trigger that will call Cloud Function to destroy the instance. 91 | 92 | ## Permissions 93 | 94 | To perform this action, it is required that the service account on behalf of which we are acting has granted 95 | the `compute.admin` role or greater. 96 | 97 | ## License Summary 98 | 99 | This code is made available under the MIT license. 100 | -------------------------------------------------------------------------------- /__tests__/.env.template: -------------------------------------------------------------------------------- 1 | INPUT_YC-SA-JSON-CREDENTIALS= 2 | 3 | GITHUB_REPOSITORY= 4 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/config.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`add secondary disk without image-id throw error 1`] = `"Secondary disk image id is missing"`; 4 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/main.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cloud-init default 1`] = ` 4 | [ 5 | "#!/bin/bash", 6 | "mkdir actions-runner && cd actions-runner", 7 | "case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=\${ARCH}", 8 | "curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 9 | "tar xzf ./actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 10 | "export RUNNER_ALLOW_RUNASROOT=1", 11 | "./config.sh --url https://github.com/owner/repo --token githubRegistrationToken --labels label", 12 | "./run.sh", 13 | ] 14 | `; 15 | 16 | exports[`cloud-init with disable update 1`] = ` 17 | [ 18 | "#!/bin/bash", 19 | "mkdir actions-runner && cd actions-runner", 20 | "case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=\${ARCH}", 21 | "curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 22 | "tar xzf ./actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 23 | "export RUNNER_ALLOW_RUNASROOT=1", 24 | "./config.sh --url https://github.com/owner/repo --token githubRegistrationToken --labels label --disableupdate", 25 | "./run.sh", 26 | ] 27 | `; 28 | 29 | exports[`cloud-init with home dir 1`] = ` 30 | [ 31 | "#!/bin/bash", 32 | "cd "foo"", 33 | "export RUNNER_ALLOW_RUNASROOT=1", 34 | "./config.sh --url https://github.com/owner/repo --token githubRegistrationToken --labels label", 35 | "./run.sh", 36 | ] 37 | `; 38 | 39 | exports[`cloud-init with home dir and user and ssh key 1`] = ` 40 | [ 41 | "#cloud-config", 42 | "ssh_pwauth: 'no'", 43 | "users:", 44 | " - name: user", 45 | " sudo: ALL=(ALL) NOPASSWD:ALL", 46 | " shell: /bin/bash", 47 | " ssh_authorized_keys:", 48 | " - key", 49 | "runcmd:", 50 | " - cd "foo"", 51 | " - export RUNNER_ALLOW_RUNASROOT=1", 52 | " - >-", 53 | " ./config.sh --url https://github.com/owner/repo --token", 54 | " githubRegistrationToken --labels label", 55 | " - ./run.sh", 56 | "", 57 | ] 58 | `; 59 | 60 | exports[`cloud-init with user and ssh key 1`] = ` 61 | [ 62 | "#cloud-config", 63 | "ssh_pwauth: 'no'", 64 | "users:", 65 | " - name: user", 66 | " sudo: ALL=(ALL) NOPASSWD:ALL", 67 | " shell: /bin/bash", 68 | " ssh_authorized_keys:", 69 | " - key", 70 | "runcmd:", 71 | " - mkdir actions-runner && cd actions-runner", 72 | " - >-", 73 | " case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;;", 74 | " esac && export RUNNER_ARCH=\${ARCH}", 75 | " - >-", 76 | " curl -O -L", 77 | " https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 78 | " - tar xzf ./actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 79 | " - export RUNNER_ALLOW_RUNASROOT=1", 80 | " - >-", 81 | " ./config.sh --url https://github.com/owner/repo --token", 82 | " githubRegistrationToken --labels label", 83 | " - ./run.sh", 84 | "", 85 | ] 86 | `; 87 | 88 | exports[`cloud-init without disable update 1`] = ` 89 | [ 90 | "#!/bin/bash", 91 | "mkdir actions-runner && cd actions-runner", 92 | "case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=\${ARCH}", 93 | "curl -O -L https://github.com/actions/runner/releases/download/v2.299.1/actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 94 | "tar xzf ./actions-runner-linux-\${RUNNER_ARCH}-2.299.1.tar.gz", 95 | "export RUNNER_ALLOW_RUNASROOT=1", 96 | "./config.sh --url https://github.com/owner/repo --token githubRegistrationToken --labels label", 97 | "./run.sh", 98 | ] 99 | `; 100 | -------------------------------------------------------------------------------- /__tests__/config.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals' 2 | import { Config } from '../src/config' 3 | import { parseMemory } from '../src/memory' 4 | 5 | test('basic Config', () => { 6 | expect(() => { 7 | new Config({ 8 | instanceId: 'instanceId', 9 | imageId: 'imageId', 10 | diskType: 'diskType', 11 | diskSize: 10 * 1024 ** 3, 12 | subnetId: 'subnetId', 13 | publicIp: true, 14 | zoneId: 'zoneId', 15 | platformId: 'platformId', 16 | folderId: 'folderId', 17 | mode: 'start', 18 | githubToken: 'githubToken', 19 | runnerHomeDir: 'runnerHomeDir', 20 | label: 'label', 21 | serviceAccountId: 'serviceAccountId', 22 | secondDiskImageId: '', 23 | secondDiskType: '', 24 | secondDiskSize: 0, 25 | user: '', 26 | sshPublicKey: '', 27 | runnerVersion: '2.299.1', 28 | disableUpdate: false, 29 | resourcesSpec: { 30 | cores: 1, 31 | memory: 10 * 1024 ** 3, 32 | coreFraction: 100 33 | } 34 | }) 35 | }).not.toThrow() 36 | }) 37 | 38 | test('add secondary disk', () => { 39 | expect(() => { 40 | new Config({ 41 | instanceId: 'instanceId', 42 | imageId: 'imageId', 43 | diskType: 'diskType', 44 | diskSize: parseMemory('256Gb'), 45 | subnetId: 'subnetId', 46 | publicIp: true, 47 | zoneId: 'zoneId', 48 | platformId: 'platformId', 49 | folderId: 'folderId', 50 | mode: 'start', 51 | githubToken: 'githubToken', 52 | runnerHomeDir: 'runnerHomeDir', 53 | label: 'label', 54 | serviceAccountId: 'serviceAccountId', 55 | secondDiskImageId: 'secondDiskImageId', 56 | secondDiskType: 'secondDiskType', 57 | secondDiskSize: parseMemory('30Gb'), 58 | user: '', 59 | sshPublicKey: '', 60 | runnerVersion: '2.299.1', 61 | disableUpdate: false, 62 | resourcesSpec: { 63 | cores: 1, 64 | memory: parseMemory('8Gb'), 65 | coreFraction: 100 66 | } 67 | }) 68 | }).not.toThrow() 69 | }) 70 | 71 | test('add secondary disk without image-id throw error', () => { 72 | expect(() => { 73 | new Config({ 74 | instanceId: 'instanceId', 75 | imageId: 'imageId', 76 | diskType: 'diskType', 77 | diskSize: parseMemory('256Gb'), 78 | subnetId: 'subnetId', 79 | publicIp: true, 80 | zoneId: 'zoneId', 81 | platformId: 'platformId', 82 | folderId: 'folderId', 83 | mode: 'start', 84 | githubToken: 'githubToken', 85 | runnerHomeDir: 'runnerHomeDir', 86 | label: 'label', 87 | serviceAccountId: 'serviceAccountId', 88 | secondDiskImageId: '', 89 | secondDiskType: 'secondDiskType', 90 | secondDiskSize: parseMemory('30Gb'), 91 | user: 'user', 92 | sshPublicKey: 'sshPublicKey', 93 | runnerVersion: '2.299.1', 94 | disableUpdate: false, 95 | resourcesSpec: { 96 | cores: 1, 97 | memory: parseMemory('8Gb'), 98 | coreFraction: 100 99 | } 100 | }) 101 | }).toThrowErrorMatchingSnapshot() 102 | }) 103 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals' 2 | import * as cp from 'child_process' 3 | import * as path from 'path' 4 | import * as process from 'process' 5 | import { buildUserDataScript } from '../src/main' 6 | 7 | // This test will run only in fully configured env and creates real VM 8 | // in the Yandex Cloud, so it will be disabled in CI/CD. You can enable it to test locally. 9 | test.skip('runs', () => { 10 | process.env['GITHUB_WORKSPACE'] = '' 11 | 12 | const np = process.execPath 13 | const ip = path.join(__dirname, '..', 'lib', 'main.js') 14 | const options: cp.ExecFileSyncOptions = { 15 | env: process.env 16 | } 17 | let res 18 | try { 19 | res = cp.execFileSync(np, [ip], options) 20 | } catch (e) { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | console.log((e as any).stdout.toString()) 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | console.log((e as any).stderr.toString()) 25 | } 26 | console.log(res?.toString()) 27 | }) 28 | 29 | describe('cloud-init', () => { 30 | test('default', () => { 31 | const actual = buildUserDataScript({ 32 | githubRegistrationToken: 'githubRegistrationToken', 33 | label: 'label', 34 | owner: 'owner', 35 | repo: 'repo', 36 | runnerHomeDir: '', 37 | user: '', 38 | sshPublicKey: '', 39 | runnerVersion: '2.299.1', 40 | disableUpdate: false 41 | }) 42 | expect(actual.length).toBe(8) 43 | expect(actual).toMatchSnapshot() 44 | expect(actual[0]).toBe('#!/bin/bash') 45 | }) 46 | 47 | test('with home dir', () => { 48 | const actual = buildUserDataScript({ 49 | githubRegistrationToken: 'githubRegistrationToken', 50 | label: 'label', 51 | owner: 'owner', 52 | repo: 'repo', 53 | runnerHomeDir: 'foo', 54 | user: '', 55 | sshPublicKey: '', 56 | runnerVersion: '2.299.1', 57 | disableUpdate: false 58 | }) 59 | expect(actual.length).toBe(5) 60 | expect(actual).toMatchSnapshot() 61 | expect(actual[0]).toBe('#!/bin/bash') 62 | }) 63 | 64 | test('with user and ssh key', () => { 65 | const actual = buildUserDataScript({ 66 | githubRegistrationToken: 'githubRegistrationToken', 67 | label: 'label', 68 | owner: 'owner', 69 | repo: 'repo', 70 | runnerHomeDir: '', 71 | user: 'user', 72 | sshPublicKey: 'key', 73 | runnerVersion: '2.299.1', 74 | disableUpdate: false 75 | }) 76 | expect(actual.length).toBe(23) 77 | expect(actual).toMatchSnapshot() 78 | expect(actual[0]).toBe('#cloud-config') 79 | }) 80 | 81 | test('with home dir and user and ssh key', () => { 82 | const actual = buildUserDataScript({ 83 | githubRegistrationToken: 'githubRegistrationToken', 84 | label: 'label', 85 | owner: 'owner', 86 | repo: 'repo', 87 | runnerHomeDir: 'foo', 88 | user: 'user', 89 | sshPublicKey: 'key', 90 | runnerVersion: '2.299.1', 91 | disableUpdate: false 92 | }) 93 | expect(actual.length).toBe(16) 94 | expect(actual).toMatchSnapshot() 95 | expect(actual[0]).toBe('#cloud-config') 96 | }) 97 | 98 | test('with disable update', () => { 99 | const actual = buildUserDataScript({ 100 | githubRegistrationToken: 'githubRegistrationToken', 101 | label: 'label', 102 | owner: 'owner', 103 | repo: 'repo', 104 | runnerHomeDir: '', 105 | user: '', 106 | sshPublicKey: '', 107 | runnerVersion: '2.299.1', 108 | disableUpdate: true 109 | }) 110 | expect(actual.length).toBe(8) 111 | expect(actual).toMatchSnapshot() 112 | expect(actual[6]).toContain('--disableupdate') 113 | }) 114 | 115 | test('without disable update', () => { 116 | const actual = buildUserDataScript({ 117 | githubRegistrationToken: 'githubRegistrationToken', 118 | label: 'label', 119 | owner: 'owner', 120 | repo: 'repo', 121 | runnerHomeDir: '', 122 | user: '', 123 | sshPublicKey: '', 124 | runnerVersion: '2.299.1', 125 | disableUpdate: false 126 | }) 127 | expect(actual.length).toBe(8) 128 | expect(actual).toMatchSnapshot() 129 | expect(actual[6]).not.toContain('--disableupdate') 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /__tests__/memory.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals' 2 | import { GB, MB, parseMemory } from '../src/memory' 3 | 4 | const mbs = ['mb', 'MB', ' mb'] 5 | const gbs = ['GB', 'Gb', ' gb'] 6 | 7 | test.each( 8 | [128, 256, 512, 1024, 2048].flatMap(x => 9 | mbs.map(u => ({ 10 | input: x.toString() + u, 11 | expected: (x as any) * MB 12 | })) 13 | ) 14 | )('test memory value $input', ({ input, expected }) => { 15 | expect(parseMemory(input)).toEqual(expected) 16 | }) 17 | 18 | test.each( 19 | [0, 1, 2, 4, 8].flatMap(x => 20 | gbs.map(u => ({ 21 | input: x.toString() + u, 22 | expected: (x as any) * GB 23 | })) 24 | ) 25 | )('test memory value $input', ({ input, expected }) => { 26 | expect(parseMemory(input)).toEqual(expected) 27 | }) 28 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'YC GitHub Runner' 2 | description: 'GitHub Action to deploy container to Yandex Cloud COI VM' 3 | author: 'Nikolay Matrosov' 4 | inputs: 5 | mode: 6 | description: >- 7 | Specify here which mode you want to use: 8 | - 'start' - to start a new runner; 9 | - 'stop' - to stop the previously created runner. 10 | required: true 11 | github-token: 12 | description: >- 13 | GitHub Personal Access Token with the 'repo' scope assigned. 14 | required: true 15 | yc-sa-json-credentials: 16 | required: false 17 | description: 'JSON containing authorized key for Service Account. 18 | Either this or `yc-iam-token` or `yc-sa-id` should be provided. 19 | More info https://cloud.yandex.ru/docs/container-registry/operations/authentication#sa-json' 20 | yc-iam-token: 21 | required: false 22 | description: 'IAM token for Yandex Cloud. 23 | Either this or `yc-sa-json-credentials` or `yc-sa-id` should be provided. 24 | More info https://yandex.cloud/ru/docs/iam/concepts/authorization/iam-token' 25 | yc-sa-id: 26 | required: false 27 | description: 'Service Account ID. Will be uses to exchange GitHub token to Yandex Cloud token. 28 | Either this or `yc-iam-token` or `yc-sa-json-credentials` should be provided. 29 | More info https://yandex.cloud/ru/docs/iam/concepts/workload-identity' 30 | folder-id: 31 | required: true 32 | description: 'Folder ID' 33 | label: 34 | description: >- 35 | Name of the unique label assigned to the runner. 36 | The label is used to remove the runner from GitHub when the runner is not needed anymore. 37 | This input is required if you use the 'stop' mode. 38 | required: false 39 | runner-home-dir: 40 | description: >- 41 | Directory that contains actions-runner software and scripts. E.g. /home/runner/actions-runner. 42 | required: false 43 | instance-id: 44 | required: false 45 | description: >- 46 | Id of the created instance. 47 | This input is required if you use the 'stop' mode. 48 | image-id: 49 | required: false 50 | description: 'Id of the image that will be used to create boot disk of the VM.' 51 | service-account-id: 52 | required: false 53 | description: 'Id of service account that will be attached to VM.' 54 | cores: 55 | required: false 56 | description: 'Number of Cores' 57 | default: '2' 58 | memory: 59 | required: false 60 | description: 'Memory. Format: `\\d(Mb|Gb)`. E.g. 1Gb' 61 | default: '1GB' 62 | core-fraction: 63 | required: false 64 | description: 'Core fraction' 65 | default: '100' 66 | disk-type: 67 | required: false 68 | description: 'Disk type. Values: network-ssd, network-hdd, etc. More info https://cloud.yandex.com/en-ru/docs/managed-clickhouse/concepts/storage' 69 | default: 'network-ssd' 70 | disk-size: 71 | required: false 72 | description: 'Disk size. Format: `\\d(Mb|Gb)`. E.g. 30Gb' 73 | default: '30GB' 74 | image2-id: 75 | required: false 76 | description: 'Id of the image that will be used to create a second disk on the VM.' 77 | disk2-type: 78 | required: false 79 | description: 'Second disk type. Values: network-ssd, network-hdd, etc. More info https://cloud.yandex.com/en-ru/docs/managed-clickhouse/concepts/storage' 80 | default: 'network-ssd' 81 | disk2-size: 82 | required: false 83 | description: 'Second disk size. Format: `\\d(Mb|Gb)`. E.g. 30Gb' 84 | zone-id: 85 | required: false 86 | description: 'Zone ID' 87 | default: 'ru-central1-a' 88 | subnet-id: 89 | required: true 90 | description: 'The subnet ID where the VM will be created' 91 | public-ip: 92 | required: false 93 | description: 'Public IP address' 94 | default: 'true' 95 | platform-id: 96 | required: false 97 | description: 'Compute platform Id' 98 | default: 'standard-v3' 99 | runner-version: 100 | required: false 101 | description: 'Version of the actions-runner software' 102 | default: '2.320.0' 103 | ttl: 104 | required: false 105 | description: 'Time to live in ISO 8601 Duration format. E.g. PT1H' 106 | disable-update: 107 | required: false 108 | description: 'Disable update' 109 | default: 'false' 110 | 111 | user: 112 | required: false 113 | description: 'User name to login with via ssh' 114 | ssh-public-key: 115 | required: false 116 | description: 'Public SSH key to login with' 117 | outputs: 118 | label: 119 | description: >- 120 | Name of the unique label assigned to the runner. 121 | The label is used in two cases: 122 | - to use as the input of 'runs-on' property for the following jobs; 123 | - to remove the runner from GitHub when it is not needed anymore. 124 | instance-id: 125 | description: >- 126 | YC Instance Id of the created runner. 127 | The id is used to terminate the instance when the runner is not needed anymore. 128 | branding: 129 | color: blue 130 | icon: upload-cloud 131 | runs: 132 | using: 'node20' 133 | main: 'dist/index.js' 134 | -------------------------------------------------------------------------------- /dist/356.index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.id = 356; 3 | exports.ids = [356]; 4 | exports.modules = { 5 | 6 | /***/ 7356: 7 | /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { 8 | 9 | __webpack_require__.r(__webpack_exports__); 10 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 11 | /* harmony export */ "toFormData": () => (/* binding */ toFormData) 12 | /* harmony export */ }); 13 | /* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2777); 14 | /* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010); 15 | 16 | 17 | 18 | let s = 0; 19 | const S = { 20 | START_BOUNDARY: s++, 21 | HEADER_FIELD_START: s++, 22 | HEADER_FIELD: s++, 23 | HEADER_VALUE_START: s++, 24 | HEADER_VALUE: s++, 25 | HEADER_VALUE_ALMOST_DONE: s++, 26 | HEADERS_ALMOST_DONE: s++, 27 | PART_DATA_START: s++, 28 | PART_DATA: s++, 29 | END: s++ 30 | }; 31 | 32 | let f = 1; 33 | const F = { 34 | PART_BOUNDARY: f, 35 | LAST_BOUNDARY: f *= 2 36 | }; 37 | 38 | const LF = 10; 39 | const CR = 13; 40 | const SPACE = 32; 41 | const HYPHEN = 45; 42 | const COLON = 58; 43 | const A = 97; 44 | const Z = 122; 45 | 46 | const lower = c => c | 0x20; 47 | 48 | const noop = () => {}; 49 | 50 | class MultipartParser { 51 | /** 52 | * @param {string} boundary 53 | */ 54 | constructor(boundary) { 55 | this.index = 0; 56 | this.flags = 0; 57 | 58 | this.onHeaderEnd = noop; 59 | this.onHeaderField = noop; 60 | this.onHeadersEnd = noop; 61 | this.onHeaderValue = noop; 62 | this.onPartBegin = noop; 63 | this.onPartData = noop; 64 | this.onPartEnd = noop; 65 | 66 | this.boundaryChars = {}; 67 | 68 | boundary = '\r\n--' + boundary; 69 | const ui8a = new Uint8Array(boundary.length); 70 | for (let i = 0; i < boundary.length; i++) { 71 | ui8a[i] = boundary.charCodeAt(i); 72 | this.boundaryChars[ui8a[i]] = true; 73 | } 74 | 75 | this.boundary = ui8a; 76 | this.lookbehind = new Uint8Array(this.boundary.length + 8); 77 | this.state = S.START_BOUNDARY; 78 | } 79 | 80 | /** 81 | * @param {Uint8Array} data 82 | */ 83 | write(data) { 84 | let i = 0; 85 | const length_ = data.length; 86 | let previousIndex = this.index; 87 | let {lookbehind, boundary, boundaryChars, index, state, flags} = this; 88 | const boundaryLength = this.boundary.length; 89 | const boundaryEnd = boundaryLength - 1; 90 | const bufferLength = data.length; 91 | let c; 92 | let cl; 93 | 94 | const mark = name => { 95 | this[name + 'Mark'] = i; 96 | }; 97 | 98 | const clear = name => { 99 | delete this[name + 'Mark']; 100 | }; 101 | 102 | const callback = (callbackSymbol, start, end, ui8a) => { 103 | if (start === undefined || start !== end) { 104 | this[callbackSymbol](ui8a && ui8a.subarray(start, end)); 105 | } 106 | }; 107 | 108 | const dataCallback = (name, clear) => { 109 | const markSymbol = name + 'Mark'; 110 | if (!(markSymbol in this)) { 111 | return; 112 | } 113 | 114 | if (clear) { 115 | callback(name, this[markSymbol], i, data); 116 | delete this[markSymbol]; 117 | } else { 118 | callback(name, this[markSymbol], data.length, data); 119 | this[markSymbol] = 0; 120 | } 121 | }; 122 | 123 | for (i = 0; i < length_; i++) { 124 | c = data[i]; 125 | 126 | switch (state) { 127 | case S.START_BOUNDARY: 128 | if (index === boundary.length - 2) { 129 | if (c === HYPHEN) { 130 | flags |= F.LAST_BOUNDARY; 131 | } else if (c !== CR) { 132 | return; 133 | } 134 | 135 | index++; 136 | break; 137 | } else if (index - 1 === boundary.length - 2) { 138 | if (flags & F.LAST_BOUNDARY && c === HYPHEN) { 139 | state = S.END; 140 | flags = 0; 141 | } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { 142 | index = 0; 143 | callback('onPartBegin'); 144 | state = S.HEADER_FIELD_START; 145 | } else { 146 | return; 147 | } 148 | 149 | break; 150 | } 151 | 152 | if (c !== boundary[index + 2]) { 153 | index = -2; 154 | } 155 | 156 | if (c === boundary[index + 2]) { 157 | index++; 158 | } 159 | 160 | break; 161 | case S.HEADER_FIELD_START: 162 | state = S.HEADER_FIELD; 163 | mark('onHeaderField'); 164 | index = 0; 165 | // falls through 166 | case S.HEADER_FIELD: 167 | if (c === CR) { 168 | clear('onHeaderField'); 169 | state = S.HEADERS_ALMOST_DONE; 170 | break; 171 | } 172 | 173 | index++; 174 | if (c === HYPHEN) { 175 | break; 176 | } 177 | 178 | if (c === COLON) { 179 | if (index === 1) { 180 | // empty header field 181 | return; 182 | } 183 | 184 | dataCallback('onHeaderField', true); 185 | state = S.HEADER_VALUE_START; 186 | break; 187 | } 188 | 189 | cl = lower(c); 190 | if (cl < A || cl > Z) { 191 | return; 192 | } 193 | 194 | break; 195 | case S.HEADER_VALUE_START: 196 | if (c === SPACE) { 197 | break; 198 | } 199 | 200 | mark('onHeaderValue'); 201 | state = S.HEADER_VALUE; 202 | // falls through 203 | case S.HEADER_VALUE: 204 | if (c === CR) { 205 | dataCallback('onHeaderValue', true); 206 | callback('onHeaderEnd'); 207 | state = S.HEADER_VALUE_ALMOST_DONE; 208 | } 209 | 210 | break; 211 | case S.HEADER_VALUE_ALMOST_DONE: 212 | if (c !== LF) { 213 | return; 214 | } 215 | 216 | state = S.HEADER_FIELD_START; 217 | break; 218 | case S.HEADERS_ALMOST_DONE: 219 | if (c !== LF) { 220 | return; 221 | } 222 | 223 | callback('onHeadersEnd'); 224 | state = S.PART_DATA_START; 225 | break; 226 | case S.PART_DATA_START: 227 | state = S.PART_DATA; 228 | mark('onPartData'); 229 | // falls through 230 | case S.PART_DATA: 231 | previousIndex = index; 232 | 233 | if (index === 0) { 234 | // boyer-moore derrived algorithm to safely skip non-boundary data 235 | i += boundaryEnd; 236 | while (i < bufferLength && !(data[i] in boundaryChars)) { 237 | i += boundaryLength; 238 | } 239 | 240 | i -= boundaryEnd; 241 | c = data[i]; 242 | } 243 | 244 | if (index < boundary.length) { 245 | if (boundary[index] === c) { 246 | if (index === 0) { 247 | dataCallback('onPartData', true); 248 | } 249 | 250 | index++; 251 | } else { 252 | index = 0; 253 | } 254 | } else if (index === boundary.length) { 255 | index++; 256 | if (c === CR) { 257 | // CR = part boundary 258 | flags |= F.PART_BOUNDARY; 259 | } else if (c === HYPHEN) { 260 | // HYPHEN = end boundary 261 | flags |= F.LAST_BOUNDARY; 262 | } else { 263 | index = 0; 264 | } 265 | } else if (index - 1 === boundary.length) { 266 | if (flags & F.PART_BOUNDARY) { 267 | index = 0; 268 | if (c === LF) { 269 | // unset the PART_BOUNDARY flag 270 | flags &= ~F.PART_BOUNDARY; 271 | callback('onPartEnd'); 272 | callback('onPartBegin'); 273 | state = S.HEADER_FIELD_START; 274 | break; 275 | } 276 | } else if (flags & F.LAST_BOUNDARY) { 277 | if (c === HYPHEN) { 278 | callback('onPartEnd'); 279 | state = S.END; 280 | flags = 0; 281 | } else { 282 | index = 0; 283 | } 284 | } else { 285 | index = 0; 286 | } 287 | } 288 | 289 | if (index > 0) { 290 | // when matching a possible boundary, keep a lookbehind reference 291 | // in case it turns out to be a false lead 292 | lookbehind[index - 1] = c; 293 | } else if (previousIndex > 0) { 294 | // if our boundary turned out to be rubbish, the captured lookbehind 295 | // belongs to partData 296 | const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); 297 | callback('onPartData', 0, previousIndex, _lookbehind); 298 | previousIndex = 0; 299 | mark('onPartData'); 300 | 301 | // reconsider the current character even so it interrupted the sequence 302 | // it could be the beginning of a new sequence 303 | i--; 304 | } 305 | 306 | break; 307 | case S.END: 308 | break; 309 | default: 310 | throw new Error(`Unexpected state entered: ${state}`); 311 | } 312 | } 313 | 314 | dataCallback('onHeaderField'); 315 | dataCallback('onHeaderValue'); 316 | dataCallback('onPartData'); 317 | 318 | // Update properties for the next call 319 | this.index = index; 320 | this.state = state; 321 | this.flags = flags; 322 | } 323 | 324 | end() { 325 | if ((this.state === S.HEADER_FIELD_START && this.index === 0) || 326 | (this.state === S.PART_DATA && this.index === this.boundary.length)) { 327 | this.onPartEnd(); 328 | } else if (this.state !== S.END) { 329 | throw new Error('MultipartParser.end(): stream ended unexpectedly'); 330 | } 331 | } 332 | } 333 | 334 | function _fileName(headerValue) { 335 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 336 | const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); 337 | if (!m) { 338 | return; 339 | } 340 | 341 | const match = m[2] || m[3] || ''; 342 | let filename = match.slice(match.lastIndexOf('\\') + 1); 343 | filename = filename.replace(/%22/g, '"'); 344 | filename = filename.replace(/&#(\d{4});/g, (m, code) => { 345 | return String.fromCharCode(code); 346 | }); 347 | return filename; 348 | } 349 | 350 | async function toFormData(Body, ct) { 351 | if (!/multipart/i.test(ct)) { 352 | throw new TypeError('Failed to fetch'); 353 | } 354 | 355 | const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); 356 | 357 | if (!m) { 358 | throw new TypeError('no or bad content-type header, no multipart boundary'); 359 | } 360 | 361 | const parser = new MultipartParser(m[1] || m[2]); 362 | 363 | let headerField; 364 | let headerValue; 365 | let entryValue; 366 | let entryName; 367 | let contentType; 368 | let filename; 369 | const entryChunks = []; 370 | const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct(); 371 | 372 | const onPartData = ui8a => { 373 | entryValue += decoder.decode(ui8a, {stream: true}); 374 | }; 375 | 376 | const appendToFile = ui8a => { 377 | entryChunks.push(ui8a); 378 | }; 379 | 380 | const appendFileToFormData = () => { 381 | const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType}); 382 | formData.append(entryName, file); 383 | }; 384 | 385 | const appendEntryToFormData = () => { 386 | formData.append(entryName, entryValue); 387 | }; 388 | 389 | const decoder = new TextDecoder('utf-8'); 390 | decoder.decode(); 391 | 392 | parser.onPartBegin = function () { 393 | parser.onPartData = onPartData; 394 | parser.onPartEnd = appendEntryToFormData; 395 | 396 | headerField = ''; 397 | headerValue = ''; 398 | entryValue = ''; 399 | entryName = ''; 400 | contentType = ''; 401 | filename = null; 402 | entryChunks.length = 0; 403 | }; 404 | 405 | parser.onHeaderField = function (ui8a) { 406 | headerField += decoder.decode(ui8a, {stream: true}); 407 | }; 408 | 409 | parser.onHeaderValue = function (ui8a) { 410 | headerValue += decoder.decode(ui8a, {stream: true}); 411 | }; 412 | 413 | parser.onHeaderEnd = function () { 414 | headerValue += decoder.decode(); 415 | headerField = headerField.toLowerCase(); 416 | 417 | if (headerField === 'content-disposition') { 418 | // matches either a quoted-string or a token (RFC 2616 section 19.5.1) 419 | const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); 420 | 421 | if (m) { 422 | entryName = m[2] || m[3] || ''; 423 | } 424 | 425 | filename = _fileName(headerValue); 426 | 427 | if (filename) { 428 | parser.onPartData = appendToFile; 429 | parser.onPartEnd = appendFileToFormData; 430 | } 431 | } else if (headerField === 'content-type') { 432 | contentType = headerValue; 433 | } 434 | 435 | headerValue = ''; 436 | headerField = ''; 437 | }; 438 | 439 | for await (const chunk of Body) { 440 | parser.write(chunk); 441 | } 442 | 443 | parser.end(); 444 | 445 | return formData; 446 | } 447 | 448 | 449 | /***/ }) 450 | 451 | }; 452 | ; 453 | //# sourceMappingURL=356.index.js.map -------------------------------------------------------------------------------- /dist/356.index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"356.index.js","mappings":";;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://yc-actions-yc-coi-deploy/./node_modules/@nikolay.matrosov/yc-ts-sdk/node_modules/node-fetch/src/utils/multipart-parser.js"],"sourcesContent":["import {File} from 'fetch-blob/from.js';\nimport {FormData} from 'formdata-polyfill/esm.min.js';\n\nlet s = 0;\nconst S = {\n\tSTART_BOUNDARY: s++,\n\tHEADER_FIELD_START: s++,\n\tHEADER_FIELD: s++,\n\tHEADER_VALUE_START: s++,\n\tHEADER_VALUE: s++,\n\tHEADER_VALUE_ALMOST_DONE: s++,\n\tHEADERS_ALMOST_DONE: s++,\n\tPART_DATA_START: s++,\n\tPART_DATA: s++,\n\tEND: s++\n};\n\nlet f = 1;\nconst F = {\n\tPART_BOUNDARY: f,\n\tLAST_BOUNDARY: f *= 2\n};\n\nconst LF = 10;\nconst CR = 13;\nconst SPACE = 32;\nconst HYPHEN = 45;\nconst COLON = 58;\nconst A = 97;\nconst Z = 122;\n\nconst lower = c => c | 0x20;\n\nconst noop = () => {};\n\nclass MultipartParser {\n\t/**\n\t * @param {string} boundary\n\t */\n\tconstructor(boundary) {\n\t\tthis.index = 0;\n\t\tthis.flags = 0;\n\n\t\tthis.onHeaderEnd = noop;\n\t\tthis.onHeaderField = noop;\n\t\tthis.onHeadersEnd = noop;\n\t\tthis.onHeaderValue = noop;\n\t\tthis.onPartBegin = noop;\n\t\tthis.onPartData = noop;\n\t\tthis.onPartEnd = noop;\n\n\t\tthis.boundaryChars = {};\n\n\t\tboundary = '\\r\\n--' + boundary;\n\t\tconst ui8a = new Uint8Array(boundary.length);\n\t\tfor (let i = 0; i < boundary.length; i++) {\n\t\t\tui8a[i] = boundary.charCodeAt(i);\n\t\t\tthis.boundaryChars[ui8a[i]] = true;\n\t\t}\n\n\t\tthis.boundary = ui8a;\n\t\tthis.lookbehind = new Uint8Array(this.boundary.length + 8);\n\t\tthis.state = S.START_BOUNDARY;\n\t}\n\n\t/**\n\t * @param {Uint8Array} data\n\t */\n\twrite(data) {\n\t\tlet i = 0;\n\t\tconst length_ = data.length;\n\t\tlet previousIndex = this.index;\n\t\tlet {lookbehind, boundary, boundaryChars, index, state, flags} = this;\n\t\tconst boundaryLength = this.boundary.length;\n\t\tconst boundaryEnd = boundaryLength - 1;\n\t\tconst bufferLength = data.length;\n\t\tlet c;\n\t\tlet cl;\n\n\t\tconst mark = name => {\n\t\t\tthis[name + 'Mark'] = i;\n\t\t};\n\n\t\tconst clear = name => {\n\t\t\tdelete this[name + 'Mark'];\n\t\t};\n\n\t\tconst callback = (callbackSymbol, start, end, ui8a) => {\n\t\t\tif (start === undefined || start !== end) {\n\t\t\t\tthis[callbackSymbol](ui8a && ui8a.subarray(start, end));\n\t\t\t}\n\t\t};\n\n\t\tconst dataCallback = (name, clear) => {\n\t\t\tconst markSymbol = name + 'Mark';\n\t\t\tif (!(markSymbol in this)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (clear) {\n\t\t\t\tcallback(name, this[markSymbol], i, data);\n\t\t\t\tdelete this[markSymbol];\n\t\t\t} else {\n\t\t\t\tcallback(name, this[markSymbol], data.length, data);\n\t\t\t\tthis[markSymbol] = 0;\n\t\t\t}\n\t\t};\n\n\t\tfor (i = 0; i < length_; i++) {\n\t\t\tc = data[i];\n\n\t\t\tswitch (state) {\n\t\t\t\tcase S.START_BOUNDARY:\n\t\t\t\t\tif (index === boundary.length - 2) {\n\t\t\t\t\t\tif (c === HYPHEN) {\n\t\t\t\t\t\t\tflags |= F.LAST_BOUNDARY;\n\t\t\t\t\t\t} else if (c !== CR) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tindex++;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t} else if (index - 1 === boundary.length - 2) {\n\t\t\t\t\t\tif (flags & F.LAST_BOUNDARY && c === HYPHEN) {\n\t\t\t\t\t\t\tstate = S.END;\n\t\t\t\t\t\t\tflags = 0;\n\t\t\t\t\t\t} else if (!(flags & F.LAST_BOUNDARY) && c === LF) {\n\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t\tcallback('onPartBegin');\n\t\t\t\t\t\t\tstate = S.HEADER_FIELD_START;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (c !== boundary[index + 2]) {\n\t\t\t\t\t\tindex = -2;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (c === boundary[index + 2]) {\n\t\t\t\t\t\tindex++;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.HEADER_FIELD_START:\n\t\t\t\t\tstate = S.HEADER_FIELD;\n\t\t\t\t\tmark('onHeaderField');\n\t\t\t\t\tindex = 0;\n\t\t\t\t\t// falls through\n\t\t\t\tcase S.HEADER_FIELD:\n\t\t\t\t\tif (c === CR) {\n\t\t\t\t\t\tclear('onHeaderField');\n\t\t\t\t\t\tstate = S.HEADERS_ALMOST_DONE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tindex++;\n\t\t\t\t\tif (c === HYPHEN) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (c === COLON) {\n\t\t\t\t\t\tif (index === 1) {\n\t\t\t\t\t\t\t// empty header field\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdataCallback('onHeaderField', true);\n\t\t\t\t\t\tstate = S.HEADER_VALUE_START;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcl = lower(c);\n\t\t\t\t\tif (cl < A || cl > Z) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.HEADER_VALUE_START:\n\t\t\t\t\tif (c === SPACE) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tmark('onHeaderValue');\n\t\t\t\t\tstate = S.HEADER_VALUE;\n\t\t\t\t\t// falls through\n\t\t\t\tcase S.HEADER_VALUE:\n\t\t\t\t\tif (c === CR) {\n\t\t\t\t\t\tdataCallback('onHeaderValue', true);\n\t\t\t\t\t\tcallback('onHeaderEnd');\n\t\t\t\t\t\tstate = S.HEADER_VALUE_ALMOST_DONE;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.HEADER_VALUE_ALMOST_DONE:\n\t\t\t\t\tif (c !== LF) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tstate = S.HEADER_FIELD_START;\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.HEADERS_ALMOST_DONE:\n\t\t\t\t\tif (c !== LF) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tcallback('onHeadersEnd');\n\t\t\t\t\tstate = S.PART_DATA_START;\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.PART_DATA_START:\n\t\t\t\t\tstate = S.PART_DATA;\n\t\t\t\t\tmark('onPartData');\n\t\t\t\t\t// falls through\n\t\t\t\tcase S.PART_DATA:\n\t\t\t\t\tpreviousIndex = index;\n\n\t\t\t\t\tif (index === 0) {\n\t\t\t\t\t\t// boyer-moore derrived algorithm to safely skip non-boundary data\n\t\t\t\t\t\ti += boundaryEnd;\n\t\t\t\t\t\twhile (i < bufferLength && !(data[i] in boundaryChars)) {\n\t\t\t\t\t\t\ti += boundaryLength;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ti -= boundaryEnd;\n\t\t\t\t\t\tc = data[i];\n\t\t\t\t\t}\n\n\t\t\t\t\tif (index < boundary.length) {\n\t\t\t\t\t\tif (boundary[index] === c) {\n\t\t\t\t\t\t\tif (index === 0) {\n\t\t\t\t\t\t\t\tdataCallback('onPartData', true);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tindex++;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (index === boundary.length) {\n\t\t\t\t\t\tindex++;\n\t\t\t\t\t\tif (c === CR) {\n\t\t\t\t\t\t\t// CR = part boundary\n\t\t\t\t\t\t\tflags |= F.PART_BOUNDARY;\n\t\t\t\t\t\t} else if (c === HYPHEN) {\n\t\t\t\t\t\t\t// HYPHEN = end boundary\n\t\t\t\t\t\t\tflags |= F.LAST_BOUNDARY;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (index - 1 === boundary.length) {\n\t\t\t\t\t\tif (flags & F.PART_BOUNDARY) {\n\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t\tif (c === LF) {\n\t\t\t\t\t\t\t\t// unset the PART_BOUNDARY flag\n\t\t\t\t\t\t\t\tflags &= ~F.PART_BOUNDARY;\n\t\t\t\t\t\t\t\tcallback('onPartEnd');\n\t\t\t\t\t\t\t\tcallback('onPartBegin');\n\t\t\t\t\t\t\t\tstate = S.HEADER_FIELD_START;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (flags & F.LAST_BOUNDARY) {\n\t\t\t\t\t\t\tif (c === HYPHEN) {\n\t\t\t\t\t\t\t\tcallback('onPartEnd');\n\t\t\t\t\t\t\t\tstate = S.END;\n\t\t\t\t\t\t\t\tflags = 0;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tindex = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (index > 0) {\n\t\t\t\t\t\t// when matching a possible boundary, keep a lookbehind reference\n\t\t\t\t\t\t// in case it turns out to be a false lead\n\t\t\t\t\t\tlookbehind[index - 1] = c;\n\t\t\t\t\t} else if (previousIndex > 0) {\n\t\t\t\t\t\t// if our boundary turned out to be rubbish, the captured lookbehind\n\t\t\t\t\t\t// belongs to partData\n\t\t\t\t\t\tconst _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength);\n\t\t\t\t\t\tcallback('onPartData', 0, previousIndex, _lookbehind);\n\t\t\t\t\t\tpreviousIndex = 0;\n\t\t\t\t\t\tmark('onPartData');\n\n\t\t\t\t\t\t// reconsider the current character even so it interrupted the sequence\n\t\t\t\t\t\t// it could be the beginning of a new sequence\n\t\t\t\t\t\ti--;\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\tcase S.END:\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new Error(`Unexpected state entered: ${state}`);\n\t\t\t}\n\t\t}\n\n\t\tdataCallback('onHeaderField');\n\t\tdataCallback('onHeaderValue');\n\t\tdataCallback('onPartData');\n\n\t\t// Update properties for the next call\n\t\tthis.index = index;\n\t\tthis.state = state;\n\t\tthis.flags = flags;\n\t}\n\n\tend() {\n\t\tif ((this.state === S.HEADER_FIELD_START && this.index === 0) ||\n\t\t\t(this.state === S.PART_DATA && this.index === this.boundary.length)) {\n\t\t\tthis.onPartEnd();\n\t\t} else if (this.state !== S.END) {\n\t\t\tthrow new Error('MultipartParser.end(): stream ended unexpectedly');\n\t\t}\n\t}\n}\n\nfunction _fileName(headerValue) {\n\t// matches either a quoted-string or a token (RFC 2616 section 19.5.1)\n\tconst m = headerValue.match(/\\bfilename=(\"(.*?)\"|([^()<>@,;:\\\\\"/[\\]?={}\\s\\t]+))($|;\\s)/i);\n\tif (!m) {\n\t\treturn;\n\t}\n\n\tconst match = m[2] || m[3] || '';\n\tlet filename = match.slice(match.lastIndexOf('\\\\') + 1);\n\tfilename = filename.replace(/%22/g, '\"');\n\tfilename = filename.replace(/&#(\\d{4});/g, (m, code) => {\n\t\treturn String.fromCharCode(code);\n\t});\n\treturn filename;\n}\n\nexport async function toFormData(Body, ct) {\n\tif (!/multipart/i.test(ct)) {\n\t\tthrow new TypeError('Failed to fetch');\n\t}\n\n\tconst m = ct.match(/boundary=(?:\"([^\"]+)\"|([^;]+))/i);\n\n\tif (!m) {\n\t\tthrow new TypeError('no or bad content-type header, no multipart boundary');\n\t}\n\n\tconst parser = new MultipartParser(m[1] || m[2]);\n\n\tlet headerField;\n\tlet headerValue;\n\tlet entryValue;\n\tlet entryName;\n\tlet contentType;\n\tlet filename;\n\tconst entryChunks = [];\n\tconst formData = new FormData();\n\n\tconst onPartData = ui8a => {\n\t\tentryValue += decoder.decode(ui8a, {stream: true});\n\t};\n\n\tconst appendToFile = ui8a => {\n\t\tentryChunks.push(ui8a);\n\t};\n\n\tconst appendFileToFormData = () => {\n\t\tconst file = new File(entryChunks, filename, {type: contentType});\n\t\tformData.append(entryName, file);\n\t};\n\n\tconst appendEntryToFormData = () => {\n\t\tformData.append(entryName, entryValue);\n\t};\n\n\tconst decoder = new TextDecoder('utf-8');\n\tdecoder.decode();\n\n\tparser.onPartBegin = function () {\n\t\tparser.onPartData = onPartData;\n\t\tparser.onPartEnd = appendEntryToFormData;\n\n\t\theaderField = '';\n\t\theaderValue = '';\n\t\tentryValue = '';\n\t\tentryName = '';\n\t\tcontentType = '';\n\t\tfilename = null;\n\t\tentryChunks.length = 0;\n\t};\n\n\tparser.onHeaderField = function (ui8a) {\n\t\theaderField += decoder.decode(ui8a, {stream: true});\n\t};\n\n\tparser.onHeaderValue = function (ui8a) {\n\t\theaderValue += decoder.decode(ui8a, {stream: true});\n\t};\n\n\tparser.onHeaderEnd = function () {\n\t\theaderValue += decoder.decode();\n\t\theaderField = headerField.toLowerCase();\n\n\t\tif (headerField === 'content-disposition') {\n\t\t\t// matches either a quoted-string or a token (RFC 2616 section 19.5.1)\n\t\t\tconst m = headerValue.match(/\\bname=(\"([^\"]*)\"|([^()<>@,;:\\\\\"/[\\]?={}\\s\\t]+))/i);\n\n\t\t\tif (m) {\n\t\t\t\tentryName = m[2] || m[3] || '';\n\t\t\t}\n\n\t\t\tfilename = _fileName(headerValue);\n\n\t\t\tif (filename) {\n\t\t\t\tparser.onPartData = appendToFile;\n\t\t\t\tparser.onPartEnd = appendFileToFormData;\n\t\t\t}\n\t\t} else if (headerField === 'content-type') {\n\t\t\tcontentType = headerValue;\n\t\t}\n\n\t\theaderValue = '';\n\t\theaderField = '';\n\t};\n\n\tfor await (const chunk of Body) {\n\t\tparser.write(chunk);\n\t}\n\n\tparser.end();\n\n\treturn formData;\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/proto/channelz.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The gRPC Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file defines an interface for exporting monitoring information 16 | // out of gRPC servers. See the full design at 17 | // https://github.com/grpc/proposal/blob/master/A14-channelz.md 18 | // 19 | // The canonical version of this proto can be found at 20 | // https://github.com/grpc/grpc-proto/blob/master/grpc/channelz/v1/channelz.proto 21 | 22 | syntax = "proto3"; 23 | 24 | package grpc.channelz.v1; 25 | 26 | import "google/protobuf/any.proto"; 27 | import "google/protobuf/duration.proto"; 28 | import "google/protobuf/timestamp.proto"; 29 | import "google/protobuf/wrappers.proto"; 30 | 31 | option go_package = "google.golang.org/grpc/channelz/grpc_channelz_v1"; 32 | option java_multiple_files = true; 33 | option java_package = "io.grpc.channelz.v1"; 34 | option java_outer_classname = "ChannelzProto"; 35 | 36 | // Channel is a logical grouping of channels, subchannels, and sockets. 37 | message Channel { 38 | // The identifier for this channel. This should bet set. 39 | ChannelRef ref = 1; 40 | // Data specific to this channel. 41 | ChannelData data = 2; 42 | // At most one of 'channel_ref+subchannel_ref' and 'socket' is set. 43 | 44 | // There are no ordering guarantees on the order of channel refs. 45 | // There may not be cycles in the ref graph. 46 | // A channel ref may be present in more than one channel or subchannel. 47 | repeated ChannelRef channel_ref = 3; 48 | 49 | // At most one of 'channel_ref+subchannel_ref' and 'socket' is set. 50 | // There are no ordering guarantees on the order of subchannel refs. 51 | // There may not be cycles in the ref graph. 52 | // A sub channel ref may be present in more than one channel or subchannel. 53 | repeated SubchannelRef subchannel_ref = 4; 54 | 55 | // There are no ordering guarantees on the order of sockets. 56 | repeated SocketRef socket_ref = 5; 57 | } 58 | 59 | // Subchannel is a logical grouping of channels, subchannels, and sockets. 60 | // A subchannel is load balanced over by it's ancestor 61 | message Subchannel { 62 | // The identifier for this channel. 63 | SubchannelRef ref = 1; 64 | // Data specific to this channel. 65 | ChannelData data = 2; 66 | // At most one of 'channel_ref+subchannel_ref' and 'socket' is set. 67 | 68 | // There are no ordering guarantees on the order of channel refs. 69 | // There may not be cycles in the ref graph. 70 | // A channel ref may be present in more than one channel or subchannel. 71 | repeated ChannelRef channel_ref = 3; 72 | 73 | // At most one of 'channel_ref+subchannel_ref' and 'socket' is set. 74 | // There are no ordering guarantees on the order of subchannel refs. 75 | // There may not be cycles in the ref graph. 76 | // A sub channel ref may be present in more than one channel or subchannel. 77 | repeated SubchannelRef subchannel_ref = 4; 78 | 79 | // There are no ordering guarantees on the order of sockets. 80 | repeated SocketRef socket_ref = 5; 81 | } 82 | 83 | // These come from the specified states in this document: 84 | // https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md 85 | message ChannelConnectivityState { 86 | enum State { 87 | UNKNOWN = 0; 88 | IDLE = 1; 89 | CONNECTING = 2; 90 | READY = 3; 91 | TRANSIENT_FAILURE = 4; 92 | SHUTDOWN = 5; 93 | } 94 | State state = 1; 95 | } 96 | 97 | // Channel data is data related to a specific Channel or Subchannel. 98 | message ChannelData { 99 | // The connectivity state of the channel or subchannel. Implementations 100 | // should always set this. 101 | ChannelConnectivityState state = 1; 102 | 103 | // The target this channel originally tried to connect to. May be absent 104 | string target = 2; 105 | 106 | // A trace of recent events on the channel. May be absent. 107 | ChannelTrace trace = 3; 108 | 109 | // The number of calls started on the channel 110 | int64 calls_started = 4; 111 | // The number of calls that have completed with an OK status 112 | int64 calls_succeeded = 5; 113 | // The number of calls that have completed with a non-OK status 114 | int64 calls_failed = 6; 115 | 116 | // The last time a call was started on the channel. 117 | google.protobuf.Timestamp last_call_started_timestamp = 7; 118 | } 119 | 120 | // A trace event is an interesting thing that happened to a channel or 121 | // subchannel, such as creation, address resolution, subchannel creation, etc. 122 | message ChannelTraceEvent { 123 | // High level description of the event. 124 | string description = 1; 125 | // The supported severity levels of trace events. 126 | enum Severity { 127 | CT_UNKNOWN = 0; 128 | CT_INFO = 1; 129 | CT_WARNING = 2; 130 | CT_ERROR = 3; 131 | } 132 | // the severity of the trace event 133 | Severity severity = 2; 134 | // When this event occurred. 135 | google.protobuf.Timestamp timestamp = 3; 136 | // ref of referenced channel or subchannel. 137 | // Optional, only present if this event refers to a child object. For example, 138 | // this field would be filled if this trace event was for a subchannel being 139 | // created. 140 | oneof child_ref { 141 | ChannelRef channel_ref = 4; 142 | SubchannelRef subchannel_ref = 5; 143 | } 144 | } 145 | 146 | // ChannelTrace represents the recent events that have occurred on the channel. 147 | message ChannelTrace { 148 | // Number of events ever logged in this tracing object. This can differ from 149 | // events.size() because events can be overwritten or garbage collected by 150 | // implementations. 151 | int64 num_events_logged = 1; 152 | // Time that this channel was created. 153 | google.protobuf.Timestamp creation_timestamp = 2; 154 | // List of events that have occurred on this channel. 155 | repeated ChannelTraceEvent events = 3; 156 | } 157 | 158 | // ChannelRef is a reference to a Channel. 159 | message ChannelRef { 160 | // The globally unique id for this channel. Must be a positive number. 161 | int64 channel_id = 1; 162 | // An optional name associated with the channel. 163 | string name = 2; 164 | // Intentionally don't use field numbers from other refs. 165 | reserved 3, 4, 5, 6, 7, 8; 166 | } 167 | 168 | // SubchannelRef is a reference to a Subchannel. 169 | message SubchannelRef { 170 | // The globally unique id for this subchannel. Must be a positive number. 171 | int64 subchannel_id = 7; 172 | // An optional name associated with the subchannel. 173 | string name = 8; 174 | // Intentionally don't use field numbers from other refs. 175 | reserved 1, 2, 3, 4, 5, 6; 176 | } 177 | 178 | // SocketRef is a reference to a Socket. 179 | message SocketRef { 180 | // The globally unique id for this socket. Must be a positive number. 181 | int64 socket_id = 3; 182 | // An optional name associated with the socket. 183 | string name = 4; 184 | // Intentionally don't use field numbers from other refs. 185 | reserved 1, 2, 5, 6, 7, 8; 186 | } 187 | 188 | // ServerRef is a reference to a Server. 189 | message ServerRef { 190 | // A globally unique identifier for this server. Must be a positive number. 191 | int64 server_id = 5; 192 | // An optional name associated with the server. 193 | string name = 6; 194 | // Intentionally don't use field numbers from other refs. 195 | reserved 1, 2, 3, 4, 7, 8; 196 | } 197 | 198 | // Server represents a single server. There may be multiple servers in a single 199 | // program. 200 | message Server { 201 | // The identifier for a Server. This should be set. 202 | ServerRef ref = 1; 203 | // The associated data of the Server. 204 | ServerData data = 2; 205 | 206 | // The sockets that the server is listening on. There are no ordering 207 | // guarantees. This may be absent. 208 | repeated SocketRef listen_socket = 3; 209 | } 210 | 211 | // ServerData is data for a specific Server. 212 | message ServerData { 213 | // A trace of recent events on the server. May be absent. 214 | ChannelTrace trace = 1; 215 | 216 | // The number of incoming calls started on the server 217 | int64 calls_started = 2; 218 | // The number of incoming calls that have completed with an OK status 219 | int64 calls_succeeded = 3; 220 | // The number of incoming calls that have a completed with a non-OK status 221 | int64 calls_failed = 4; 222 | 223 | // The last time a call was started on the server. 224 | google.protobuf.Timestamp last_call_started_timestamp = 5; 225 | } 226 | 227 | // Information about an actual connection. Pronounced "sock-ay". 228 | message Socket { 229 | // The identifier for the Socket. 230 | SocketRef ref = 1; 231 | 232 | // Data specific to this Socket. 233 | SocketData data = 2; 234 | // The locally bound address. 235 | Address local = 3; 236 | // The remote bound address. May be absent. 237 | Address remote = 4; 238 | // Security details for this socket. May be absent if not available, or 239 | // there is no security on the socket. 240 | Security security = 5; 241 | 242 | // Optional, represents the name of the remote endpoint, if different than 243 | // the original target name. 244 | string remote_name = 6; 245 | } 246 | 247 | // SocketData is data associated for a specific Socket. The fields present 248 | // are specific to the implementation, so there may be minor differences in 249 | // the semantics. (e.g. flow control windows) 250 | message SocketData { 251 | // The number of streams that have been started. 252 | int64 streams_started = 1; 253 | // The number of streams that have ended successfully: 254 | // On client side, received frame with eos bit set; 255 | // On server side, sent frame with eos bit set. 256 | int64 streams_succeeded = 2; 257 | // The number of streams that have ended unsuccessfully: 258 | // On client side, ended without receiving frame with eos bit set; 259 | // On server side, ended without sending frame with eos bit set. 260 | int64 streams_failed = 3; 261 | // The number of grpc messages successfully sent on this socket. 262 | int64 messages_sent = 4; 263 | // The number of grpc messages received on this socket. 264 | int64 messages_received = 5; 265 | 266 | // The number of keep alives sent. This is typically implemented with HTTP/2 267 | // ping messages. 268 | int64 keep_alives_sent = 6; 269 | 270 | // The last time a stream was created by this endpoint. Usually unset for 271 | // servers. 272 | google.protobuf.Timestamp last_local_stream_created_timestamp = 7; 273 | // The last time a stream was created by the remote endpoint. Usually unset 274 | // for clients. 275 | google.protobuf.Timestamp last_remote_stream_created_timestamp = 8; 276 | 277 | // The last time a message was sent by this endpoint. 278 | google.protobuf.Timestamp last_message_sent_timestamp = 9; 279 | // The last time a message was received by this endpoint. 280 | google.protobuf.Timestamp last_message_received_timestamp = 10; 281 | 282 | // The amount of window, granted to the local endpoint by the remote endpoint. 283 | // This may be slightly out of date due to network latency. This does NOT 284 | // include stream level or TCP level flow control info. 285 | google.protobuf.Int64Value local_flow_control_window = 11; 286 | 287 | // The amount of window, granted to the remote endpoint by the local endpoint. 288 | // This may be slightly out of date due to network latency. This does NOT 289 | // include stream level or TCP level flow control info. 290 | google.protobuf.Int64Value remote_flow_control_window = 12; 291 | 292 | // Socket options set on this socket. May be absent if 'summary' is set 293 | // on GetSocketRequest. 294 | repeated SocketOption option = 13; 295 | } 296 | 297 | // Address represents the address used to create the socket. 298 | message Address { 299 | message TcpIpAddress { 300 | // Either the IPv4 or IPv6 address in bytes. Will be either 4 bytes or 16 301 | // bytes in length. 302 | bytes ip_address = 1; 303 | // 0-64k, or -1 if not appropriate. 304 | int32 port = 2; 305 | } 306 | // A Unix Domain Socket address. 307 | message UdsAddress { 308 | string filename = 1; 309 | } 310 | // An address type not included above. 311 | message OtherAddress { 312 | // The human readable version of the value. This value should be set. 313 | string name = 1; 314 | // The actual address message. 315 | google.protobuf.Any value = 2; 316 | } 317 | 318 | oneof address { 319 | TcpIpAddress tcpip_address = 1; 320 | UdsAddress uds_address = 2; 321 | OtherAddress other_address = 3; 322 | } 323 | } 324 | 325 | // Security represents details about how secure the socket is. 326 | message Security { 327 | message Tls { 328 | oneof cipher_suite { 329 | // The cipher suite name in the RFC 4346 format: 330 | // https://tools.ietf.org/html/rfc4346#appendix-C 331 | string standard_name = 1; 332 | // Some other way to describe the cipher suite if 333 | // the RFC 4346 name is not available. 334 | string other_name = 2; 335 | } 336 | // the certificate used by this endpoint. 337 | bytes local_certificate = 3; 338 | // the certificate used by the remote endpoint. 339 | bytes remote_certificate = 4; 340 | } 341 | message OtherSecurity { 342 | // The human readable version of the value. 343 | string name = 1; 344 | // The actual security details message. 345 | google.protobuf.Any value = 2; 346 | } 347 | oneof model { 348 | Tls tls = 1; 349 | OtherSecurity other = 2; 350 | } 351 | } 352 | 353 | // SocketOption represents socket options for a socket. Specifically, these 354 | // are the options returned by getsockopt(). 355 | message SocketOption { 356 | // The full name of the socket option. Typically this will be the upper case 357 | // name, such as "SO_REUSEPORT". 358 | string name = 1; 359 | // The human readable value of this socket option. At least one of value or 360 | // additional will be set. 361 | string value = 2; 362 | // Additional data associated with the socket option. At least one of value 363 | // or additional will be set. 364 | google.protobuf.Any additional = 3; 365 | } 366 | 367 | // For use with SocketOption's additional field. This is primarily used for 368 | // SO_RCVTIMEO and SO_SNDTIMEO 369 | message SocketOptionTimeout { 370 | google.protobuf.Duration duration = 1; 371 | } 372 | 373 | // For use with SocketOption's additional field. This is primarily used for 374 | // SO_LINGER. 375 | message SocketOptionLinger { 376 | // active maps to `struct linger.l_onoff` 377 | bool active = 1; 378 | // duration maps to `struct linger.l_linger` 379 | google.protobuf.Duration duration = 2; 380 | } 381 | 382 | // For use with SocketOption's additional field. Tcp info for 383 | // SOL_TCP and TCP_INFO. 384 | message SocketOptionTcpInfo { 385 | uint32 tcpi_state = 1; 386 | 387 | uint32 tcpi_ca_state = 2; 388 | uint32 tcpi_retransmits = 3; 389 | uint32 tcpi_probes = 4; 390 | uint32 tcpi_backoff = 5; 391 | uint32 tcpi_options = 6; 392 | uint32 tcpi_snd_wscale = 7; 393 | uint32 tcpi_rcv_wscale = 8; 394 | 395 | uint32 tcpi_rto = 9; 396 | uint32 tcpi_ato = 10; 397 | uint32 tcpi_snd_mss = 11; 398 | uint32 tcpi_rcv_mss = 12; 399 | 400 | uint32 tcpi_unacked = 13; 401 | uint32 tcpi_sacked = 14; 402 | uint32 tcpi_lost = 15; 403 | uint32 tcpi_retrans = 16; 404 | uint32 tcpi_fackets = 17; 405 | 406 | uint32 tcpi_last_data_sent = 18; 407 | uint32 tcpi_last_ack_sent = 19; 408 | uint32 tcpi_last_data_recv = 20; 409 | uint32 tcpi_last_ack_recv = 21; 410 | 411 | uint32 tcpi_pmtu = 22; 412 | uint32 tcpi_rcv_ssthresh = 23; 413 | uint32 tcpi_rtt = 24; 414 | uint32 tcpi_rttvar = 25; 415 | uint32 tcpi_snd_ssthresh = 26; 416 | uint32 tcpi_snd_cwnd = 27; 417 | uint32 tcpi_advmss = 28; 418 | uint32 tcpi_reordering = 29; 419 | } 420 | 421 | // Channelz is a service exposed by gRPC servers that provides detailed debug 422 | // information. 423 | service Channelz { 424 | // Gets all root channels (i.e. channels the application has directly 425 | // created). This does not include subchannels nor non-top level channels. 426 | rpc GetTopChannels(GetTopChannelsRequest) returns (GetTopChannelsResponse); 427 | // Gets all servers that exist in the process. 428 | rpc GetServers(GetServersRequest) returns (GetServersResponse); 429 | // Returns a single Server, or else a NOT_FOUND code. 430 | rpc GetServer(GetServerRequest) returns (GetServerResponse); 431 | // Gets all server sockets that exist in the process. 432 | rpc GetServerSockets(GetServerSocketsRequest) returns (GetServerSocketsResponse); 433 | // Returns a single Channel, or else a NOT_FOUND code. 434 | rpc GetChannel(GetChannelRequest) returns (GetChannelResponse); 435 | // Returns a single Subchannel, or else a NOT_FOUND code. 436 | rpc GetSubchannel(GetSubchannelRequest) returns (GetSubchannelResponse); 437 | // Returns a single Socket or else a NOT_FOUND code. 438 | rpc GetSocket(GetSocketRequest) returns (GetSocketResponse); 439 | } 440 | 441 | message GetTopChannelsRequest { 442 | // start_channel_id indicates that only channels at or above this id should be 443 | // included in the results. 444 | // To request the first page, this should be set to 0. To request 445 | // subsequent pages, the client generates this value by adding 1 to 446 | // the highest seen result ID. 447 | int64 start_channel_id = 1; 448 | 449 | // If non-zero, the server will return a page of results containing 450 | // at most this many items. If zero, the server will choose a 451 | // reasonable page size. Must never be negative. 452 | int64 max_results = 2; 453 | } 454 | 455 | message GetTopChannelsResponse { 456 | // list of channels that the connection detail service knows about. Sorted in 457 | // ascending channel_id order. 458 | // Must contain at least 1 result, otherwise 'end' must be true. 459 | repeated Channel channel = 1; 460 | // If set, indicates that the list of channels is the final list. Requesting 461 | // more channels can only return more if they are created after this RPC 462 | // completes. 463 | bool end = 2; 464 | } 465 | 466 | message GetServersRequest { 467 | // start_server_id indicates that only servers at or above this id should be 468 | // included in the results. 469 | // To request the first page, this must be set to 0. To request 470 | // subsequent pages, the client generates this value by adding 1 to 471 | // the highest seen result ID. 472 | int64 start_server_id = 1; 473 | 474 | // If non-zero, the server will return a page of results containing 475 | // at most this many items. If zero, the server will choose a 476 | // reasonable page size. Must never be negative. 477 | int64 max_results = 2; 478 | } 479 | 480 | message GetServersResponse { 481 | // list of servers that the connection detail service knows about. Sorted in 482 | // ascending server_id order. 483 | // Must contain at least 1 result, otherwise 'end' must be true. 484 | repeated Server server = 1; 485 | // If set, indicates that the list of servers is the final list. Requesting 486 | // more servers will only return more if they are created after this RPC 487 | // completes. 488 | bool end = 2; 489 | } 490 | 491 | message GetServerRequest { 492 | // server_id is the identifier of the specific server to get. 493 | int64 server_id = 1; 494 | } 495 | 496 | message GetServerResponse { 497 | // The Server that corresponds to the requested server_id. This field 498 | // should be set. 499 | Server server = 1; 500 | } 501 | 502 | message GetServerSocketsRequest { 503 | int64 server_id = 1; 504 | // start_socket_id indicates that only sockets at or above this id should be 505 | // included in the results. 506 | // To request the first page, this must be set to 0. To request 507 | // subsequent pages, the client generates this value by adding 1 to 508 | // the highest seen result ID. 509 | int64 start_socket_id = 2; 510 | 511 | // If non-zero, the server will return a page of results containing 512 | // at most this many items. If zero, the server will choose a 513 | // reasonable page size. Must never be negative. 514 | int64 max_results = 3; 515 | } 516 | 517 | message GetServerSocketsResponse { 518 | // list of socket refs that the connection detail service knows about. Sorted in 519 | // ascending socket_id order. 520 | // Must contain at least 1 result, otherwise 'end' must be true. 521 | repeated SocketRef socket_ref = 1; 522 | // If set, indicates that the list of sockets is the final list. Requesting 523 | // more sockets will only return more if they are created after this RPC 524 | // completes. 525 | bool end = 2; 526 | } 527 | 528 | message GetChannelRequest { 529 | // channel_id is the identifier of the specific channel to get. 530 | int64 channel_id = 1; 531 | } 532 | 533 | message GetChannelResponse { 534 | // The Channel that corresponds to the requested channel_id. This field 535 | // should be set. 536 | Channel channel = 1; 537 | } 538 | 539 | message GetSubchannelRequest { 540 | // subchannel_id is the identifier of the specific subchannel to get. 541 | int64 subchannel_id = 1; 542 | } 543 | 544 | message GetSubchannelResponse { 545 | // The Subchannel that corresponds to the requested subchannel_id. This 546 | // field should be set. 547 | Subchannel subchannel = 1; 548 | } 549 | 550 | message GetSocketRequest { 551 | // socket_id is the identifier of the specific socket to get. 552 | int64 socket_id = 1; 553 | 554 | // If true, the response will contain only high level information 555 | // that is inexpensive to obtain. Fields thay may be omitted are 556 | // documented. 557 | bool summary = 2; 558 | } 559 | 560 | message GetSocketResponse { 561 | // The Socket that corresponds to the requested socket_id. This field 562 | // should be set. 563 | Socket socket = 1; 564 | } -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={296:e=>{var r=Object.prototype.toString;var n=typeof Buffer!=="undefined"&&typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},599:(e,r,n)=>{e=n.nmd(e);var t=n(927).SourceMapConsumer;var o=n(928);var i;try{i=n(896);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(296);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var d=[];var h=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=d.slice(0);var _=h.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){d.length=0}d.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){h.length=0}h.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){d.length=0;h.length=0;d=S.slice(0);h=_.slice(0);v=handlerExec(h);m=handlerExec(d)}},517:(e,r,n)=>{var t=n(297);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(158);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},24:(e,r,n)=>{var t=n(297);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.P=MappingList},299:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(297);var i=n(197);var a=n(517).C;var u=n(818);var s=n(299).g;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){h.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(h,o.compareByOriginalPositions);this.__originalMappings=h};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(818);var o=n(297);var i=n(517).C;var a=n(24).P;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var d=0,h=g.length;d0){if(!o.compareByGeneratedPositionsInflated(c,g[d-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.x=SourceMapGenerator},565:(e,r,n)=>{var t;var o=n(163).x;var i=n(297);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},927:(e,r,n)=>{n(163).x;r.SourceMapConsumer=n(684).SourceMapConsumer;n(565)},896:e=>{"use strict";e.exports=require("fs")},928:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};__webpack_require__(599).install();module.exports=n})(); -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import github from 'eslint-plugin-github' 2 | 3 | export default [ 4 | // github.getFlatConfigs().browser, 5 | github.getFlatConfigs().recommended, 6 | github.getFlatConfigs().react, 7 | ...github.getFlatConfigs().typescript, 8 | { 9 | files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], 10 | ignores: ['eslint.config.mjs'], 11 | rules: { 12 | 'github/array-foreach': 'error', 13 | 'github/async-preventdefault': 'warn', 14 | 'github/no-then': 'error', 15 | 'github/no-blur': 'error', 16 | 'i18n-text/no-en': 'off' 17 | }, 18 | settings: { 19 | 'import/resolver': { 20 | typescript: { 21 | alwaysTryTypes: false, 22 | project: './tsconfig.json' 23 | } 24 | } 25 | } 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.ts$': 'ts-jest' 7 | }, 8 | verbose: true, 9 | setupFiles: ["dotenv/config"], 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yc-github-runner", 3 | "version": "2.0.0", 4 | "description": "GitHub Action to On-demand self-hosted YC runner for GitHub Actions.", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "format": "prettier --write '**/*.ts'", 9 | "format-check": "prettier --check '**/*.ts'", 10 | "lint": "eslint src/**/*.ts --fix", 11 | "package": "ncc build --source-map --license licenses.txt", 12 | "test": "GITHUB_REPOSITORY=owner/repo GITHUB_WORKSPACE=__tests__ jest --verbose", 13 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test", 14 | "git-tag": "git tag v`cat package.json | jq -r '.version' | awk -F. '{print $1}'` -f && git tag v`cat package.json | jq -r '.version'` -f" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/yc-actions/yc-github-runner.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "yandex cloud", 23 | "runner", 24 | "github action" 25 | ], 26 | "author": "Nikolay Matrosov", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@actions/core": "^1.11.1", 30 | "@actions/github": "^6.0.1", 31 | "@grpc/grpc-js": "1.13.4", 32 | "@yandex-cloud/nodejs-sdk": "^2.9.0", 33 | "js-yaml": "^4.1.0", 34 | "moment": "^2.30.1", 35 | "axios": "^1.9.0" 36 | }, 37 | "devDependencies": { 38 | "@eslint/eslintrc": "^3.3.1", 39 | "@eslint/js": "^9.28.0", 40 | "@stylistic/eslint-plugin-ts": "^4.4.1", 41 | "@types/js-yaml": "^4.0.9", 42 | "@types/node": "^22.15.30", 43 | "@typescript-eslint/eslint-plugin": "^8.34.0", 44 | "@typescript-eslint/parser": "^8.34.0", 45 | "@vercel/ncc": "^0.38.3", 46 | "dotenv": "^16.5.0", 47 | "eslint": "^9.28.0", 48 | "eslint-import-resolver-typescript": "^4.4.3", 49 | "eslint-plugin-github": "^6.0.0", 50 | "eslint-plugin-jest": "^28.13.0", 51 | "eslint-plugin-sort-imports-es6-autofix": "^0.6.0", 52 | "globals": "^16.2.0", 53 | "jest": "^29.7.0", 54 | "js-yaml": "^4.1.0", 55 | "prettier": "3.5.3", 56 | "ts-jest": "^29.3.4", 57 | "typescript": "^5.8.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { startGroup, getInput, getBooleanInput, endGroup } from '@actions/core' 2 | import { context } from '@actions/github' 3 | import { parseMemory } from './memory' 4 | import moment from 'moment' 5 | 6 | export interface ResourcesSpec { 7 | memory: number 8 | cores: number 9 | coreFraction: number 10 | } 11 | 12 | export interface ActionConfig { 13 | imageId: string 14 | mode: string 15 | githubToken: string 16 | runnerHomeDir: string 17 | label: string 18 | subnetId: string 19 | publicIp: boolean 20 | serviceAccountId: string 21 | diskType: string 22 | diskSize: number 23 | folderId: string 24 | zoneId: string 25 | platformId: string 26 | resourcesSpec: ResourcesSpec 27 | 28 | secondDiskImageId: string 29 | secondDiskType: string 30 | secondDiskSize: number 31 | 32 | user: string 33 | sshPublicKey: string 34 | 35 | instanceId?: string 36 | 37 | runnerVersion: string 38 | ttl?: moment.Duration 39 | disableUpdate: boolean 40 | } 41 | 42 | export interface GithubRepo { 43 | owner: string 44 | repo: string 45 | } 46 | 47 | export class Config { 48 | input: ActionConfig 49 | githubContext: GithubRepo 50 | 51 | constructor(input?: ActionConfig) { 52 | this.input = input ?? parseVmInputs() 53 | 54 | // the values of github.context.repo.owner and github.context.repo.repo are taken from 55 | // the environment variable GITHUB_REPOSITORY specified in "owner/repo" format and 56 | // provided by the GitHub Action on the runtime 57 | this.githubContext = { 58 | owner: context.repo.owner, 59 | repo: context.repo.repo 60 | } 61 | 62 | // 63 | // validate input 64 | // 65 | 66 | if (!this.input.mode) { 67 | throw new Error(`The 'mode' input is not specified`) 68 | } 69 | 70 | if (!this.input.githubToken) { 71 | throw new Error(`The 'github-token' input is not specified`) 72 | } 73 | 74 | if (this.input.mode === 'start') { 75 | if (!this.input.imageId || !this.input.subnetId || !this.input.folderId) { 76 | throw new Error(`Not all the required inputs are provided for the 'start' mode`) 77 | } 78 | 79 | if (this.input.secondDiskSize > 0 && !this.input.secondDiskImageId) { 80 | throw new Error(`Secondary disk image id is missing`) 81 | } 82 | } else if (this.input.mode === 'stop') { 83 | if (!this.input.label || !this.input.instanceId) { 84 | throw new Error(`Not all the required inputs are provided for the 'stop' mode`) 85 | } 86 | } else { 87 | throw new Error('Wrong mode. Allowed values: start, stop.') 88 | } 89 | } 90 | 91 | generateUniqueLabel(): string { 92 | return Math.random().toString(36).slice(2, 7) 93 | } 94 | } 95 | 96 | function parseVmInputs(): ActionConfig { 97 | startGroup('Parsing Action Inputs') 98 | 99 | const folderId: string = getInput('folder-id') 100 | 101 | const mode = getInput('mode') 102 | const githubToken = getInput('github-token') 103 | const runnerHomeDir = getInput('runner-home-dir') 104 | const label = getInput('label') 105 | 106 | const serviceAccountId: string = getInput('service-account-id') 107 | 108 | const imageId: string = getInput('image-id') 109 | const zoneId: string = getInput('zone-id') || 'ru-central1-a' 110 | const subnetId: string = getInput('subnet-id') 111 | const publicIp: boolean = getBooleanInput('public-ip', { required: false }) 112 | const platformId: string = getInput('platform-id') || 'standard-v3' 113 | const cores: number = parseInt(getInput('cores') || '2', 10) 114 | const memory: number = parseMemory(getInput('memory') || '1Gb') 115 | const diskType: string = getInput('disk-type') || 'network-ssd' 116 | const diskSize: number = parseMemory(getInput('disk-size') || '30Gb') 117 | const coreFraction: number = parseInt(getInput('core-fraction') || '100', 10) 118 | 119 | const secondDiskImageId: string = getInput('image2-id') 120 | const secondDiskType: string = getInput('disk2-type') || 'network-ssd' 121 | const secondDiskSize: number = parseMemory(getInput('disk2-size') || '0Gb') 122 | 123 | const user: string = getInput('user') 124 | const sshPublicKey: string = getInput('ssh-public-key') 125 | 126 | const instanceId: string = getInput('instance-id', { required: false }) 127 | 128 | const runnerVersion: string = getInput('runner-version', { required: false }) 129 | const disableUpdate: boolean = getBooleanInput('disable-update', { required: false }) 130 | 131 | let ttl: moment.Duration | undefined = undefined 132 | const ttlInput = getInput('ttl', { required: false }) 133 | if (ttlInput) { 134 | ttl = moment.duration(ttlInput) 135 | } 136 | 137 | endGroup() 138 | return { 139 | instanceId, 140 | imageId, 141 | diskType, 142 | diskSize, 143 | subnetId, 144 | publicIp, 145 | zoneId, 146 | platformId, 147 | folderId, 148 | mode, 149 | githubToken, 150 | runnerHomeDir, 151 | label, 152 | serviceAccountId, 153 | secondDiskImageId, 154 | secondDiskType, 155 | secondDiskSize, 156 | user, 157 | sshPublicKey, 158 | runnerVersion, 159 | ttl, 160 | disableUpdate, 161 | resourcesSpec: { 162 | cores, 163 | memory, 164 | coreFraction 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/gh.ts: -------------------------------------------------------------------------------- 1 | import { info, error as core_error } from '@actions/core' 2 | import { getOctokit } from '@actions/github' 3 | import { Config } from './config' 4 | 5 | interface Runner { 6 | /** The id of the runner. */ 7 | id: number 8 | /** The name of the runner. */ 9 | name: string 10 | /** The Operating System of the runner. */ 11 | os: string 12 | /** The status of the runner. */ 13 | status: string 14 | busy: boolean 15 | labels: Array<{ 16 | /** Unique identifier of the label. */ 17 | id?: number 18 | /** Name of the label. */ 19 | name?: string 20 | /** The type of label. Read-only labels are applied automatically when the runner is configured. */ 21 | type?: 'read-only' | 'custom' 22 | }> 23 | } 24 | 25 | // use the unique label to find the runner 26 | // as we don't have the runner's id, it's not possible to get it in any other way 27 | export async function getRunner(config: Config, label: string): Promise { 28 | const octokit = getOctokit(config.input.githubToken) 29 | 30 | try { 31 | const runners = (await octokit.paginate( 32 | 'GET /repos/{owner}/{repo}/actions/runners', 33 | config.githubContext 34 | )) as Runner[] 35 | const foundRunners = runners.filter(x => x.labels.some(l => l.name === label)) 36 | return foundRunners.length > 0 ? foundRunners[0] : null 37 | } catch (error) { 38 | info(`GitHub self-hosted runner receiving error: ${error}`) 39 | return null 40 | } 41 | } 42 | 43 | // get GitHub Registration Token for registering a self-hosted runner 44 | export async function getRegistrationToken(config: Config): Promise { 45 | const octokit = getOctokit(config.input.githubToken) 46 | 47 | try { 48 | const req = { 49 | ...config.githubContext 50 | } 51 | const response = await octokit.request('POST /repos/{owner}/{repo}/actions/runners/registration-token', req) 52 | info('GitHub Registration Token is received') 53 | return response.data.token 54 | } catch (error) { 55 | core_error('GitHub Registration Token receiving error') 56 | throw error 57 | } 58 | } 59 | 60 | export async function removeRunner(config: Config): Promise { 61 | const runner = await getRunner(config, config.input.label) 62 | const octokit = getOctokit(config.input.githubToken) 63 | 64 | // skip the runner removal process if the runner is not found 65 | if (!runner) { 66 | info(`GitHub self-hosted runner with label ${config.input.label} is not found, so the removal is skipped`) 67 | return 68 | } 69 | 70 | try { 71 | const req = { 72 | ...config.githubContext, 73 | runner_id: runner.id 74 | } 75 | await octokit.request('DELETE /repos/{owner}/{repo}/actions/runners/{runner_id}', req) 76 | info(`GitHub self-hosted runner ${runner.name} is removed`) 77 | return 78 | } catch (error) { 79 | core_error('GitHub self-hosted runner removal error') 80 | throw error 81 | } 82 | } 83 | 84 | export async function waitForRunnerRegistered(config: Config, label: string): Promise { 85 | const timeoutMinutes = 5 86 | const retryIntervalSeconds = 10 87 | const quietPeriodSeconds = 30 88 | let waitSeconds = 0 89 | 90 | info(`Waiting ${quietPeriodSeconds}s for the instance to be registered in GitHub as a new self-hosted runner`) 91 | await new Promise(r => setTimeout(r, quietPeriodSeconds * 1000)) 92 | info(`Checking every ${retryIntervalSeconds}s if the GitHub self-hosted runner is registered`) 93 | 94 | return new Promise((resolve, reject) => { 95 | const interval = setInterval(async () => { 96 | const runner = await getRunner(config, label) 97 | 98 | if (waitSeconds > timeoutMinutes * 60) { 99 | core_error('GitHub self-hosted runner registration error') 100 | clearInterval(interval) 101 | 102 | reject( 103 | new Error( 104 | `A timeout of ${timeoutMinutes} minutes is exceeded. Your YC instance was not able to register itself in GitHub as a new self-hosted runner.` 105 | ) 106 | ) 107 | } 108 | 109 | if (runner && runner.status === 'online') { 110 | info(`GitHub self-hosted runner ${runner.name} is registered and ready to use`) 111 | clearInterval(interval) 112 | resolve() 113 | } else { 114 | waitSeconds += retryIntervalSeconds 115 | info('Checking...') 116 | } 117 | }, retryIntervalSeconds * 1000) 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { 3 | endGroup, 4 | error as core_error, 5 | getIDToken, 6 | getInput, 7 | info, 8 | setCommandEcho, 9 | setFailed, 10 | setOutput, 11 | startGroup 12 | } from '@actions/core' 13 | import { context } from '@actions/github' 14 | import { 15 | decodeMessage, 16 | errors, 17 | serviceClients, 18 | Session, 19 | waitForOperation, 20 | WrappedServiceClientType 21 | } from '@yandex-cloud/nodejs-sdk' 22 | import { Instance, IpVersion } from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/compute/v1/instance' 23 | import { 24 | AttachedDiskSpec, 25 | AttachedDiskSpec_Mode, 26 | CreateInstanceMetadata, 27 | CreateInstanceRequest, 28 | DeleteInstanceRequest, 29 | InstanceServiceService, 30 | NetworkInterfaceSpec 31 | } from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/compute/v1/instance_service' 32 | import { dump, load } from 'js-yaml' 33 | import { Config, GithubRepo } from './config' 34 | import { getRegistrationToken, removeRunner, waitForRunnerRegistered } from './gh' 35 | import { fromServiceAccountJsonFile } from './service-account-json' 36 | import moment from 'moment' 37 | import { SessionConfig } from '@yandex-cloud/nodejs-sdk/dist/types' 38 | 39 | let config: Config 40 | 41 | try { 42 | config = new Config() 43 | } catch (error) { 44 | const err = error as Error 45 | core_error(err) 46 | setFailed(err.message) 47 | } 48 | 49 | interface BuildUserDataScriptParams { 50 | githubRegistrationToken: string 51 | label: string 52 | runnerHomeDir: string 53 | owner: string 54 | repo: string 55 | user: string 56 | sshPublicKey: string 57 | runnerVersion: string 58 | disableUpdate: boolean 59 | } 60 | 61 | // User data scripts are run as the root user 62 | export function buildUserDataScript(params: BuildUserDataScriptParams): string[] { 63 | const { githubRegistrationToken, label, runnerHomeDir, repo, owner, user, sshPublicKey } = params 64 | let script: string[] 65 | 66 | if (runnerHomeDir) { 67 | // If runner home directory is specified, we expect the actions-runner software (and dependencies) 68 | // to be pre-installed in the image, so we simply cd into that directory and then start the runner 69 | script = [ 70 | '#!/bin/bash', 71 | `cd "${runnerHomeDir}"`, 72 | 'export RUNNER_ALLOW_RUNASROOT=1', 73 | `./config.sh --url https://github.com/${owner}/${repo} --token ${githubRegistrationToken} --labels ${label}${params.disableUpdate ? ' --disableupdate' : ''}`, 74 | './run.sh' 75 | ] 76 | } else { 77 | const version = params.runnerVersion 78 | script = [ 79 | '#!/bin/bash', 80 | 'mkdir actions-runner && cd actions-runner', 81 | 'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}', 82 | `curl -O -L https://github.com/actions/runner/releases/download/v${version}/actions-runner-linux-\${RUNNER_ARCH}-${version}.tar.gz`, 83 | `tar xzf ./actions-runner-linux-\${RUNNER_ARCH}-${version}.tar.gz`, 84 | 'export RUNNER_ALLOW_RUNASROOT=1', 85 | `./config.sh --url https://github.com/${owner}/${repo} --token ${githubRegistrationToken} --labels ${label}${params.disableUpdate ? ' --disableupdate' : ''}`, 86 | './run.sh' 87 | ] 88 | } 89 | if (user !== '' && sshPublicKey !== '') { 90 | const cloudInit = load(`ssh_pwauth: no 91 | users: 92 | - name: ${user} 93 | sudo: ALL=(ALL) NOPASSWD:ALL 94 | shell: /bin/bash 95 | ssh_authorized_keys: 96 | - "${sshPublicKey}"`) as Record 97 | cloudInit['runcmd'] = script.slice(1) 98 | return ['#cloud-config', ...dump(cloudInit).split('\n')] 99 | } else { 100 | return script 101 | } 102 | } 103 | 104 | async function createVm( 105 | session: Session, 106 | instanceService: WrappedServiceClientType, 107 | repo: GithubRepo, 108 | githubRegistrationToken: string, 109 | label: string 110 | ): Promise { 111 | startGroup('Create VM') 112 | 113 | const secondaryDiskSpecs: AttachedDiskSpec[] = [] 114 | 115 | if (config.input.secondDiskSize > 0) { 116 | secondaryDiskSpecs.push( 117 | AttachedDiskSpec.fromPartial({ 118 | autoDelete: true, 119 | mode: AttachedDiskSpec_Mode.READ_WRITE, 120 | diskSpec: { 121 | imageId: config.input.secondDiskImageId, 122 | size: config.input.secondDiskSize, 123 | typeId: config.input.secondDiskType 124 | } 125 | }) 126 | ) 127 | } 128 | let primaryV4AddressSpec = {} 129 | 130 | if (config.input.publicIp) { 131 | primaryV4AddressSpec = { 132 | oneToOneNatSpec: { 133 | ipVersion: IpVersion.IPV4 134 | } 135 | } 136 | } 137 | 138 | const networkInterfaceSpec = NetworkInterfaceSpec.fromPartial({ 139 | subnetId: config.input.subnetId, 140 | primaryV4AddressSpec 141 | }) 142 | 143 | const labels: Record = {} 144 | 145 | if (config.input.ttl) { 146 | // Set `expires` label to the current time + TTL Duration 147 | // Instance won't automatically be destroyed by Yandex.Cloud, you should handle it yourself 148 | // For example, by using Cron trigger that will call Cloud Function to destroy the instance. 149 | labels['expires'] = moment.utc().add(config.input.ttl).unix().toString() 150 | } 151 | 152 | const op = await instanceService.create( 153 | CreateInstanceRequest.fromPartial({ 154 | folderId: config.input.folderId, 155 | description: `Runner for: ${repo.owner}/${repo.repo}`, 156 | zoneId: config.input.zoneId, 157 | platformId: config.input.platformId, 158 | resourcesSpec: config.input.resourcesSpec, 159 | metadata: { 160 | 'user-data': buildUserDataScript({ 161 | githubRegistrationToken, 162 | label, 163 | runnerHomeDir: config.input.runnerHomeDir, 164 | user: config.input.user, 165 | sshPublicKey: config.input.sshPublicKey, 166 | repo: config.githubContext.repo, 167 | owner: config.githubContext.owner, 168 | runnerVersion: config.input.runnerVersion, 169 | disableUpdate: config.input.disableUpdate 170 | }).join('\n') 171 | }, 172 | labels, 173 | 174 | bootDiskSpec: { 175 | mode: AttachedDiskSpec_Mode.READ_WRITE, 176 | autoDelete: true, 177 | diskSpec: { 178 | typeId: config.input.diskType, 179 | size: config.input.diskSize, 180 | imageId: config.input.imageId 181 | } 182 | }, 183 | secondaryDiskSpecs, 184 | networkInterfaceSpecs: [networkInterfaceSpec], 185 | serviceAccountId: config.input.serviceAccountId 186 | }) 187 | ) 188 | const finishedOp = await waitForOperation(op, session) 189 | if (finishedOp.response) { 190 | const instanceId = decodeMessage(finishedOp.response).id 191 | info(`Created instance with id '${instanceId}'`) 192 | endGroup() 193 | return instanceId 194 | } else { 195 | core_error(`Failed to create instance'`) 196 | endGroup() 197 | throw new Error('Failed to create instance') 198 | } 199 | } 200 | 201 | async function destroyVm( 202 | session: Session, 203 | instanceService: WrappedServiceClientType 204 | ): Promise { 205 | startGroup('Create VM') 206 | 207 | const op = await instanceService.delete( 208 | DeleteInstanceRequest.fromPartial({ 209 | instanceId: config.input.instanceId 210 | }) 211 | ) 212 | const finishedOp = await waitForOperation(op, session) 213 | if (finishedOp.metadata) { 214 | const instanceId = decodeMessage(finishedOp.metadata).instanceId 215 | info(`Destroyed instance with id '${instanceId}'`) 216 | } else { 217 | core_error(`Failed to create instance'`) 218 | throw new Error('Failed to create instance') 219 | } 220 | endGroup() 221 | } 222 | 223 | async function start( 224 | session: Session, 225 | instanceService: WrappedServiceClientType 226 | ): Promise { 227 | const label = config.generateUniqueLabel() 228 | const githubRegistrationToken = await getRegistrationToken(config) 229 | const instanceId = await createVm(session, instanceService, context.repo, githubRegistrationToken, label) 230 | setOutput('label', label) 231 | setOutput('instance-id', instanceId) 232 | await waitForRunnerRegistered(config, label) 233 | } 234 | 235 | async function stop( 236 | session: Session, 237 | instanceService: WrappedServiceClientType 238 | ): Promise { 239 | await destroyVm(session, instanceService) 240 | await removeRunner(config) 241 | } 242 | 243 | async function run(): Promise { 244 | setCommandEcho(true) 245 | try { 246 | info(`start`) 247 | let sessionConfig: SessionConfig = {} 248 | const ycSaJsonCredentials = getInput('yc-sa-json-credentials') 249 | const ycIamToken = getInput('yc-iam-token') 250 | const ycSaId = getInput('yc-sa-id') 251 | if (ycSaJsonCredentials !== '') { 252 | const serviceAccountJson = fromServiceAccountJsonFile(JSON.parse(ycSaJsonCredentials)) 253 | info('Parsed Service account JSON') 254 | sessionConfig = { serviceAccountJson } 255 | } else if (ycIamToken !== '') { 256 | sessionConfig = { iamToken: ycIamToken } 257 | info('Using IAM token') 258 | } else if (ycSaId !== '') { 259 | const ghToken = await getIDToken() 260 | if (!ghToken) { 261 | throw new Error('No credentials provided') 262 | } 263 | const saToken = await exchangeToken(ghToken, ycSaId) 264 | sessionConfig = { iamToken: saToken } 265 | } else { 266 | throw new Error('No credentials') 267 | } 268 | const session = new Session(sessionConfig) 269 | 270 | info(`Folder ID: ${config.input.folderId}`) 271 | 272 | const instanceService = session.client(serviceClients.InstanceServiceClient) 273 | 274 | switch (config.input.mode) { 275 | case 'start': { 276 | await start(session, instanceService) 277 | break 278 | } 279 | case 'stop': { 280 | await stop(session, instanceService) 281 | break 282 | } 283 | default: 284 | // noinspection ExceptionCaughtLocallyJS 285 | throw new Error(`Unknown mode ${config.input.mode}`) 286 | } 287 | } catch (error) { 288 | if (error instanceof errors.ApiError) { 289 | core_error(`${error.message}\nx-request-id: ${error.requestId}\nx-server-trace-id: ${error.serverTraceId}`) 290 | } 291 | setFailed(error as Error) 292 | } 293 | } 294 | 295 | async function exchangeToken(token: string, saId: string): Promise { 296 | info(`Exchanging token for service account ${saId}`) 297 | const res = await axios.post( 298 | 'https://auth.yandex.cloud/oauth/token', 299 | { 300 | grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', 301 | requested_token_type: 'urn:ietf:params:oauth:token-type:access_token', 302 | audience: saId, 303 | subject_token: token, 304 | subject_token_type: 'urn:ietf:params:oauth:token-type:id_token' 305 | }, 306 | { 307 | headers: { 308 | 'Content-Type': 'application/x-www-form-urlencoded' 309 | } 310 | } 311 | ) 312 | if (res.status !== 200) { 313 | throw new Error(`Failed to exchange token: ${res.status} ${res.statusText}`) 314 | } 315 | if (!res.data.access_token) { 316 | throw new Error(`Failed to exchange token: ${res.data.error} ${res.data.error_description}`) 317 | } 318 | info(`Token exchanged successfully`) 319 | return res.data.access_token 320 | } 321 | 322 | run() 323 | -------------------------------------------------------------------------------- /src/memory.ts: -------------------------------------------------------------------------------- 1 | export const MB = 1024 ** 2 2 | export const GB = 1024 * MB 3 | 4 | export function parseMemory(input: string): number { 5 | const match = input.match(/^(\d+)\s?(mb|gb)$/i) 6 | if (!match) { 7 | throw new Error('memory has unknown format') 8 | } 9 | const digits = parseInt(match[1], 10) 10 | const multiplier = match[2].toLowerCase() === 'mb' ? MB : GB 11 | return digits * multiplier 12 | } 13 | -------------------------------------------------------------------------------- /src/service-account-json.ts: -------------------------------------------------------------------------------- 1 | import { IIAmCredentials } from '@yandex-cloud/nodejs-sdk/dist/types' 2 | 3 | export interface ServiceAccountJsonFileContents { 4 | id: string 5 | created_at: string 6 | key_algorithm: string 7 | service_account_id: string 8 | private_key: string 9 | public_key: string 10 | } 11 | 12 | export function fromServiceAccountJsonFile(data: ServiceAccountJsonFileContents): IIAmCredentials { 13 | return { 14 | accessKeyId: data.id, 15 | privateKey: data.private_key, 16 | serviceAccountId: data.service_account_id 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "rootDir": "./src", 6 | "moduleResolution": "NodeNext", 7 | "baseUrl": "./", 8 | "sourceMap": true, 9 | "outDir": "./lib", 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "newLine": "lf" 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "**/*.test.ts", 20 | "**/__mocks__" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------